Compare commits
180 Commits
0.11.0-bet
...
0.13.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
06a57ded02
|
|||
| 6f92ba0deb | |||
| dcd830e3f8 | |||
| 8056703d59 | |||
|
566bdf2bd8
|
|||
|
24288c81d3
|
|||
|
2ef2a7ed2c
|
|||
|
cf8cd7423d
|
|||
| a18f57488f | |||
|
b2e691265a
|
|||
|
bff23f0ae6
|
|||
| 403c7a3e5b | |||
| 66b932a553 | |||
| f64e4b62cf | |||
| e80ecbc332 | |||
| abcbdf57f1 | |||
|
78899f173c
|
|||
| 90142cb783 | |||
|
8dbde3d158
|
|||
| 8f42e36302 | |||
| c2552ec2f6 | |||
|
8c64a8049d
|
|||
|
b073072489
|
|||
|
9daa4fee48
|
|||
| ea48917e6c | |||
| 728f873a3e | |||
|
ddb90dd44d
|
|||
|
7a8485492e
|
|||
| 32bb05abba | |||
|
3d2006a696
|
|||
| 521f5c1647 | |||
| 5eb41bc803 | |||
| fc39721501 | |||
| 44bacc582b | |||
|
53e8b52717
|
|||
|
0aba922dda
|
|||
| 4e0eb739b4 | |||
| 6b661dd7a7 | |||
|
39102752c0
|
|||
| 5cfc1c076c | |||
| 10f7ed74b0 | |||
|
227d37dc26
|
|||
|
0ccf3d2b12
|
|||
|
f87ce74027
|
|||
| 4349ee82bc | |||
|
f9ea7506d0
|
|||
|
1fe2d0421b
|
|||
| 59c0d1f4c5 | |||
|
7c3364f87a
|
|||
| 2c5a273fa7 | |||
|
c54fe3ef85
|
|||
| bda0d23d39 | |||
| b9dc7b8437 | |||
| 8f7dbfedbc | |||
| 4b863f1e15 | |||
| 064c9f5d65 | |||
| 98e48c95c7 | |||
| c85d8ee6d1 | |||
| 23268a0e92 | |||
| d60d426752 | |||
| 0e273de8f6 | |||
| ab32118bfe | |||
| 683396d75a | |||
| 4db6755f0d | |||
| 4c132e30f6 | |||
| f5aeae30c7 | |||
| 1d24107956 | |||
| f835b87255 | |||
|
dba21d6a29
|
|||
| 182fc41c58 | |||
| 304ac87cec | |||
| 5b3929d885 | |||
| c41df874d1 | |||
|
b721adbf9c
|
|||
| 42f9e6d458 | |||
| 9e7bc31d4d | |||
| b79c4f33b6 | |||
| cc87d5b3da | |||
| 8b5e3f3c78 | |||
|
db7c4042d0
|
|||
|
ed1a66dc5f
|
|||
|
bb93e4266a
|
|||
|
a2cc70b2f5
|
|||
|
ce1aa3d870
|
|||
|
d75700c8a9
|
|||
|
0ccc4aae72
|
|||
|
ec22d5d51d
|
|||
|
ab42584d05
|
|||
| 40eb6e9a18 | |||
| 35eb9d4a89 | |||
|
08cc63d523
|
|||
| 797b8d899b | |||
|
fb786306b5
|
|||
| c3a2048eba | |||
| 1bdc11ba62 | |||
| cc8703310c | |||
|
fcd5bd863d
|
|||
|
e6af2da9dd
|
|||
| 4b688825e0 | |||
| b0cf2a1f8e | |||
| 6b7020d457 | |||
| efdac610bd | |||
| cd6021f116 | |||
| ee8de8ef5c | |||
|
e5a653c002
|
|||
|
2cca04de90
|
|||
| f2f79e2df8 | |||
|
dd83741a9f
|
|||
|
dc2cd85d91
|
|||
|
96e59cf196
|
|||
|
11656c009d
|
|||
|
e4e1b58501
|
|||
|
3b8f12643c
|
|||
|
e5f5154197
|
|||
|
6c1c0a8a8a
|
|||
| 662f45008c | |||
|
708c5f5223
|
|||
| d58552b748 | |||
|
51fe809851
|
|||
| 3f6a22747f | |||
|
4e75b96914
|
|||
| fd4ee75ab7 | |||
|
964ed834ee
|
|||
| fcb3167394 | |||
|
3845b40aa3
|
|||
| 0dc5c307af | |||
| fc5855ff28 | |||
|
5b504a1550
|
|||
| fc16a21f1c | |||
|
7b4d2d7230
|
|||
|
d0ccb805c6
|
|||
|
2460dd9438
|
|||
| 9c648a2566 | |||
|
22ecfb9c4c
|
|||
|
9f3cf718be
|
|||
|
b737ce2107
|
|||
|
a3d0ece7cb
|
|||
|
d63a1c28ea
|
|||
| 1c10e64c58 | |||
|
21826ec555
|
|||
|
4b4c56d406
|
|||
|
4314195dd7
|
|||
| df4447b038 | |||
|
3fa660e579
|
|||
|
a430b1e4fd
|
|||
| 896c434f38 | |||
|
847b7238c5
|
|||
|
89d5fc91b0
|
|||
| 5af3c5f56e | |||
|
beb3864b2d
|
|||
|
581e6ef538
|
|||
|
fd642ddb84
|
|||
|
1ad8c127d9
|
|||
| 40aab6a6c1 | |||
|
4d33a24a07
|
|||
|
ee59eb350b
|
|||
|
5da13ff15a
|
|||
|
491c594ad3
|
|||
|
c794d533be
|
|||
|
a6daf7030e
|
|||
|
fe3b7ffa9c
|
|||
|
4c066a92d8
|
|||
|
7899b57781
|
|||
|
6e0a901887
|
|||
|
713fdebc90
|
|||
|
6944d138c6
|
|||
|
fbb1f16470
|
|||
|
2473cafdf5
|
|||
| 0ccfbd253e | |||
|
6c4bee0ac7
|
|||
| 4fa9f536eb | |||
|
033c9bfc13
|
|||
| 0db1ee87fc | |||
|
d180bb924f
|
|||
|
d50d68d95a
|
|||
|
f468bc7443
|
|||
|
dee2d9d104
|
|||
|
5c892b1d6a
|
|||
|
81b96fc7b1
|
|||
| c92a0d0703 |
@ -4,5 +4,4 @@
|
||||
Dockerfile
|
||||
abra
|
||||
dist
|
||||
kadabra
|
||||
tags
|
||||
|
||||
@ -11,10 +11,15 @@ steps:
|
||||
image: git.coopcloud.tech/toolshed/drone-xgettext-go:latest
|
||||
settings:
|
||||
keyword: i18n.G
|
||||
keyword_ctx: i18n.GC
|
||||
out: pkg/i18n/locales/abra.pot
|
||||
comments_tag: translators
|
||||
depends_on:
|
||||
- make check
|
||||
when:
|
||||
event:
|
||||
exclude:
|
||||
- tag
|
||||
|
||||
- name: xgettext-go status
|
||||
image: golang:1.24-alpine3.22
|
||||
@ -27,6 +32,10 @@ steps:
|
||||
- git diff-files --exit-code
|
||||
depends_on:
|
||||
- xgettext-go
|
||||
when:
|
||||
event:
|
||||
exclude:
|
||||
- tag
|
||||
|
||||
- name: make test
|
||||
image: golang:1.24
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -4,6 +4,6 @@
|
||||
.envrc
|
||||
.vscode/
|
||||
/abra
|
||||
/kadabra
|
||||
/bin
|
||||
dist/
|
||||
tests/integration/.bats
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
---
|
||||
version: 2
|
||||
|
||||
gitea_urls:
|
||||
api: https://git.coopcloud.tech/api/v1
|
||||
download: https://git.coopcloud.tech/
|
||||
@ -26,31 +28,9 @@ builds:
|
||||
- 5
|
||||
- 6
|
||||
- 7
|
||||
ldflags:
|
||||
- "-X 'main.Commit={{ .Commit }}'"
|
||||
- "-X 'main.Version={{ .Version }}'"
|
||||
- "-s"
|
||||
- "-w"
|
||||
|
||||
- id: kadabra
|
||||
binary: kadabra
|
||||
dir: cmd/kadabra
|
||||
env:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- linux
|
||||
- darwin
|
||||
goarch:
|
||||
- 386
|
||||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
goarm:
|
||||
- 5
|
||||
- 6
|
||||
- 7
|
||||
gcflags:
|
||||
- "all=-l -B"
|
||||
flags:
|
||||
- -v
|
||||
- -trimpath
|
||||
ldflags:
|
||||
- "-X 'main.Commit={{ .Commit }}'"
|
||||
- "-X 'main.Version={{ .Version }}'"
|
||||
|
||||
@ -10,14 +10,17 @@
|
||||
- cassowary
|
||||
- chasqui
|
||||
- codegod100
|
||||
- cyrnel
|
||||
- decentral1se
|
||||
- fauno
|
||||
- frando
|
||||
- iexos
|
||||
- jade
|
||||
- kawaiipunk
|
||||
- knoflook
|
||||
- mayel
|
||||
- moritz
|
||||
- namnatulco
|
||||
- p4u1
|
||||
- rix
|
||||
- roxxers
|
||||
|
||||
42
Makefile
42
Makefile
@ -1,10 +1,11 @@
|
||||
ABRA := ./cmd/abra
|
||||
KADABRA := ./cmd/kadabra
|
||||
XGETTEXT := ./bin/xgettext-go
|
||||
COMMIT := $(shell git rev-list -1 HEAD)
|
||||
GOPATH := $(shell go env GOPATH)
|
||||
GOVERSION := 1.24
|
||||
LDFLAGS := "-X 'main.Commit=$(COMMIT)'"
|
||||
DIST_LDFLAGS := $(LDFLAGS)" -s -w"
|
||||
BFLAGS := -v -trimpath
|
||||
GCFLAGS := "all=-l -B"
|
||||
DOMAIN := abra
|
||||
POFILES := $(wildcard pkg/i18n/locales/*.po)
|
||||
@ -13,40 +14,23 @@ LINGUAS := $(basename $(POFILES))
|
||||
|
||||
export GOPRIVATE=coopcloud.tech
|
||||
|
||||
# NOTE(d1): default `make` optimised for Abra hacking
|
||||
all: format check build-abra test
|
||||
all: format check build
|
||||
|
||||
run-abra:
|
||||
run:
|
||||
@go run -gcflags=$(GCFLAGS) -ldflags=$(LDFLAGS) $(ABRA)
|
||||
|
||||
run-kadabra:
|
||||
@go run -gcflags=$(GCFLAGS) -ldflags=$(LDFLAGS) $(KADABRA)
|
||||
|
||||
install-abra:
|
||||
install:
|
||||
@go install -gcflags=$(GCFLAGS) -ldflags=$(LDFLAGS) $(ABRA)
|
||||
|
||||
install-kadabra:
|
||||
@go install -gcflags=$(GCFLAGS) -ldflags=$(LDFLAGS) $(KADABRA)
|
||||
build:
|
||||
@go build $(BFLAGS) -gcflags=$(GCFLAGS) -ldflags=$(DIST_LDFLAGS) $(ABRA)
|
||||
|
||||
install: install-abra install-kadabra
|
||||
|
||||
build-abra:
|
||||
@go build -v -gcflags=$(GCFLAGS) -ldflags=$(DIST_LDFLAGS) $(ABRA)
|
||||
|
||||
build-kadabra:
|
||||
@go build -v -gcflags=$(GCFLAGS) -ldflags=$(DIST_LDFLAGS) $(KADABRA)
|
||||
|
||||
build: build-abra build-kadabra
|
||||
|
||||
build-docker-abra:
|
||||
build-docker:
|
||||
@docker run -it -v $(PWD):/abra golang:$(GOVERSION) \
|
||||
bash -c 'cd /abra; ./scripts/docker/build.sh'
|
||||
|
||||
build-docker: build-docker-abra
|
||||
|
||||
clean:
|
||||
@rm '$(GOPATH)/bin/abra'
|
||||
@rm '$(GOPATH)/bin/kadabra'
|
||||
|
||||
format:
|
||||
@gofmt -s -w $$(find . -type f -name '*.go' | grep -v "/vendor/")
|
||||
@ -78,14 +62,20 @@ update-po:
|
||||
done
|
||||
|
||||
.PHONY: update-pot
|
||||
update-pot:
|
||||
@xgettext-go \
|
||||
update-pot: $(XGETTEXT)
|
||||
@${XGETTEXT} \
|
||||
-o pkg/i18n/locales/$(DOMAIN).pot \
|
||||
--keyword=i18n.G \
|
||||
--keyword-ctx=i18n.GC \
|
||||
--sort-output \
|
||||
--add-comments-tag="translators" \
|
||||
$$(find . -name "*.go" -not -path "*vendor*" | sort)
|
||||
|
||||
${XGETTEXT}:
|
||||
@mkdir -p ./bin && \
|
||||
wget -O ./bin/xgettext-go https://git.coopcloud.tech/toolshed/xgettext-go/raw/branch/main/xgettext-go && \
|
||||
chmod +x ./bin/xgettext-go
|
||||
|
||||
.PHONY: update-pot-po-metadata
|
||||
update-pot-po-metadata:
|
||||
@sed -i "s/charset=CHARSET/charset=UTF-8/g" pkg/i18n/locales/*.po pkg/i18n/locales/*.pot
|
||||
|
||||
@ -9,7 +9,7 @@ import (
|
||||
|
||||
// translators: `abra app` aliases. use a comma separated list of aliases with
|
||||
// no spaces in between
|
||||
var appAliases = i18n.G("a")
|
||||
var appAliases = i18n.GC("a", "abra app")
|
||||
|
||||
var AppCommand = &cobra.Command{
|
||||
// translators: `app` command group
|
||||
|
||||
@ -268,7 +268,7 @@ func init() {
|
||||
AppBackupListCommand.Flags().BoolVarP(
|
||||
&showAllPaths,
|
||||
i18n.G("all"),
|
||||
i18n.G("a"),
|
||||
i18n.GC("a", "app backup list"),
|
||||
false,
|
||||
i18n.G("show all paths"),
|
||||
)
|
||||
|
||||
@ -3,12 +3,14 @@ package app
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
"coopcloud.tech/abra/pkg/secret"
|
||||
"coopcloud.tech/tagcmp"
|
||||
|
||||
appPkg "coopcloud.tech/abra/pkg/app"
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
@ -103,18 +105,26 @@ checkout as-is. Recipe commit hashes are also supported as values for
|
||||
|
||||
toDeployVersion, err = getDeployVersion(args, deployMeta, app)
|
||||
if err != nil {
|
||||
log.Fatal(i18n.G("get deploy version: %s", err))
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
isChaosCommit, err := app.Recipe.IsChaosCommit(toDeployVersion)
|
||||
if err != nil {
|
||||
log.Fatal(i18n.G("unable to determine if %s is a chaos commit: %s", toDeployVersion, err))
|
||||
}
|
||||
|
||||
if !isChaosCommit && !tagcmp.IsParsable(toDeployVersion) {
|
||||
log.Fatal(i18n.G("unable to parse deploy version: %s", toDeployVersion))
|
||||
}
|
||||
|
||||
versionIsChaos := false
|
||||
if !internal.Chaos {
|
||||
var err error
|
||||
versionIsChaos, err = app.Recipe.EnsureVersion(toDeployVersion)
|
||||
isChaosCommit, err := app.Recipe.EnsureVersion(toDeployVersion)
|
||||
if err != nil {
|
||||
log.Fatal(i18n.G("ensure recipe: %s", err))
|
||||
}
|
||||
if versionIsChaos {
|
||||
if isChaosCommit {
|
||||
log.Warnf(i18n.G("version '%s' appears to be a chaos commit, but --chaos/-C was not provided", toDeployVersion))
|
||||
internal.Chaos = true
|
||||
}
|
||||
}
|
||||
|
||||
@ -152,14 +162,26 @@ checkout as-is. Recipe commit hashes are also supported as values for
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
appPkg.ExposeAllEnv(stackName, compose, app.Env)
|
||||
appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name)
|
||||
appPkg.SetChaosLabel(compose, stackName, internal.Chaos || versionIsChaos)
|
||||
appPkg.SetChaosLabel(compose, stackName, internal.Chaos)
|
||||
if internal.Chaos {
|
||||
appPkg.SetChaosVersionLabel(compose, stackName, toDeployVersion)
|
||||
}
|
||||
appPkg.SetUpdateLabel(compose, stackName, app.Env)
|
||||
appPkg.SetVersionLabel(compose, stackName, toDeployVersion)
|
||||
|
||||
versionLabel := toDeployVersion
|
||||
if internal.Chaos {
|
||||
for _, service := range compose.Services {
|
||||
if service.Name == "app" {
|
||||
labelKey := fmt.Sprintf("coop-cloud.%s.version", stackName)
|
||||
// NOTE(d1): keep non-chaos version labbeling when doing chaos ops
|
||||
versionLabel = service.Deploy.Labels[labelKey]
|
||||
}
|
||||
}
|
||||
}
|
||||
appPkg.SetVersionLabel(compose, stackName, versionLabel)
|
||||
|
||||
newRecipeWithDeployVersion := fmt.Sprintf("%s:%s", app.Recipe.Name, toDeployVersion)
|
||||
appPkg.ExposeAllEnv(stackName, compose, app.Env, newRecipeWithDeployVersion)
|
||||
|
||||
envVars, err := appPkg.CheckEnv(app)
|
||||
if err != nil {
|
||||
@ -186,9 +208,12 @@ checkout as-is. Recipe commit hashes are also supported as values for
|
||||
log.Debug(i18n.G("skipping domain checks"))
|
||||
}
|
||||
|
||||
deployedVersion := config.NO_VERSION_DEFAULT
|
||||
deployedVersion := config.MISSING_DEFAULT
|
||||
if deployMeta.IsDeployed {
|
||||
deployedVersion = deployMeta.Version
|
||||
if deployMeta.IsChaos {
|
||||
deployedVersion = deployMeta.ChaosVersion
|
||||
}
|
||||
}
|
||||
|
||||
// Gather secrets
|
||||
@ -245,6 +270,7 @@ checkout as-is. Recipe commit hashes are also supported as values for
|
||||
app.Name,
|
||||
app.Server,
|
||||
internal.DontWaitConverge,
|
||||
internal.NoInput,
|
||||
f,
|
||||
); err != nil {
|
||||
log.Fatal(err)
|
||||
@ -265,13 +291,21 @@ checkout as-is. Recipe commit hashes are also supported as values for
|
||||
}
|
||||
|
||||
func getLatestVersionOrCommit(app appPkg.App) (string, error) {
|
||||
versions, err := app.Recipe.Tags()
|
||||
recipeVersions, warnings, err := app.Recipe.GetRecipeVersions()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(versions) > 0 && !internal.Chaos {
|
||||
return versions[len(versions)-1], nil
|
||||
for _, warning := range warnings {
|
||||
log.Warn(warning)
|
||||
}
|
||||
|
||||
if len(recipeVersions) > 0 && !internal.Chaos {
|
||||
latest := recipeVersions[len(recipeVersions)-1]
|
||||
for tag := range latest {
|
||||
log.Debug(i18n.G("selected latest recipe version: %s (from %d available versions)", tag, len(recipeVersions)))
|
||||
return tag, nil
|
||||
}
|
||||
}
|
||||
|
||||
head, err := app.Recipe.Head()
|
||||
@ -300,6 +334,16 @@ func validateArgsAndFlags(args []string) error {
|
||||
}
|
||||
|
||||
func validateSecrets(cl *dockerClient.Client, app appPkg.App) error {
|
||||
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
secretsConfig, err := secret.ReadSecretsConfig(app.Path, composeFiles, app.StackName())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
secStats, err := secret.PollSecretsStatus(cl, app)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -307,6 +351,10 @@ func validateSecrets(cl *dockerClient.Client, app appPkg.App) error {
|
||||
|
||||
for _, secStat := range secStats {
|
||||
if !secStat.CreatedOnRemote {
|
||||
secretConfig := secretsConfig[secStat.LocalName]
|
||||
if secretConfig.SkipGenerate {
|
||||
return errors.New(i18n.G("secret not inserted (#generate=false): %s", secStat.LocalName))
|
||||
}
|
||||
return errors.New(i18n.G("secret not generated: %s", secStat.LocalName))
|
||||
}
|
||||
}
|
||||
@ -334,7 +382,12 @@ func getDeployVersion(cliArgs []string, deployMeta stack.DeployMeta, app appPkg.
|
||||
// Check if the recipe has a version in the .env file
|
||||
if app.Recipe.EnvVersion != "" && !internal.DeployLatest {
|
||||
if strings.HasSuffix(app.Recipe.EnvVersionRaw, "+U") {
|
||||
return "", errors.New(i18n.G("version: can not redeploy chaos version %s", app.Recipe.EnvVersionRaw))
|
||||
// NOTE(d1): use double-line 5 spaces ("FATA ") trick to make a more
|
||||
// informative error message. it's ugly but that's our logging situation
|
||||
// atm
|
||||
return "", errors.New(i18n.G(`cannot redeploy previous chaos version (%s), did you mean to use "--chaos"?
|
||||
to return to a regular release, specify a release tag, commit SHA or use "--latest"`,
|
||||
formatter.BoldDirtyDefault(app.Recipe.EnvVersionRaw)))
|
||||
}
|
||||
log.Debug(i18n.G("version: taking version from .env file: %s", app.Recipe.EnvVersion))
|
||||
return app.Recipe.EnvVersion, nil
|
||||
|
||||
311
cli/app/env.go
311
cli/app/env.go
@ -1,28 +1,50 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/app"
|
||||
appPkg "coopcloud.tech/abra/pkg/app"
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
containerPkg "coopcloud.tech/abra/pkg/container"
|
||||
contextPkg "coopcloud.tech/abra/pkg/context"
|
||||
"coopcloud.tech/abra/pkg/formatter"
|
||||
"coopcloud.tech/abra/pkg/i18n"
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
"coopcloud.tech/abra/pkg/upstream/stack"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// translators: `abra app env` aliases. use a comma separated list of aliases with
|
||||
// no spaces in between
|
||||
// translators: `abra app env` aliases. use a comma separated list of aliases
|
||||
// with no spaces in between
|
||||
var appEnvAliases = i18n.G("e")
|
||||
|
||||
var AppEnvCommand = &cobra.Command{
|
||||
// translators: `app env` command
|
||||
Use: i18n.G("env <domain> [flags]"),
|
||||
Aliases: strings.Split(appEnvAliases, ","),
|
||||
// translators: Short description for `app env` command
|
||||
Short: i18n.G("Show app .env values"),
|
||||
Example: i18n.G(" abra app env 1312.net"),
|
||||
// translators: `abra app env list` aliases. use a comma separated list of
|
||||
// aliases with no spaces in between
|
||||
var appEnvListAliases = i18n.G("l,ls")
|
||||
|
||||
// translators: `abra app env pull` aliases. use a comma separated list of
|
||||
// aliases with no spaces in between
|
||||
var appEnvPullAliases = i18n.G("pl,p")
|
||||
|
||||
var AppEnvListCommand = &cobra.Command{
|
||||
// translators: `app env list` command
|
||||
Use: i18n.G("list <domain> [flags]"),
|
||||
Aliases: strings.Split(appEnvListAliases, ","),
|
||||
// translators: Short description for `app env list` command
|
||||
Short: i18n.G("List all app environment values"),
|
||||
Example: i18n.G(" abra app env list 1312.net"),
|
||||
Args: cobra.ExactArgs(1),
|
||||
ValidArgsFunction: func(
|
||||
cmd *cobra.Command,
|
||||
@ -49,3 +71,274 @@ var AppEnvCommand = &cobra.Command{
|
||||
fmt.Println(overview)
|
||||
},
|
||||
}
|
||||
|
||||
var AppEnvPullCommand = &cobra.Command{
|
||||
// translators: `app pull` command
|
||||
Use: i18n.G("pull <domain> [flags]"),
|
||||
Aliases: strings.Split(appEnvPullAliases, ","),
|
||||
// translators: Short description for `app env pull` command
|
||||
Short: i18n.G("Pull app environment values from a deployed app"),
|
||||
Long: i18n.G(`Pull app environment values from a deploymed app.
|
||||
|
||||
A convenient command for when you've lost your app environment file or want to
|
||||
synchronize your local app environment values with what is deployed live.`),
|
||||
Example: i18n.G(` # pull existing .env file and overwrite local values
|
||||
abra app env pull 1312.net --force
|
||||
|
||||
# pull lost app .env file
|
||||
abra app env pull my.gitea.net --server 1312.net`),
|
||||
Args: cobra.MaximumNArgs(2),
|
||||
ValidArgsFunction: func(
|
||||
cmd *cobra.Command,
|
||||
args []string,
|
||||
toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return autocomplete.AppNameComplete()
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
appName := args[0]
|
||||
|
||||
appEnvPath := path.Join(config.ABRA_DIR, "servers", server, fmt.Sprintf("%s.env", appName))
|
||||
if _, err := os.Stat(appEnvPath); !os.IsNotExist(err) {
|
||||
log.Fatal(i18n.G("%s already exists?", appEnvPath))
|
||||
}
|
||||
|
||||
if server == "" {
|
||||
log.Fatal(i18n.G("unable to determine server of app %s, please pass --server/-s", appName))
|
||||
}
|
||||
|
||||
serverDir := filepath.Join(config.SERVERS_DIR, server)
|
||||
if _, err := os.Stat(serverDir); os.IsNotExist(err) {
|
||||
log.Fatal(i18n.G("unknown server %s, run \"abra server add %s\"?", server, server))
|
||||
}
|
||||
|
||||
store := contextPkg.NewDefaultDockerContextStore()
|
||||
contexts, err := store.Store.List()
|
||||
if err != nil {
|
||||
log.Fatal(i18n.G("unable to look up server context for %s: %s", server, err))
|
||||
}
|
||||
|
||||
var contextCreated bool
|
||||
if server == "default" {
|
||||
contextCreated = true
|
||||
}
|
||||
|
||||
for _, context := range contexts {
|
||||
if context.Name == server {
|
||||
contextCreated = true
|
||||
}
|
||||
}
|
||||
|
||||
if !contextCreated {
|
||||
log.Fatal(i18n.G("%s missing context, run \"abra server add %s\"?", server, server))
|
||||
}
|
||||
|
||||
cl, err := client.New(server)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
deployMeta, err := stack.IsDeployed(context.Background(), cl, appPkg.StackName(appName))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if !deployMeta.IsDeployed {
|
||||
log.Fatal(i18n.G("%s is not deployed?", appName))
|
||||
}
|
||||
|
||||
filters := filters.NewArgs()
|
||||
filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(appName), "app"))
|
||||
targetContainer, err := containerPkg.GetContainer(context.Background(), cl, filters, internal.NoInput)
|
||||
if err != nil {
|
||||
log.Fatal(i18n.G("unable to retrieve container for %s: %s", appName, err))
|
||||
}
|
||||
|
||||
inspectResult, err := cl.ContainerInspect(context.Background(), targetContainer.ID)
|
||||
if err != nil {
|
||||
log.Fatal(i18n.G("unable to inspect container for %s: %s", appName, err))
|
||||
}
|
||||
|
||||
deploymentEnv := make(map[string]string)
|
||||
for _, envVar := range inspectResult.Config.Env {
|
||||
split := strings.SplitN(envVar, "=", 2)
|
||||
if len(split) != 2 {
|
||||
log.Debug(i18n.G("no value attached to %s", envVar))
|
||||
continue
|
||||
}
|
||||
|
||||
key, val := split[0], split[1]
|
||||
deploymentEnv[key] = val
|
||||
}
|
||||
|
||||
log.Debug(i18n.G("pulled env values from %s deployment: %s", appName, deploymentEnv))
|
||||
|
||||
var (
|
||||
recipeEnvVar string
|
||||
recipeKey string
|
||||
)
|
||||
|
||||
if r, ok := deploymentEnv["TYPE"]; ok {
|
||||
recipeKey = "TYPE"
|
||||
recipeEnvVar = r
|
||||
}
|
||||
|
||||
if r, ok := deploymentEnv["RECIPE"]; ok {
|
||||
recipeKey = "RECIPE"
|
||||
recipeEnvVar = r
|
||||
}
|
||||
|
||||
if recipeEnvVar == "" {
|
||||
log.Fatal(i18n.G("unable to determine recipe type from %s, env: %v", appName, inspectResult.Config.Env))
|
||||
}
|
||||
|
||||
var recipeName = recipeEnvVar
|
||||
if strings.Contains(recipeEnvVar, ":") {
|
||||
split := strings.Split(recipeEnvVar, ":")
|
||||
recipeName = split[0]
|
||||
}
|
||||
|
||||
recipe := internal.ValidateRecipe(
|
||||
[]string{recipeName},
|
||||
cmd.Name(),
|
||||
)
|
||||
|
||||
version := deployMeta.Version
|
||||
if deployMeta.IsChaos {
|
||||
version = deployMeta.ChaosVersion
|
||||
}
|
||||
|
||||
if _, err := recipe.EnsureVersion(version); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
mergedEnv, err := recipe.SampleEnv()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Debug(i18n.G("retrieved env values from .env.sample of %s: %s", recipe.Name, mergedEnv))
|
||||
|
||||
for k, v := range deploymentEnv {
|
||||
mergedEnv[k] = v
|
||||
}
|
||||
|
||||
if !strings.Contains(recipeEnvVar, ":") {
|
||||
mergedEnv[recipeKey] = fmt.Sprintf("%s:%s", mergedEnv[recipeKey], version)
|
||||
}
|
||||
|
||||
log.Debug(i18n.G("final merged env values for %s are: %s", appName, mergedEnv))
|
||||
|
||||
envSample, err := os.ReadFile(recipe.SampleEnvPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(appEnvPath, envSample, 0o664)
|
||||
if err != nil {
|
||||
log.Fatal(i18n.G("unable to write new env %s: %s", appEnvPath, err))
|
||||
}
|
||||
|
||||
read, err := os.ReadFile(appEnvPath)
|
||||
if err != nil {
|
||||
log.Fatal(i18n.G("unable to read new env %s: %s", appEnvPath, err))
|
||||
}
|
||||
|
||||
sampleEnv, err := recipe.SampleEnv()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var composeFileUpdated bool
|
||||
newContents := string(read)
|
||||
for key, val := range mergedEnv {
|
||||
if sampleEnv[key] == val {
|
||||
continue
|
||||
}
|
||||
|
||||
if key == "COMPOSE_FILE" {
|
||||
composeFileUpdated = true
|
||||
continue
|
||||
}
|
||||
|
||||
if m, _ := regexp.MatchString(fmt.Sprintf(`#%s=`, key), newContents); m {
|
||||
log.Debug(i18n.G("uncommenting %s", key))
|
||||
re := regexp.MustCompile(fmt.Sprintf(`#%s=`, key))
|
||||
newContents = re.ReplaceAllString(newContents, fmt.Sprintf("%s=", key))
|
||||
}
|
||||
|
||||
if m, _ := regexp.MatchString(fmt.Sprintf(`# %s=`, key), newContents); m {
|
||||
log.Debug(i18n.G("uncommenting %s", key))
|
||||
re := regexp.MustCompile(fmt.Sprintf(`# %s=`, key))
|
||||
newContents = re.ReplaceAllString(newContents, fmt.Sprintf("%s=", key))
|
||||
}
|
||||
|
||||
if m, _ := regexp.MatchString(fmt.Sprintf(`%s=".*"`, key), newContents); m {
|
||||
log.Debug(i18n.G(`inserting %s="%s" (double quotes)`, key, val))
|
||||
re := regexp.MustCompile(fmt.Sprintf(`%s=".*"`, key))
|
||||
newContents = re.ReplaceAllString(newContents, fmt.Sprintf(`%s="%s"`, key, val))
|
||||
continue
|
||||
}
|
||||
|
||||
if m, _ := regexp.MatchString(fmt.Sprintf(`%s='.*'`, key), newContents); m {
|
||||
log.Debug(i18n.G(`inserting %s='%s' (single quotes)`, key, val))
|
||||
re := regexp.MustCompile(fmt.Sprintf(`%s='.*'`, key))
|
||||
newContents = re.ReplaceAllString(newContents, fmt.Sprintf(`%s='%s'`, key, val))
|
||||
continue
|
||||
}
|
||||
|
||||
if m, _ := regexp.MatchString(fmt.Sprintf("%s=.*", key), newContents); m {
|
||||
log.Debug(i18n.G("inserting %s=%s (no quotes)", key, val))
|
||||
re := regexp.MustCompile(fmt.Sprintf("%s=.*", key))
|
||||
newContents = re.ReplaceAllString(newContents, fmt.Sprintf("%s=%s", key, val))
|
||||
}
|
||||
}
|
||||
|
||||
err = os.WriteFile(appEnvPath, []byte(newContents), 0)
|
||||
if err != nil {
|
||||
log.Fatal(i18n.G("unable to write new env %s: %s", appEnvPath, err))
|
||||
}
|
||||
|
||||
log.Info(i18n.G("%s successfully created", appEnvPath))
|
||||
|
||||
if composeFileUpdated {
|
||||
log.Warn(i18n.G("manual update required: COMPOSE_FILE=\"%s\"", mergedEnv["COMPOSE_FILE"]))
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
var AppEnvCommand = &cobra.Command{
|
||||
// translators: `app env` command group
|
||||
Use: i18n.G("env [cmd] [args] [flags]"),
|
||||
Aliases: strings.Split(appEnvAliases, ","),
|
||||
// translators: Short description for `app env` command group
|
||||
Short: i18n.G("Manage app environment values"),
|
||||
}
|
||||
|
||||
var (
|
||||
server string
|
||||
)
|
||||
|
||||
func init() {
|
||||
AppEnvPullCommand.Flags().BoolVarP(
|
||||
&internal.Force,
|
||||
i18n.G("force"),
|
||||
i18n.G("f"),
|
||||
false,
|
||||
i18n.G("perform action without further prompt"),
|
||||
)
|
||||
|
||||
AppEnvPullCommand.Flags().StringVarP(
|
||||
&server,
|
||||
i18n.G("server"),
|
||||
i18n.G("s"),
|
||||
"",
|
||||
i18n.G("server associated with deployed app"),
|
||||
)
|
||||
|
||||
AppEnvPullCommand.RegisterFlagCompletionFunc(
|
||||
i18n.G("server"),
|
||||
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
return autocomplete.ServerNameComplete()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@ -25,7 +25,6 @@ type appStatus struct {
|
||||
Status string `json:"status"`
|
||||
Chaos string `json:"chaos"`
|
||||
ChaosVersion string `json:"chaosVersion"`
|
||||
AutoUpdate string `json:"autoUpdate"`
|
||||
Version string `json:"version"`
|
||||
Upgrade string `json:"upgrade"`
|
||||
}
|
||||
@ -114,11 +113,14 @@ Use "--status/-S" flag to query all servers for the live deployment status.`),
|
||||
totalAppsCount++
|
||||
|
||||
if status {
|
||||
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
status := i18n.G("unknown")
|
||||
version := i18n.G("unknown")
|
||||
chaos := i18n.G("unknown")
|
||||
chaosVersion := i18n.G("unknown")
|
||||
autoUpdate := i18n.G("unknown")
|
||||
if statusMeta, ok := statuses[app.StackName()]; ok {
|
||||
if currentVersion, exists := statusMeta["version"]; exists {
|
||||
if currentVersion != "" {
|
||||
@ -131,9 +133,6 @@ Use "--status/-S" flag to query all servers for the live deployment status.`),
|
||||
if chaosDeployVersion, exists := statusMeta["chaosVersion"]; exists {
|
||||
chaosVersion = chaosDeployVersion
|
||||
}
|
||||
if autoUpdateState, exists := statusMeta["autoUpdate"]; exists {
|
||||
autoUpdate = autoUpdateState
|
||||
}
|
||||
if statusMeta["status"] != "" {
|
||||
status = statusMeta["status"]
|
||||
}
|
||||
@ -146,7 +145,6 @@ Use "--status/-S" flag to query all servers for the live deployment status.`),
|
||||
appStats.Chaos = chaos
|
||||
appStats.ChaosVersion = chaosVersion
|
||||
appStats.Version = version
|
||||
appStats.AutoUpdate = autoUpdate
|
||||
|
||||
var newUpdates []string
|
||||
if version != "unknown" && chaos == "false" {
|
||||
@ -165,6 +163,11 @@ Use "--status/-S" flag to query all servers for the live deployment status.`),
|
||||
}
|
||||
|
||||
for _, update := range updates {
|
||||
if ok := tagcmp.IsParsable(update); !ok {
|
||||
log.Debug(i18n.G("unable to parse %s, skipping as upgrade option", update))
|
||||
continue
|
||||
}
|
||||
|
||||
parsedUpdate, err := tagcmp.Parse(update)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
@ -226,7 +229,6 @@ Use "--status/-S" flag to query all servers for the live deployment status.`),
|
||||
i18n.G("CHAOS"),
|
||||
i18n.G("VERSION"),
|
||||
i18n.G("UPGRADE"),
|
||||
i18n.G("AUTOUPDATE"),
|
||||
}...,
|
||||
)
|
||||
}
|
||||
@ -257,8 +259,7 @@ Use "--status/-S" flag to query all servers for the live deployment status.`),
|
||||
appStat.Status,
|
||||
chaosStatus,
|
||||
appStat.Version,
|
||||
appStat.Upgrade,
|
||||
appStat.AutoUpdate}...,
|
||||
appStat.Upgrade}...,
|
||||
)
|
||||
}
|
||||
|
||||
@ -328,6 +329,14 @@ func init() {
|
||||
i18n.G("show apps of a specific server"),
|
||||
)
|
||||
|
||||
AppListCommand.Flags().BoolVarP(
|
||||
&internal.Chaos,
|
||||
i18n.G("chaos"),
|
||||
i18n.G("C"),
|
||||
false,
|
||||
i18n.G("ignore uncommitted recipes changes"),
|
||||
)
|
||||
|
||||
AppListCommand.RegisterFlagCompletionFunc(
|
||||
i18n.G("server"),
|
||||
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
|
||||
@ -128,6 +128,10 @@ Use "--dry-run/-r" to see which secrets and volumes will be moved.`),
|
||||
secretName := strings.Join(sname[:len(sname)-1], "_")
|
||||
data := resources.Secrets[secretName]
|
||||
if err := client.StoreSecret(newServerClient, s.Spec.Name, data); err != nil {
|
||||
if strings.Contains(err.Error(), "already exists") {
|
||||
log.Info(i18n.G("skipping secret (because it already exists) on %s: %s", s.Spec.Name, newServer))
|
||||
continue
|
||||
}
|
||||
log.Fatal(i18n.G("failed to store secret on %s: %s", err, newServer))
|
||||
}
|
||||
log.Info(i18n.G("created secret on %s: %s", s.Spec.Name, newServer))
|
||||
|
||||
@ -72,6 +72,10 @@ var AppNewCommand = &cobra.Command{
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
recipe := internal.ValidateRecipe(args, cmd.Name())
|
||||
|
||||
if err := recipe.Ensure(internal.GetEnsureContext()); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if len(args) == 2 && internal.Chaos {
|
||||
log.Fatal(i18n.G("cannot use [version] and --chaos together"))
|
||||
}
|
||||
@ -98,10 +102,14 @@ var AppNewCommand = &cobra.Command{
|
||||
var recipeVersions recipePkg.RecipeVersions
|
||||
if recipeVersion == "" {
|
||||
var err error
|
||||
recipeVersions, _, err = recipe.GetRecipeVersions()
|
||||
var warnings []string
|
||||
recipeVersions, warnings, err = recipe.GetRecipeVersions()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for _, warning := range warnings {
|
||||
log.Warn(warning)
|
||||
}
|
||||
}
|
||||
|
||||
if len(recipeVersions) > 0 {
|
||||
@ -110,6 +118,8 @@ var AppNewCommand = &cobra.Command{
|
||||
recipeVersion = tag
|
||||
}
|
||||
|
||||
log.Debug(i18n.G("selected recipe version: %s (from %d available versions)", recipeVersion, len(recipeVersions)))
|
||||
|
||||
if _, err := recipe.EnsureVersion(recipeVersion); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@ -192,7 +202,27 @@ var AppNewCommand = &cobra.Command{
|
||||
log.Info(i18n.G("%s created (version: %s)", appDomain, recipeVersion))
|
||||
|
||||
if len(secretsConfig) > 0 {
|
||||
log.Warn(i18n.G("%s requires secret generation before deploying, run \"abra app secret generate %s --all\"", recipe.Name, appDomain))
|
||||
var (
|
||||
hasSecretToGenerate bool
|
||||
hasSecretToSkip bool
|
||||
)
|
||||
|
||||
for _, secretConfig := range secretsConfig {
|
||||
if secretConfig.SkipGenerate {
|
||||
hasSecretToSkip = true
|
||||
continue
|
||||
}
|
||||
|
||||
hasSecretToGenerate = true
|
||||
}
|
||||
|
||||
if hasSecretToGenerate && !generateSecrets {
|
||||
log.Warn(i18n.G("%s requires secret generation before deploy, run \"abra app secret generate %s --all\"", recipe.Name, appDomain))
|
||||
}
|
||||
|
||||
if hasSecretToSkip {
|
||||
log.Warn(i18n.G("%s requires secret insertion before deploy (#generate=false)", recipe.Name))
|
||||
}
|
||||
}
|
||||
|
||||
if len(appSecrets) > 0 {
|
||||
|
||||
@ -128,6 +128,7 @@ Pass "--all-services/-a" to restart all services.`),
|
||||
AppName: app.Name,
|
||||
ServerName: app.Server,
|
||||
Filters: f,
|
||||
NoInput: internal.NoInput,
|
||||
NoLog: true,
|
||||
Quiet: true,
|
||||
}
|
||||
@ -166,7 +167,7 @@ func init() {
|
||||
AppRestartCommand.Flags().BoolVarP(
|
||||
&allServices,
|
||||
i18n.G("all-services"),
|
||||
i18n.G("a"),
|
||||
i18n.GC("a", "app restart"),
|
||||
false,
|
||||
i18n.G("restart all services"),
|
||||
)
|
||||
|
||||
@ -2,6 +2,7 @@ package app
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
appPkg "coopcloud.tech/abra/pkg/app"
|
||||
@ -177,13 +178,14 @@ beforehand. See "abra app backup" for more.`),
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
appPkg.ExposeAllEnv(stackName, compose, app.Env)
|
||||
newRecipeWithDowngradeVersion := fmt.Sprintf("%s:%s", app.Recipe.Name, chosenDowngrade)
|
||||
appPkg.ExposeAllEnv(stackName, compose, app.Env, newRecipeWithDowngradeVersion)
|
||||
|
||||
appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name)
|
||||
appPkg.SetChaosLabel(compose, stackName, internal.Chaos)
|
||||
if internal.Chaos {
|
||||
appPkg.SetChaosVersionLabel(compose, stackName, chosenDowngrade)
|
||||
}
|
||||
appPkg.SetUpdateLabel(compose, stackName, app.Env)
|
||||
|
||||
// Gather secrets
|
||||
secretInfo, err := deploy.GatherSecretsForDeploy(cl, app, internal.ShowUnchanged)
|
||||
@ -203,10 +205,15 @@ beforehand. See "abra app backup" for more.`),
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
deployedVersion := deployMeta.Version
|
||||
if deployMeta.IsChaos {
|
||||
deployedVersion = deployMeta.ChaosVersion
|
||||
}
|
||||
|
||||
// NOTE(d1): no release notes implemeneted for rolling back
|
||||
if err := internal.DeployOverview(
|
||||
app,
|
||||
deployMeta.Version,
|
||||
deployedVersion,
|
||||
chosenDowngrade,
|
||||
"",
|
||||
downgradeWarnMessages,
|
||||
@ -239,6 +246,7 @@ beforehand. See "abra app backup" for more.`),
|
||||
stackName,
|
||||
app.Server,
|
||||
internal.DontWaitConverge,
|
||||
internal.NoInput,
|
||||
f,
|
||||
); err != nil {
|
||||
log.Fatal(err)
|
||||
|
||||
@ -165,7 +165,7 @@ var AppSecretInsertCommand = &cobra.Command{
|
||||
Arbitrary secret insertion is not supported. Secrets that are inserted must
|
||||
match those configured in the recipe beforehand.
|
||||
|
||||
This can be useful when you want to manually generate secrets for an app
|
||||
This command can be useful when you want to manually generate secrets for an app
|
||||
environment. Typically, you can let Abra generate them for you on app creation
|
||||
(see "abra app new --secrets/-S" for more).`),
|
||||
Example: i18n.G(` # insert regular secret
|
||||
@ -574,7 +574,7 @@ func init() {
|
||||
AppSecretGenerateCommand.Flags().BoolVarP(
|
||||
&generateAllSecrets,
|
||||
i18n.G("all"),
|
||||
i18n.G("a"),
|
||||
i18n.GC("a", "app secret generate"),
|
||||
false,
|
||||
i18n.G("generate all secrets"),
|
||||
)
|
||||
@ -614,7 +614,7 @@ func init() {
|
||||
AppSecretRmCommand.Flags().BoolVarP(
|
||||
&rmAllSecrets,
|
||||
i18n.G("all"),
|
||||
i18n.G("a"),
|
||||
i18n.GC("a", "app secret rm"),
|
||||
false,
|
||||
i18n.G("remove all secrets"),
|
||||
)
|
||||
|
||||
@ -28,6 +28,7 @@ var AppUndeployCommand = &cobra.Command{
|
||||
Use: i18n.G("undeploy <domain> [flags]"),
|
||||
// translators: Short description for `app undeploy` command
|
||||
Aliases: strings.Split(appUndeployAliases, ","),
|
||||
Short: i18n.G("Undeploy a deployed app"),
|
||||
Long: i18n.G(`This does not destroy any application data.
|
||||
|
||||
However, you should remain vigilant, as your swarm installation will consider
|
||||
@ -65,10 +66,15 @@ Passing "--prune/-p" does not remove those volumes.`),
|
||||
log.Fatal(i18n.G("%s is not deployed?", app.Name))
|
||||
}
|
||||
|
||||
version := deployMeta.Version
|
||||
if deployMeta.IsChaos {
|
||||
version = deployMeta.ChaosVersion
|
||||
}
|
||||
|
||||
if err := internal.DeployOverview(
|
||||
app,
|
||||
deployMeta.Version,
|
||||
config.NO_DOMAIN_DEFAULT,
|
||||
version,
|
||||
config.MISSING_DEFAULT,
|
||||
"",
|
||||
nil,
|
||||
nil,
|
||||
@ -110,7 +116,7 @@ Passing "--prune/-p" does not remove those volumes.`),
|
||||
|
||||
log.Info(i18n.G("undeploy succeeded 🟢"))
|
||||
|
||||
if err := app.WriteRecipeVersion(deployMeta.Version, false); err != nil {
|
||||
if err := app.WriteRecipeVersion(version, false); err != nil {
|
||||
log.Fatal(i18n.G("writing recipe version failed: %s", err))
|
||||
}
|
||||
},
|
||||
|
||||
@ -190,13 +190,14 @@ beforehand. See "abra app backup" for more.`),
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
appPkg.ExposeAllEnv(stackName, compose, app.Env)
|
||||
newRecipeWithUpgradeVersion := fmt.Sprintf("%s:%s", app.Recipe.Name, chosenUpgrade)
|
||||
appPkg.ExposeAllEnv(stackName, compose, app.Env, newRecipeWithUpgradeVersion)
|
||||
|
||||
appPkg.SetRecipeLabel(compose, stackName, app.Recipe.Name)
|
||||
appPkg.SetChaosLabel(compose, stackName, internal.Chaos)
|
||||
if internal.Chaos {
|
||||
appPkg.SetChaosVersionLabel(compose, stackName, chosenUpgrade)
|
||||
}
|
||||
appPkg.SetUpdateLabel(compose, stackName, app.Env)
|
||||
|
||||
envVars, err := appPkg.CheckEnv(app)
|
||||
if err != nil {
|
||||
@ -241,9 +242,14 @@ beforehand. See "abra app backup" for more.`),
|
||||
)
|
||||
}
|
||||
|
||||
deployedVersion := deployMeta.Version
|
||||
if deployMeta.IsChaos {
|
||||
deployedVersion = deployMeta.ChaosVersion
|
||||
}
|
||||
|
||||
if err := internal.DeployOverview(
|
||||
app,
|
||||
deployMeta.Version,
|
||||
deployedVersion,
|
||||
chosenUpgrade,
|
||||
upgradeReleaseNotes,
|
||||
upgradeWarnMessages,
|
||||
@ -276,6 +282,7 @@ beforehand. See "abra app backup" for more.`),
|
||||
stackName,
|
||||
app.Server,
|
||||
internal.DontWaitConverge,
|
||||
internal.NoInput,
|
||||
f,
|
||||
); err != nil {
|
||||
log.Fatal(err)
|
||||
|
||||
@ -64,14 +64,14 @@ func DeployOverview(
|
||||
server = "local"
|
||||
}
|
||||
|
||||
domain := app.Domain
|
||||
domain := fmt.Sprintf("https://%s", app.Domain)
|
||||
if domain == "" {
|
||||
domain = config.NO_DOMAIN_DEFAULT
|
||||
domain = config.MISSING_DEFAULT
|
||||
}
|
||||
|
||||
envVersion := app.Recipe.EnvVersionRaw
|
||||
if envVersion == "" {
|
||||
envVersion = config.NO_VERSION_DEFAULT
|
||||
envVersion = config.MISSING_DEFAULT
|
||||
}
|
||||
|
||||
rows := [][]string{
|
||||
@ -127,7 +127,7 @@ func DeployOverview(
|
||||
}
|
||||
|
||||
response := false
|
||||
prompt := &survey.Confirm{Message: "proceed?"}
|
||||
prompt := &survey.Confirm{Message: i18n.G("proceed?")}
|
||||
if err := survey.AskOne(prompt, &response); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -140,32 +140,40 @@ func DeployOverview(
|
||||
}
|
||||
|
||||
func getDeployType(currentVersion, newVersion string) string {
|
||||
if newVersion == config.NO_DOMAIN_DEFAULT {
|
||||
if newVersion == config.MISSING_DEFAULT {
|
||||
return i18n.G("UNDEPLOY")
|
||||
}
|
||||
|
||||
if strings.Contains(newVersion, "+U") {
|
||||
return i18n.G("CHAOS DEPLOY")
|
||||
}
|
||||
|
||||
if strings.Contains(currentVersion, "+U") {
|
||||
return i18n.G("UNCHAOS DEPLOY")
|
||||
}
|
||||
|
||||
if currentVersion == newVersion {
|
||||
return ("REDEPLOY")
|
||||
}
|
||||
if currentVersion == config.NO_VERSION_DEFAULT {
|
||||
|
||||
if currentVersion == config.MISSING_DEFAULT {
|
||||
return i18n.G("NEW DEPLOY")
|
||||
}
|
||||
|
||||
currentParsed, err := tagcmp.Parse(currentVersion)
|
||||
if err != nil {
|
||||
return i18n.G("DEPLOY")
|
||||
}
|
||||
|
||||
newParsed, err := tagcmp.Parse(newVersion)
|
||||
if err != nil {
|
||||
return i18n.G("DEPLOY")
|
||||
}
|
||||
|
||||
if currentParsed.IsLessThan(newParsed) {
|
||||
return i18n.G("UPGRADE")
|
||||
}
|
||||
|
||||
return i18n.G("DOWNGRADE")
|
||||
}
|
||||
|
||||
@ -183,17 +191,17 @@ func MoveOverview(
|
||||
|
||||
domain := app.Domain
|
||||
if domain == "" {
|
||||
domain = config.NO_DOMAIN_DEFAULT
|
||||
domain = config.MISSING_DEFAULT
|
||||
}
|
||||
|
||||
secretsOverview := strings.Join(secrets, "\n")
|
||||
if len(secrets) == 0 {
|
||||
secretsOverview = config.NO_SECRETS_DEFAULT
|
||||
secretsOverview = config.MISSING_DEFAULT
|
||||
}
|
||||
|
||||
volumesOverview := strings.Join(volumes, "\n")
|
||||
if len(volumes) == 0 {
|
||||
volumesOverview = config.NO_VOLUMES_DEFAULT
|
||||
volumesOverview = config.MISSING_DEFAULT
|
||||
}
|
||||
|
||||
rows := [][]string{
|
||||
|
||||
@ -119,7 +119,7 @@ func init() {
|
||||
RecipeFetchCommand.Flags().BoolVarP(
|
||||
&fetchAllRecipes,
|
||||
i18n.G("all"),
|
||||
i18n.G("a"),
|
||||
i18n.GC("a", "recipe fetch"),
|
||||
false,
|
||||
i18n.G("fetch all recipes"),
|
||||
)
|
||||
|
||||
@ -14,7 +14,7 @@ import (
|
||||
gitPkg "coopcloud.tech/abra/pkg/git"
|
||||
"coopcloud.tech/abra/pkg/i18n"
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
"coopcloud.tech/abra/pkg/recipe"
|
||||
recipePkg "coopcloud.tech/abra/pkg/recipe"
|
||||
"coopcloud.tech/tagcmp"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/distribution/reference"
|
||||
@ -23,6 +23,9 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Errors
|
||||
var errEmptyVersionsInCatalogue = errors.New(i18n.G("catalogue versions list is unexpectedly empty"))
|
||||
|
||||
// translators: `abra recipe release` aliases. use a comma separated list of
|
||||
// aliases with no spaces in between
|
||||
var recipeReleaseAliases = i18n.G("rl")
|
||||
@ -50,7 +53,7 @@ recipe updates are properly communicated. I.e. developers of an app might
|
||||
publish a minor version but that might lead to changes in the recipe which are
|
||||
major and therefore require intervention while doing the upgrade work.
|
||||
|
||||
Publish your new release to git.coopcloud.tech with "--publish/-p". This
|
||||
This command will publish your new release to git.coopcloud.tech. This
|
||||
requires that you have permission to git push to these repositories and have
|
||||
your SSH keys configured on your account. Enable ssh-agent and make sure to add
|
||||
your private key and enter your passphrase beforehand.
|
||||
@ -60,12 +63,13 @@ your private key and enter your passphrase beforehand.
|
||||
Example: ` # publish release
|
||||
eval ` + "`ssh-agent`" + `
|
||||
ssh-add ~/.ssh/id_ed25519
|
||||
abra recipe release gitea -p`,
|
||||
abra recipe release gitea`,
|
||||
Args: cobra.RangeArgs(1, 2),
|
||||
ValidArgsFunction: func(
|
||||
cmd *cobra.Command,
|
||||
args []string,
|
||||
toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
toComplete string,
|
||||
) ([]string, cobra.ShellCompDirective) {
|
||||
switch l := len(args); l {
|
||||
case 0:
|
||||
return autocomplete.RecipeNameComplete()
|
||||
@ -93,19 +97,207 @@ your private key and enter your passphrase beforehand.
|
||||
log.Fatal(i18n.G("main app service version for %s is empty?", recipe.Name))
|
||||
}
|
||||
|
||||
isClean, err := gitPkg.IsClean(recipe.Dir)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if !isClean {
|
||||
log.Fatal(i18n.G("working directory not clean in %s, aborting", recipe.Dir))
|
||||
}
|
||||
|
||||
tags, err := recipe.Tags()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var tagString string
|
||||
if len(args) == 2 {
|
||||
tagString = args[1]
|
||||
}
|
||||
|
||||
if tagString != "" {
|
||||
if _, err := tagcmp.Parse(tagString); err != nil {
|
||||
log.Fatal(i18n.G("cannot parse %s, invalid tag specified?", tagString))
|
||||
if (internal.Major || internal.Minor || internal.Patch) && tagString != "" {
|
||||
log.Fatal(i18n.G("cannot specify tag and bump type at the same time"))
|
||||
}
|
||||
|
||||
if len(tags) == 0 && tagString == "" {
|
||||
log.Warn(i18n.G("no git tags found for %s", recipe.Name))
|
||||
if internal.NoInput {
|
||||
log.Fatal(i18n.G("unable to continue, input required for initial version"))
|
||||
}
|
||||
fmt.Println(i18n.G(`
|
||||
The following options are two types of initial semantic version that you can
|
||||
pick for %s that will be published in the recipe catalogue. This follows the
|
||||
semver convention (more on https://semver.org), here is a short cheatsheet
|
||||
|
||||
0.1.0: development release, still hacking. when you make a major upgrade
|
||||
you increment the "y" part (i.e. 0.1.0 -> 0.2.0) and only move to
|
||||
using the "x" part when things are stable.
|
||||
|
||||
1.0.0: public release, assumed to be working. you already have a stable
|
||||
and reliable deployment of this app and feel relatively confident
|
||||
about it.
|
||||
|
||||
If you want people to be able alpha test your current config for %s but don't
|
||||
think it is quite reliable, go with 0.1.0 and people will know that things are
|
||||
likely to change.
|
||||
|
||||
`, recipe.Name, recipe.Name))
|
||||
var chosenVersion string
|
||||
edPrompt := &survey.Select{
|
||||
Message: i18n.G("which version do you want to begin with?"),
|
||||
Options: []string{"0.1.0", "1.0.0"},
|
||||
}
|
||||
|
||||
if err := survey.AskOne(edPrompt, &chosenVersion); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
tagString = fmt.Sprintf("%s+%s", chosenVersion, mainAppVersion)
|
||||
}
|
||||
|
||||
if tagString == "" && (!internal.Major && !internal.Minor && !internal.Patch) {
|
||||
|
||||
catl, err := recipePkg.ReadRecipeCatalogue(false)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
changesTable, err := formatter.CreateTable()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
latestRelease := tags[len(tags)-1]
|
||||
latestRecipeVersion, err := getLatestVersion(recipe, catl)
|
||||
if err != nil && err != errEmptyVersionsInCatalogue {
|
||||
log.Fatal(err)
|
||||
}
|
||||
changesTable.Headers(i18n.G("SERVICE"), latestRelease, i18n.G("PROPOSED CHANGES"))
|
||||
|
||||
allRecipeVersions := catl[recipe.Name].Versions
|
||||
for _, recipeVersion := range allRecipeVersions {
|
||||
if serviceVersions, ok := recipeVersion[latestRecipeVersion]; ok {
|
||||
for serviceName := range serviceVersions {
|
||||
serviceMeta := serviceVersions[serviceName]
|
||||
|
||||
existingImageTag := fmt.Sprintf("%s:%s", serviceMeta.Image, serviceMeta.Tag)
|
||||
newImageTag := fmt.Sprintf("%s:%s", serviceMeta.Image, imagesTmp[serviceMeta.Image])
|
||||
|
||||
if existingImageTag == newImageTag {
|
||||
continue
|
||||
}
|
||||
|
||||
changesTable.Row([]string{serviceName, existingImageTag, newImageTag}...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
changeOverview := changesTable.Render()
|
||||
|
||||
if err := internal.PromptBumpType("", latestRelease, changeOverview); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
if (internal.Major || internal.Minor || internal.Patch) && tagString != "" {
|
||||
log.Fatal(i18n.G("cannot specify tag and bump type at the same time"))
|
||||
if tagString == "" {
|
||||
repo, err := git.PlainOpen(recipe.Dir)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var lastGitTag tagcmp.Tag
|
||||
iter, err := repo.Tags()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := iter.ForEach(func(ref *plumbing.Reference) error {
|
||||
obj, err := repo.TagObject(ref.Hash())
|
||||
if err != nil {
|
||||
log.Fatal(i18n.G("tag at commit %s is unannotated or otherwise broken", ref.Hash()))
|
||||
return err
|
||||
}
|
||||
|
||||
tagcmpTag, err := tagcmp.Parse(obj.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if (lastGitTag == tagcmp.Tag{}) {
|
||||
lastGitTag = tagcmpTag
|
||||
} else if tagcmpTag.IsGreaterThan(lastGitTag) {
|
||||
lastGitTag = tagcmpTag
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// bumpType is used to decide what part of the tag should be incremented
|
||||
bumpType := btoi(internal.Major)*4 + btoi(internal.Minor)*2 + btoi(internal.Patch)
|
||||
if bumpType != 0 {
|
||||
// a bitwise check if the number is a power of 2
|
||||
if (bumpType & (bumpType - 1)) != 0 {
|
||||
log.Fatal(i18n.G("you can only use one version flag: --major, --minor or --patch"))
|
||||
}
|
||||
}
|
||||
|
||||
newTag := lastGitTag
|
||||
if bumpType > 0 {
|
||||
if internal.Patch {
|
||||
now, err := strconv.Atoi(newTag.Patch)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
newTag.Patch = strconv.Itoa(now + 1)
|
||||
} else if internal.Minor {
|
||||
now, err := strconv.Atoi(newTag.Minor)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
newTag.Patch = "0"
|
||||
newTag.Minor = strconv.Itoa(now + 1)
|
||||
} else if internal.Major {
|
||||
now, err := strconv.Atoi(newTag.Major)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
newTag.Patch = "0"
|
||||
newTag.Minor = "0"
|
||||
newTag.Major = strconv.Itoa(now + 1)
|
||||
}
|
||||
}
|
||||
|
||||
newTag.Metadata = mainAppVersion
|
||||
log.Debug(i18n.G("choosing %s as new version for %s", newTag.String(), recipe.Name))
|
||||
tagString = newTag.String()
|
||||
}
|
||||
|
||||
if _, err := tagcmp.Parse(tagString); err != nil {
|
||||
log.Fatal(i18n.G("invalid version %s specified", tagString))
|
||||
}
|
||||
|
||||
mainService := "app"
|
||||
label := i18n.G("coop-cloud.${STACK_NAME}.version=%s", tagString)
|
||||
if !internal.Dry {
|
||||
if err := recipe.UpdateLabel("compose.y*ml", mainService, label); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
} else {
|
||||
log.Info(i18n.G("dry run: not syncing label %s for recipe %s", tagString, recipe.Name))
|
||||
}
|
||||
|
||||
for _, tag := range tags {
|
||||
previousTagLeftHand := strings.Split(tag, "+")[0]
|
||||
newTagStringLeftHand := strings.Split(tagString, "+")[0]
|
||||
if previousTagLeftHand == newTagStringLeftHand {
|
||||
log.Fatal(i18n.G("%s+... conflicts with a previous release: %s", newTagStringLeftHand, tag))
|
||||
}
|
||||
}
|
||||
|
||||
repo, err := git.PlainOpen(recipe.Dir)
|
||||
@ -118,75 +310,20 @@ your private key and enter your passphrase beforehand.
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if tagString != "" {
|
||||
if err := createReleaseFromTag(recipe, tagString, mainAppVersion); err != nil {
|
||||
if cleanErr := cleanTag(recipe, tagString); cleanErr != nil {
|
||||
log.Fatal(cleanErr)
|
||||
}
|
||||
if cleanErr := cleanCommit(recipe, preCommitHead); cleanErr != nil {
|
||||
log.Fatal(cleanErr)
|
||||
}
|
||||
log.Fatal(err)
|
||||
if err := createReleaseFromTag(recipe, tagString, mainAppVersion); err != nil {
|
||||
if cleanErr := cleanTag(recipe, tagString); cleanErr != nil {
|
||||
log.Fatal(cleanErr)
|
||||
}
|
||||
if cleanErr := cleanCommit(recipe, preCommitHead); cleanErr != nil {
|
||||
log.Fatal(cleanErr)
|
||||
}
|
||||
}
|
||||
|
||||
tags, err := recipe.Tags()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if tagString == "" && (!internal.Major && !internal.Minor && !internal.Patch) {
|
||||
var err error
|
||||
tagString, err = getLabelVersion(recipe, false)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
isClean, err := gitPkg.IsClean(recipe.Dir)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if !isClean {
|
||||
log.Info(i18n.G("%s currently has these unstaged changes 👇", recipe.Name))
|
||||
if err := gitPkg.DiffUnstaged(recipe.Dir); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(tags) > 0 {
|
||||
log.Warn(i18n.G("previous git tags detected, assuming new semver release"))
|
||||
|
||||
if err := createReleaseFromPreviousTag(tagString, mainAppVersion, recipe, tags); err != nil {
|
||||
if cleanErr := cleanTag(recipe, tagString); cleanErr != nil {
|
||||
log.Fatal(cleanErr)
|
||||
}
|
||||
if cleanErr := cleanCommit(recipe, preCommitHead); cleanErr != nil {
|
||||
log.Fatal(cleanErr)
|
||||
}
|
||||
log.Fatal(err)
|
||||
}
|
||||
} else {
|
||||
log.Warn(i18n.G("no tag specified and no previous tag available for %s, assuming initial release", recipe.Name))
|
||||
|
||||
if err := createReleaseFromTag(recipe, tagString, mainAppVersion); err != nil {
|
||||
if cleanErr := cleanTag(recipe, tagString); cleanErr != nil {
|
||||
log.Fatal(cleanErr)
|
||||
}
|
||||
if cleanErr := cleanCommit(recipe, preCommitHead); cleanErr != nil {
|
||||
log.Fatal(cleanErr)
|
||||
}
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
},
|
||||
}
|
||||
|
||||
// GetImageVersions retrieves image versions for a recipe
|
||||
func GetImageVersions(recipe recipe.Recipe) (map[string]string, error) {
|
||||
func GetImageVersions(recipe recipePkg.Recipe) (map[string]string, error) {
|
||||
services := make(map[string]string)
|
||||
|
||||
config, err := recipe.GetComposeConfig(nil)
|
||||
@ -230,7 +367,7 @@ func GetImageVersions(recipe recipe.Recipe) (map[string]string, error) {
|
||||
}
|
||||
|
||||
// createReleaseFromTag creates a new release based on a supplied recipe version string
|
||||
func createReleaseFromTag(recipe recipe.Recipe, tagString, mainAppVersion string) error {
|
||||
func createReleaseFromTag(recipe recipePkg.Recipe, tagString, mainAppVersion string) error {
|
||||
var err error
|
||||
|
||||
repo, err := git.PlainOpen(recipe.Dir)
|
||||
@ -293,7 +430,7 @@ func getTagCreateOptions(tag string) (git.CreateTagOptions, error) {
|
||||
|
||||
// addReleaseNotes checks if the release/next release note exists and moves the
|
||||
// file to release/<tag>.
|
||||
func addReleaseNotes(recipe recipe.Recipe, tag string) error {
|
||||
func addReleaseNotes(recipe recipePkg.Recipe, tag string) error {
|
||||
releaseDir := path.Join(recipe.Dir, "release")
|
||||
if _, err := os.Stat(releaseDir); errors.Is(err, os.ErrNotExist) {
|
||||
if err := os.Mkdir(releaseDir, 0755); err != nil {
|
||||
@ -321,7 +458,7 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error {
|
||||
|
||||
if !internal.NoInput {
|
||||
prompt := &survey.Confirm{
|
||||
Message: i18n.G("Use release note in release/next?"),
|
||||
Message: i18n.G("use release note in release/next?"),
|
||||
}
|
||||
|
||||
if err := survey.AskOne(prompt, &addNextAsReleaseNotes); err != nil {
|
||||
@ -378,7 +515,7 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func commitRelease(recipe recipe.Recipe, tag string) error {
|
||||
func commitRelease(recipe recipePkg.Recipe, tag string) error {
|
||||
if internal.Dry {
|
||||
log.Debug(i18n.G("dry run: no changes committed"))
|
||||
return nil
|
||||
@ -430,140 +567,29 @@ func tagRelease(tagString string, repo *git.Repository) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func pushRelease(recipe recipe.Recipe, tagString string) error {
|
||||
func pushRelease(recipe recipePkg.Recipe, tagString string) error {
|
||||
if internal.Dry {
|
||||
log.Info(i18n.G("dry run: no changes published"))
|
||||
return nil
|
||||
}
|
||||
|
||||
if !publish && !internal.NoInput {
|
||||
prompt := &survey.Confirm{
|
||||
Message: i18n.G("publish new release?"),
|
||||
}
|
||||
|
||||
if err := survey.AskOne(prompt, &publish); err != nil {
|
||||
return err
|
||||
}
|
||||
if os.Getenv("SSH_AUTH_SOCK") == "" {
|
||||
return errors.New(i18n.G("ssh-agent not found. see \"abra recipe release --help\" and try again"))
|
||||
}
|
||||
|
||||
if publish {
|
||||
if os.Getenv("SSH_AUTH_SOCK") == "" {
|
||||
return errors.New(i18n.G("ssh-agent not found. see \"abra recipe release --help\" and try again"))
|
||||
}
|
||||
|
||||
if err := recipe.Push(internal.Dry); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/src/tag/%s", recipe.GitURL, tagString)
|
||||
log.Info(i18n.G("new release published: %s", url))
|
||||
} else {
|
||||
log.Info(i18n.G("no -p/--publish passed, not publishing"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createReleaseFromPreviousTag(tagString, mainAppVersion string, recipe recipe.Recipe, tags []string) error {
|
||||
repo, err := git.PlainOpen(recipe.Dir)
|
||||
if err != nil {
|
||||
if err := recipe.Push(internal.Dry); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bumpType := btoi(internal.Major)*4 + btoi(internal.Minor)*2 + btoi(internal.Patch)
|
||||
if bumpType != 0 {
|
||||
if (bumpType & (bumpType - 1)) != 0 {
|
||||
return errors.New(i18n.G("you can only use one of: --major, --minor, --patch"))
|
||||
}
|
||||
}
|
||||
|
||||
var lastGitTag tagcmp.Tag
|
||||
for _, tag := range tags {
|
||||
parsed, err := tagcmp.Parse(tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if (lastGitTag == tagcmp.Tag{}) {
|
||||
lastGitTag = parsed
|
||||
} else if parsed.IsGreaterThan(lastGitTag) {
|
||||
lastGitTag = parsed
|
||||
}
|
||||
}
|
||||
|
||||
newTag := lastGitTag
|
||||
if internal.Patch {
|
||||
now, err := strconv.Atoi(newTag.Patch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newTag.Patch = strconv.Itoa(now + 1)
|
||||
} else if internal.Minor {
|
||||
now, err := strconv.Atoi(newTag.Minor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newTag.Patch = "0"
|
||||
newTag.Minor = strconv.Itoa(now + 1)
|
||||
} else if internal.Major {
|
||||
now, err := strconv.Atoi(newTag.Major)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newTag.Patch = "0"
|
||||
newTag.Minor = "0"
|
||||
newTag.Major = strconv.Itoa(now + 1)
|
||||
}
|
||||
|
||||
if internal.Major || internal.Minor || internal.Patch {
|
||||
newTag.Metadata = mainAppVersion
|
||||
tagString = newTag.String()
|
||||
}
|
||||
|
||||
if lastGitTag.String() == tagString {
|
||||
return errors.New(i18n.G("latest git tag (%s) and synced label (%s) are the same?", lastGitTag, tagString))
|
||||
}
|
||||
|
||||
if !internal.NoInput {
|
||||
prompt := &survey.Confirm{
|
||||
Message: i18n.G("current: %s, new: %s, correct?", lastGitTag, tagString),
|
||||
}
|
||||
|
||||
var ok bool
|
||||
if err := survey.AskOne(prompt, &ok); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !ok {
|
||||
return errors.New(i18n.G("exiting as requested"))
|
||||
}
|
||||
}
|
||||
|
||||
if err := addReleaseNotes(recipe, tagString); err != nil {
|
||||
return errors.New(i18n.G("failed to add release notes: %s", err.Error()))
|
||||
}
|
||||
|
||||
if err := commitRelease(recipe, tagString); err != nil {
|
||||
return errors.New(i18n.G("failed to commit changes: %s", err.Error()))
|
||||
}
|
||||
|
||||
if err := tagRelease(tagString, repo); err != nil {
|
||||
return errors.New(i18n.G("failed to tag release: %s", err.Error()))
|
||||
}
|
||||
|
||||
if err := pushRelease(recipe, tagString); err != nil {
|
||||
return errors.New(i18n.G("failed to publish new release: %s", err.Error()))
|
||||
}
|
||||
url := fmt.Sprintf("%s/src/tag/%s", recipe.GitURL, tagString)
|
||||
log.Info(i18n.G("new release published: %s", url))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// cleanCommit soft removes the latest release commit. No change are lost the
|
||||
// the commit itself is removed. This is the equivalent of `git reset HEAD~1`.
|
||||
func cleanCommit(recipe recipe.Recipe, head *plumbing.Reference) error {
|
||||
func cleanCommit(recipe recipePkg.Recipe, head *plumbing.Reference) error {
|
||||
repo, err := git.PlainOpen(recipe.Dir)
|
||||
if err != nil {
|
||||
return errors.New(i18n.G("unable to open repo in %s: %s", recipe.Dir, err))
|
||||
@ -585,7 +611,7 @@ func cleanCommit(recipe recipe.Recipe, head *plumbing.Reference) error {
|
||||
}
|
||||
|
||||
// cleanTag removes a freshly created tag
|
||||
func cleanTag(recipe recipe.Recipe, tag string) error {
|
||||
func cleanTag(recipe recipePkg.Recipe, tag string) error {
|
||||
repo, err := git.PlainOpen(recipe.Dir)
|
||||
if err != nil {
|
||||
return errors.New(i18n.G("unable to open repo in %s: %s", recipe.Dir, err))
|
||||
@ -602,37 +628,17 @@ func cleanTag(recipe recipe.Recipe, tag string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func getLabelVersion(recipe recipe.Recipe, prompt bool) (string, error) {
|
||||
initTag, err := recipe.GetVersionLabelLocal()
|
||||
func getLatestVersion(recipe recipePkg.Recipe, catl recipePkg.RecipeCatalogue) (string, error) {
|
||||
versions, err := recipePkg.GetRecipeCatalogueVersions(recipe.Name, catl)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if initTag == "" {
|
||||
return "", errors.New(i18n.G("unable to read version for %s from synced label. Did you try running \"abra recipe sync %s\" already?", recipe.Name, recipe.Name))
|
||||
if len(versions) > 0 {
|
||||
return versions[len(versions)-1], nil
|
||||
}
|
||||
|
||||
log.Warn(i18n.G("discovered %s as currently synced recipe label", initTag))
|
||||
|
||||
if prompt && !internal.NoInput {
|
||||
var response bool
|
||||
prompt := &survey.Confirm{Message: i18n.G("use %s as the new version?", initTag)}
|
||||
if err := survey.AskOne(prompt, &response); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if !response {
|
||||
return "", errors.New(i18n.G("please fix your synced label for %s and re-run this command", recipe.Name))
|
||||
}
|
||||
}
|
||||
|
||||
return initTag, nil
|
||||
return "", errEmptyVersionsInCatalogue
|
||||
}
|
||||
|
||||
var (
|
||||
publish bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
RecipeReleaseCommand.Flags().BoolVarP(
|
||||
&internal.Dry,
|
||||
@ -665,12 +671,4 @@ func init() {
|
||||
false,
|
||||
i18n.G("increase the patch part of the version"),
|
||||
)
|
||||
|
||||
RecipeReleaseCommand.Flags().BoolVarP(
|
||||
&publish,
|
||||
i18n.G("publish"),
|
||||
i18n.G("p"),
|
||||
false,
|
||||
i18n.G("publish changes to git.coopcloud.tech"),
|
||||
)
|
||||
}
|
||||
|
||||
33
cli/recipe/release_test.go
Normal file
33
cli/recipe/release_test.go
Normal file
@ -0,0 +1,33 @@
|
||||
package recipe
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
recipePkg "coopcloud.tech/abra/pkg/recipe"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetLatestVersionReturnsErrorWhenVersionsIsEmpty(t *testing.T) {
|
||||
recipe := recipePkg.Recipe{}
|
||||
catalogue := recipePkg.RecipeCatalogue{}
|
||||
_, err := getLatestVersion(recipe, catalogue)
|
||||
assert.Equal(t, err, errEmptyVersionsInCatalogue)
|
||||
}
|
||||
|
||||
func TestGetLatestVersionReturnsLastVersion(t *testing.T) {
|
||||
recipe := recipePkg.Recipe{
|
||||
Name: "test",
|
||||
}
|
||||
versions := []map[string]map[string]recipePkg.ServiceMeta{
|
||||
make(map[string]map[string]recipePkg.ServiceMeta),
|
||||
make(map[string]map[string]recipePkg.ServiceMeta),
|
||||
}
|
||||
versions[0]["0.0.3"] = make(map[string]recipePkg.ServiceMeta)
|
||||
versions[1]["0.0.2"] = make(map[string]recipePkg.ServiceMeta)
|
||||
catalogue := make(recipePkg.RecipeCatalogue)
|
||||
catalogue["test"] = recipePkg.RecipeMeta{
|
||||
Versions: versions,
|
||||
}
|
||||
version, _ := getLatestVersion(recipe, catalogue)
|
||||
assert.Equal(t, version, "0.0.3")
|
||||
}
|
||||
@ -1,298 +0,0 @@
|
||||
package recipe
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
"coopcloud.tech/abra/pkg/formatter"
|
||||
gitPkg "coopcloud.tech/abra/pkg/git"
|
||||
"coopcloud.tech/abra/pkg/i18n"
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
recipePkg "coopcloud.tech/abra/pkg/recipe"
|
||||
"coopcloud.tech/tagcmp"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// translators: `abra recipe reset` aliases. use a comma separated list of
|
||||
// aliases with no spaces in between
|
||||
var recipeSyncAliases = i18n.G("s")
|
||||
|
||||
var RecipeSyncCommand = &cobra.Command{
|
||||
// translators: `recipe sync` command
|
||||
Use: i18n.G("sync <recipe> [version] [flags]"),
|
||||
Aliases: strings.Split(recipeSyncAliases, ","),
|
||||
// translators: Short description for `recipe sync` command
|
||||
Short: i18n.G("Sync recipe version label"),
|
||||
Long: i18n.G(`Generate labels for the main recipe service.
|
||||
|
||||
By convention, the service named "app" using the following format:
|
||||
|
||||
coop-cloud.${STACK_NAME}.version=<version>
|
||||
|
||||
Where [version] can be specifed on the command-line or Abra can attempt to
|
||||
auto-generate it for you. The <recipe> configuration will be updated on the
|
||||
local file system.`),
|
||||
Args: cobra.RangeArgs(1, 2),
|
||||
ValidArgsFunction: func(
|
||||
cmd *cobra.Command,
|
||||
args []string,
|
||||
toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
switch l := len(args); l {
|
||||
case 0:
|
||||
return autocomplete.RecipeNameComplete()
|
||||
case 1:
|
||||
return autocomplete.RecipeVersionComplete(args[0])
|
||||
default:
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
recipe := internal.ValidateRecipe(args, cmd.Name())
|
||||
|
||||
mainApp, err := internal.GetMainAppImage(recipe)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
imagesTmp, err := GetImageVersions(recipe)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
mainAppVersion := imagesTmp[mainApp]
|
||||
|
||||
tags, err := recipe.Tags()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var nextTag string
|
||||
if len(args) == 2 {
|
||||
nextTag = args[1]
|
||||
}
|
||||
|
||||
if len(tags) == 0 && nextTag == "" {
|
||||
log.Warn(i18n.G("no git tags found for %s", recipe.Name))
|
||||
if internal.NoInput {
|
||||
log.Fatal(i18n.G("unable to continue, input required for initial version"))
|
||||
}
|
||||
fmt.Println(i18n.G(`
|
||||
The following options are two types of initial semantic version that you can
|
||||
pick for %s that will be published in the recipe catalogue. This follows the
|
||||
semver convention (more on https://semver.org), here is a short cheatsheet
|
||||
|
||||
0.1.0: development release, still hacking. when you make a major upgrade
|
||||
you increment the "y" part (i.e. 0.1.0 -> 0.2.0) and only move to
|
||||
using the "x" part when things are stable.
|
||||
|
||||
1.0.0: public release, assumed to be working. you already have a stable
|
||||
and reliable deployment of this app and feel relatively confident
|
||||
about it.
|
||||
|
||||
If you want people to be able alpha test your current config for %s but don't
|
||||
think it is quite reliable, go with 0.1.0 and people will know that things are
|
||||
likely to change.
|
||||
|
||||
`, recipe.Name, recipe.Name))
|
||||
var chosenVersion string
|
||||
edPrompt := &survey.Select{
|
||||
Message: i18n.G("which version do you want to begin with?"),
|
||||
Options: []string{"0.1.0", "1.0.0"},
|
||||
}
|
||||
|
||||
if err := survey.AskOne(edPrompt, &chosenVersion); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
nextTag = fmt.Sprintf("%s+%s", chosenVersion, mainAppVersion)
|
||||
}
|
||||
|
||||
if nextTag == "" && (!internal.Major && !internal.Minor && !internal.Patch) {
|
||||
var changeOverview string
|
||||
|
||||
catl, err := recipePkg.ReadRecipeCatalogue(false)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
versions, err := recipePkg.GetRecipeCatalogueVersions(recipe.Name, catl)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
changesTable, err := formatter.CreateTable()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
latestRelease := tags[len(tags)-1]
|
||||
changesTable.Headers(i18n.G("SERVICE"), latestRelease, i18n.G("PROPOSED CHANGES"))
|
||||
|
||||
latestRecipeVersion := versions[len(versions)-1]
|
||||
allRecipeVersions := catl[recipe.Name].Versions
|
||||
for _, recipeVersion := range allRecipeVersions {
|
||||
if serviceVersions, ok := recipeVersion[latestRecipeVersion]; ok {
|
||||
for serviceName := range serviceVersions {
|
||||
serviceMeta := serviceVersions[serviceName]
|
||||
changesTable.Row(
|
||||
[]string{
|
||||
serviceName,
|
||||
fmt.Sprintf("%s:%s", serviceMeta.Image, serviceMeta.Tag),
|
||||
fmt.Sprintf("%s:%s", serviceMeta.Image, imagesTmp[serviceMeta.Image]),
|
||||
}...,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
changeOverview = changesTable.Render()
|
||||
|
||||
if err := internal.PromptBumpType("", latestRelease, changeOverview); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
if nextTag == "" {
|
||||
repo, err := git.PlainOpen(recipe.Dir)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var lastGitTag tagcmp.Tag
|
||||
iter, err := repo.Tags()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := iter.ForEach(func(ref *plumbing.Reference) error {
|
||||
obj, err := repo.TagObject(ref.Hash())
|
||||
if err != nil {
|
||||
log.Fatal(i18n.G("tag at commit %s is unannotated or otherwise broken", ref.Hash()))
|
||||
return err
|
||||
}
|
||||
|
||||
tagcmpTag, err := tagcmp.Parse(obj.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if (lastGitTag == tagcmp.Tag{}) {
|
||||
lastGitTag = tagcmpTag
|
||||
} else if tagcmpTag.IsGreaterThan(lastGitTag) {
|
||||
lastGitTag = tagcmpTag
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// bumpType is used to decide what part of the tag should be incremented
|
||||
bumpType := btoi(internal.Major)*4 + btoi(internal.Minor)*2 + btoi(internal.Patch)
|
||||
if bumpType != 0 {
|
||||
// a bitwise check if the number is a power of 2
|
||||
if (bumpType & (bumpType - 1)) != 0 {
|
||||
log.Fatal(i18n.G("you can only use one version flag: --major, --minor or --patch"))
|
||||
}
|
||||
}
|
||||
|
||||
newTag := lastGitTag
|
||||
if bumpType > 0 {
|
||||
if internal.Patch {
|
||||
now, err := strconv.Atoi(newTag.Patch)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
newTag.Patch = strconv.Itoa(now + 1)
|
||||
} else if internal.Minor {
|
||||
now, err := strconv.Atoi(newTag.Minor)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
newTag.Patch = "0"
|
||||
newTag.Minor = strconv.Itoa(now + 1)
|
||||
} else if internal.Major {
|
||||
now, err := strconv.Atoi(newTag.Major)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
newTag.Patch = "0"
|
||||
newTag.Minor = "0"
|
||||
newTag.Major = strconv.Itoa(now + 1)
|
||||
}
|
||||
}
|
||||
|
||||
newTag.Metadata = mainAppVersion
|
||||
log.Debug(i18n.G("choosing %s as new version for %s", newTag.String(), recipe.Name))
|
||||
nextTag = newTag.String()
|
||||
}
|
||||
|
||||
if _, err := tagcmp.Parse(nextTag); err != nil {
|
||||
log.Fatal(i18n.G("invalid version %s specified", nextTag))
|
||||
}
|
||||
|
||||
mainService := "app"
|
||||
label := i18n.G("coop-cloud.${STACK_NAME}.version=%s", nextTag)
|
||||
if !internal.Dry {
|
||||
if err := recipe.UpdateLabel("compose.y*ml", mainService, label); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
} else {
|
||||
log.Info(i18n.G("dry run: not syncing label %s for recipe %s", nextTag, recipe.Name))
|
||||
}
|
||||
|
||||
isClean, err := gitPkg.IsClean(recipe.Dir)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if !isClean {
|
||||
log.Info(i18n.G("%s currently has these unstaged changes 👇", recipe.Name))
|
||||
if err := gitPkg.DiffUnstaged(recipe.Dir); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
RecipeSyncCommand.Flags().BoolVarP(
|
||||
&internal.Dry,
|
||||
i18n.G("dry-run"),
|
||||
i18n.G("r"),
|
||||
false,
|
||||
i18n.G("report changes that would be made"),
|
||||
)
|
||||
|
||||
RecipeSyncCommand.Flags().BoolVarP(
|
||||
&internal.Major,
|
||||
i18n.G("major"),
|
||||
i18n.G("x"),
|
||||
false,
|
||||
i18n.G("increase the major part of the version"),
|
||||
)
|
||||
|
||||
RecipeSyncCommand.Flags().BoolVarP(
|
||||
&internal.Minor,
|
||||
i18n.G("minor"),
|
||||
i18n.G("y"),
|
||||
false,
|
||||
i18n.G("increase the minor part of the version"),
|
||||
)
|
||||
|
||||
RecipeSyncCommand.Flags().BoolVarP(
|
||||
&internal.Patch,
|
||||
i18n.G("patch"),
|
||||
i18n.G("z"),
|
||||
false,
|
||||
i18n.G("increase the patch part of the version"),
|
||||
)
|
||||
}
|
||||
@ -57,14 +57,13 @@ is up to the end-user to decide.
|
||||
|
||||
The command is interactive and will show a select input which allows you to
|
||||
make a seclection. Use the "?" key to see more help on navigating this
|
||||
interface.
|
||||
|
||||
You may invoke this command in "wizard" mode and be prompted for input.`),
|
||||
interface.`),
|
||||
Args: cobra.RangeArgs(0, 1),
|
||||
ValidArgsFunction: func(
|
||||
cmd *cobra.Command,
|
||||
args []string,
|
||||
toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
toComplete string,
|
||||
) ([]string, cobra.ShellCompDirective) {
|
||||
return autocomplete.RecipeNameComplete()
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
@ -337,12 +336,37 @@ You may invoke this command in "wizard" mode and be prompted for input.`),
|
||||
if err := gitPkg.DiffUnstaged(recipe.Dir); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if !internal.NoInput && !createCommit {
|
||||
prompt := &survey.Confirm{
|
||||
Message: i18n.G("commit changes?"),
|
||||
Default: true,
|
||||
}
|
||||
|
||||
if err := survey.AskOne(prompt, &createCommit); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
if createCommit {
|
||||
msg := i18n.G("chore: update image tags")
|
||||
if err := gitPkg.Commit(recipe.Dir, msg, internal.Dry); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Info(i18n.G("committed changes as '%s'", msg))
|
||||
}
|
||||
|
||||
} else {
|
||||
if createCommit {
|
||||
log.Warn(i18n.G("no changes, skip creating commit"))
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
var (
|
||||
allTags bool
|
||||
allTags bool
|
||||
createCommit bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -381,8 +405,16 @@ func init() {
|
||||
RecipeUpgradeCommand.Flags().BoolVarP(
|
||||
&allTags,
|
||||
i18n.G("all-tags"),
|
||||
i18n.G("a"),
|
||||
i18n.GC("a", "recipe upgrade"),
|
||||
false,
|
||||
i18n.G("list all tags, not just upgrades"),
|
||||
)
|
||||
|
||||
RecipeUpgradeCommand.Flags().BoolVarP(
|
||||
&createCommit,
|
||||
i18n.G("commit"),
|
||||
i18n.GC("c", "recipe upgrade"),
|
||||
false,
|
||||
i18n.G("commit changes"),
|
||||
)
|
||||
}
|
||||
|
||||
30
cli/run.go
30
cli/run.go
@ -187,28 +187,20 @@ Config:
|
||||
|
||||
rootCmd.PersistentFlags().BoolVarP(
|
||||
&internal.Debug,
|
||||
"debug",
|
||||
"d",
|
||||
i18n.G("debug"),
|
||||
i18n.G("d"),
|
||||
false,
|
||||
i18n.G("show debug messages"),
|
||||
)
|
||||
|
||||
rootCmd.PersistentFlags().BoolVarP(
|
||||
&internal.NoInput,
|
||||
"no-input",
|
||||
"n",
|
||||
i18n.G("no-input"),
|
||||
i18n.G("n"),
|
||||
false,
|
||||
i18n.G("toggle non-interactive mode"),
|
||||
)
|
||||
|
||||
rootCmd.PersistentFlags().BoolVarP(
|
||||
&internal.Offline,
|
||||
"offline",
|
||||
"o",
|
||||
false,
|
||||
i18n.G("prefer offline & filesystem access"),
|
||||
)
|
||||
|
||||
rootCmd.PersistentFlags().BoolVarP(
|
||||
&internal.Help,
|
||||
i18n.G("help"),
|
||||
@ -217,6 +209,14 @@ Config:
|
||||
i18n.G("help for abra"),
|
||||
)
|
||||
|
||||
rootCmd.PersistentFlags().BoolVarP(
|
||||
&internal.Offline,
|
||||
i18n.G("offline"),
|
||||
i18n.G("o"),
|
||||
false,
|
||||
i18n.G("prefer offline & filesystem access"),
|
||||
)
|
||||
|
||||
rootCmd.Flags().BoolVarP(
|
||||
&internal.Version,
|
||||
i18n.G("version"),
|
||||
@ -245,7 +245,6 @@ Config:
|
||||
recipe.RecipeNewCommand,
|
||||
recipe.RecipeReleaseCommand,
|
||||
recipe.RecipeResetCommand,
|
||||
recipe.RecipeSyncCommand,
|
||||
recipe.RecipeUpgradeCommand,
|
||||
recipe.RecipeVersionCommand,
|
||||
)
|
||||
@ -283,6 +282,11 @@ Config:
|
||||
app.AppBackupSnapshotsCommand,
|
||||
)
|
||||
|
||||
app.AppEnvCommand.AddCommand(
|
||||
app.AppEnvListCommand,
|
||||
app.AppEnvPullCommand,
|
||||
)
|
||||
|
||||
app.AppCommand.AddCommand(
|
||||
app.AppBackupCommand,
|
||||
app.AppCheckCommand,
|
||||
|
||||
@ -20,7 +20,7 @@ import (
|
||||
|
||||
// translators: `abra server add` aliases. use a comma separated list of
|
||||
// aliases with no spaces in between
|
||||
var serverAddAliases = i18n.G("a")
|
||||
var serverAddAliases = i18n.GC("a", "server add")
|
||||
|
||||
var ServerAddCommand = &cobra.Command{
|
||||
// translators: `server add` command
|
||||
|
||||
@ -96,7 +96,7 @@ func init() {
|
||||
ServerPruneCommand.Flags().BoolVarP(
|
||||
&allFilter,
|
||||
i18n.G("all"),
|
||||
i18n.G("a"),
|
||||
i18n.GC("a", "server prune"),
|
||||
false,
|
||||
i18n.G("remove all unused images"),
|
||||
)
|
||||
|
||||
@ -1,558 +0,0 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
appPkg "coopcloud.tech/abra/pkg/app"
|
||||
"coopcloud.tech/abra/pkg/client"
|
||||
"coopcloud.tech/abra/pkg/deploy"
|
||||
"coopcloud.tech/abra/pkg/envfile"
|
||||
"coopcloud.tech/abra/pkg/i18n"
|
||||
"coopcloud.tech/abra/pkg/lint"
|
||||
"coopcloud.tech/abra/pkg/recipe"
|
||||
"coopcloud.tech/abra/pkg/upstream/convert"
|
||||
"coopcloud.tech/abra/pkg/upstream/stack"
|
||||
"coopcloud.tech/tagcmp"
|
||||
charmLog "github.com/charmbracelet/log"
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
dockerclient "github.com/docker/docker/client"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
)
|
||||
|
||||
const SERVER = "localhost"
|
||||
|
||||
// translators: `kadabra notify` aliases. use a comma separated list of aliases
|
||||
// with no spaces in between
|
||||
var notifyAliases = i18n.G("n")
|
||||
|
||||
// NotifyCommand checks for available upgrades.
|
||||
var NotifyCommand = &cobra.Command{
|
||||
// translators: `notify` command
|
||||
Use: i18n.G("notify [flags]"),
|
||||
Aliases: strings.Split(notifyAliases, ","),
|
||||
// translators: Short description for `notify` command
|
||||
Short: i18n.G("Check for available upgrades"),
|
||||
Long: i18n.G(`Notify on new versions for deployed apps.
|
||||
|
||||
If a new patch/minor version is available, a notification is printed.
|
||||
|
||||
Use "--major/-m" to include new major versions.`),
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cl, err := client.New("default")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
stacks, err := stack.GetStacks(cl)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
for _, stackInfo := range stacks {
|
||||
stackName := stackInfo.Name
|
||||
recipeName, err := getLabel(cl, stackName, "recipe")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if recipeName != "" {
|
||||
_, err = getLatestUpgrade(cl, stackName, recipeName)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// translators: `kadabra upgrade` aliases. use a comma separated list of aliases with
|
||||
// no spaces in between
|
||||
var upgradeAliases = i18n.G("u")
|
||||
|
||||
// UpgradeCommand upgrades apps.
|
||||
var UpgradeCommand = &cobra.Command{
|
||||
// translators: `app upgrade` command
|
||||
Use: i18n.G("upgrade [[stack] [recipe] | --all] [flags]"),
|
||||
Aliases: strings.Split(upgradeAliases, ","),
|
||||
// translators: Short description for `app upgrade` command
|
||||
Short: i18n.G("Upgrade apps"),
|
||||
Long: i18n.G(`Upgrade an app by specifying stack name and recipe.
|
||||
|
||||
Use "--all" to upgrade every deployed app.
|
||||
|
||||
For each app with auto updates enabled, the deployed version is compared with
|
||||
the current recipe catalogue version. If a new patch/minor version is
|
||||
available, the app is upgraded.
|
||||
|
||||
To include major versions use the "--major/-m" flag. You probably don't want
|
||||
that as it will break things. Only apps that are not deployed with "--chaos/-C"
|
||||
are upgraded, to update chaos deployments use the "--chaos/-C" flag. Use it
|
||||
with care.`),
|
||||
Args: cobra.RangeArgs(0, 2),
|
||||
// TODO(d1): complete stack/recipe
|
||||
// ValidArgsFunction: func(
|
||||
// cmd *cobra.Command,
|
||||
// args []string,
|
||||
// toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
// },
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cl, err := client.New("default")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if !updateAll && len(args) != 2 {
|
||||
log.Fatal(i18n.G("missing arguments or --all/-a flag"))
|
||||
}
|
||||
|
||||
if !updateAll {
|
||||
stackName := args[0]
|
||||
recipeName := args[1]
|
||||
|
||||
err = tryUpgrade(cl, stackName, recipeName)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
stacks, err := stack.GetStacks(cl)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
for _, stackInfo := range stacks {
|
||||
stackName := stackInfo.Name
|
||||
recipeName, err := getLabel(cl, stackName, "recipe")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = tryUpgrade(cl, stackName, recipeName)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// getLabel reads docker labels from running services in the format of "coop-cloud.${STACK_NAME}.${LABEL}".
|
||||
func getLabel(cl *dockerclient.Client, stackName string, label string) (string, error) {
|
||||
filter := filters.NewArgs()
|
||||
filter.Add("label", fmt.Sprintf("%s=%s", convert.LabelNamespace, stackName))
|
||||
|
||||
services, err := cl.ServiceList(context.Background(), types.ServiceListOptions{Filters: filter})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, service := range services {
|
||||
labelKey := fmt.Sprintf("coop-cloud.%s.%s", stackName, label)
|
||||
if labelValue, ok := service.Spec.Labels[labelKey]; ok {
|
||||
return labelValue, nil
|
||||
}
|
||||
}
|
||||
|
||||
log.Debug(i18n.G("no %s label found for %s", label, stackName))
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// getBoolLabel reads a boolean docker label from running services
|
||||
func getBoolLabel(cl *dockerclient.Client, stackName string, label string) (bool, error) {
|
||||
lableValue, err := getLabel(cl, stackName, label)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if lableValue != "" {
|
||||
value, err := strconv.ParseBool(lableValue)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return value, nil
|
||||
}
|
||||
|
||||
log.Debug(i18n.G("boolean label %s could not be found for %s, set default to false.", label, stackName))
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// getEnv reads env variables from docker services.
|
||||
func getEnv(cl *dockerclient.Client, stackName string) (envfile.AppEnv, error) {
|
||||
envMap := make(map[string]string)
|
||||
filter := filters.NewArgs()
|
||||
filter.Add("label", fmt.Sprintf("%s=%s", convert.LabelNamespace, stackName))
|
||||
|
||||
services, err := cl.ServiceList(context.Background(), types.ServiceListOptions{Filters: filter})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, service := range services {
|
||||
envList := service.Spec.TaskTemplate.ContainerSpec.Env
|
||||
for _, envString := range envList {
|
||||
splitString := strings.SplitN(envString, "=", 2)
|
||||
if len(splitString) != 2 {
|
||||
log.Debug(i18n.G("can't separate key from value: %s (this variable is probably unset)", envString))
|
||||
continue
|
||||
}
|
||||
k := splitString[0]
|
||||
v := splitString[1]
|
||||
log.Debugf(i18n.G("for %s read env %s with value: %s from docker service", stackName, k, v))
|
||||
envMap[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return envMap, nil
|
||||
}
|
||||
|
||||
// getLatestUpgrade returns the latest available version for an app respecting
|
||||
// the "--major" flag if it is newer than the currently deployed version.
|
||||
func getLatestUpgrade(cl *dockerclient.Client, stackName string, recipeName string) (string, error) {
|
||||
deployedVersion, err := getDeployedVersion(cl, stackName, recipeName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
availableUpgrades, err := getAvailableUpgrades(cl, stackName, recipeName, deployedVersion)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(availableUpgrades) == 0 {
|
||||
log.Debugf(i18n.G("no available upgrades for %s", stackName))
|
||||
return "", nil
|
||||
}
|
||||
|
||||
var chosenUpgrade string
|
||||
if len(availableUpgrades) > 0 {
|
||||
chosenUpgrade = availableUpgrades[len(availableUpgrades)-1]
|
||||
log.Info(i18n.G("%s (%s) can be upgraded from version %s to %s", stackName, recipeName, deployedVersion, chosenUpgrade))
|
||||
}
|
||||
|
||||
return chosenUpgrade, nil
|
||||
}
|
||||
|
||||
// getDeployedVersion returns the currently deployed version of an app.
|
||||
func getDeployedVersion(cl *dockerclient.Client, stackName string, recipeName string) (string, error) {
|
||||
log.Debug(i18n.G("retrieve deployed version whether %s is already deployed", stackName))
|
||||
|
||||
deployMeta, err := stack.IsDeployed(context.Background(), cl, stackName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if !deployMeta.IsDeployed {
|
||||
return "", errors.New(i18n.G("%s is not deployed?", stackName))
|
||||
}
|
||||
|
||||
if deployMeta.Version == "unknown" {
|
||||
return "", errors.New(i18n.G("failed to determine deployed version of %s", stackName))
|
||||
}
|
||||
|
||||
return deployMeta.Version, nil
|
||||
}
|
||||
|
||||
// getAvailableUpgrades returns all available versions of an app that are newer
|
||||
// than the deployed version. It only includes major upgrades if the "--major"
|
||||
// flag is set.
|
||||
func getAvailableUpgrades(cl *dockerclient.Client, stackName string, recipeName string,
|
||||
deployedVersion string) ([]string, error) {
|
||||
catl, err := recipe.ReadRecipeCatalogue(internal.Offline)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
versions, err := recipe.GetRecipeCatalogueVersions(recipeName, catl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(versions) == 0 {
|
||||
log.Warn(i18n.G("no published releases for %s in the recipe catalogue?", recipeName))
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var availableUpgrades []string
|
||||
for _, version := range versions {
|
||||
parsedDeployedVersion, err := tagcmp.Parse(deployedVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parsedVersion, err := tagcmp.Parse(version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
versionDelta, err := parsedDeployedVersion.UpgradeDelta(parsedVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if 0 < versionDelta.UpgradeType() && (versionDelta.UpgradeType() < 4 || includeMajorUpdates) {
|
||||
availableUpgrades = append(availableUpgrades, version)
|
||||
}
|
||||
}
|
||||
|
||||
log.Debug(i18n.G("available updates for %s: %s", stackName, availableUpgrades))
|
||||
|
||||
return availableUpgrades, nil
|
||||
}
|
||||
|
||||
// processRecipeRepoVersion clones, pulls, checks out the version and lints the
|
||||
// recipe repository.
|
||||
func processRecipeRepoVersion(r recipe.Recipe, version string) error {
|
||||
if err := r.EnsureExists(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := r.EnsureUpToDate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := r.EnsureVersion(version); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := lint.LintForErrors(r); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createDeployConfig merges and enriches the compose config for the deployment.
|
||||
func createDeployConfig(r recipe.Recipe, stackName string, env envfile.AppEnv) (*composetypes.Config, stack.Deploy, error) {
|
||||
env["STACK_NAME"] = stackName
|
||||
|
||||
deployOpts := stack.Deploy{
|
||||
Namespace: stackName,
|
||||
Prune: false,
|
||||
ResolveImage: stack.ResolveImageAlways,
|
||||
Detach: false,
|
||||
}
|
||||
|
||||
composeFiles, err := r.GetComposeFiles(env)
|
||||
if err != nil {
|
||||
return nil, deployOpts, err
|
||||
}
|
||||
|
||||
deployOpts.Composefiles = composeFiles
|
||||
compose, err := appPkg.GetAppComposeConfig(stackName, deployOpts, env)
|
||||
if err != nil {
|
||||
return nil, deployOpts, err
|
||||
}
|
||||
|
||||
appPkg.ExposeAllEnv(stackName, compose, env)
|
||||
|
||||
// after the upgrade the deployment won't be in chaos state anymore
|
||||
appPkg.SetChaosLabel(compose, stackName, false)
|
||||
appPkg.SetRecipeLabel(compose, stackName, r.Name)
|
||||
appPkg.SetUpdateLabel(compose, stackName, env)
|
||||
|
||||
return compose, deployOpts, nil
|
||||
}
|
||||
|
||||
// tryUpgrade performs the upgrade if all the requirements are fulfilled.
|
||||
func tryUpgrade(cl *dockerclient.Client, stackName, recipeName string) error {
|
||||
if recipeName == "" {
|
||||
log.Debug(i18n.G("don't update %s due to missing recipe name", stackName))
|
||||
return nil
|
||||
}
|
||||
|
||||
chaos, err := getBoolLabel(cl, stackName, "chaos")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if chaos && !internal.Chaos {
|
||||
log.Debug(i18n.G("don't update %s due to chaos deployment", stackName))
|
||||
return nil
|
||||
}
|
||||
|
||||
updatesEnabled, err := getBoolLabel(cl, stackName, "autoupdate")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !updatesEnabled {
|
||||
log.Debug(i18n.G("don't update %s due to disabled auto updates or missing ENABLE_AUTO_UPDATE env", stackName))
|
||||
return nil
|
||||
}
|
||||
|
||||
upgradeVersion, err := getLatestUpgrade(cl, stackName, recipeName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if upgradeVersion == "" {
|
||||
log.Debug(i18n.G("don't update %s due to no new version", stackName))
|
||||
return nil
|
||||
}
|
||||
|
||||
err = upgrade(cl, stackName, recipeName, upgradeVersion)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// upgrade performs all necessary steps to upgrade an app.
|
||||
func upgrade(cl *dockerclient.Client, stackName, recipeName, upgradeVersion string) error {
|
||||
env, err := getEnv(cl, stackName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
app := appPkg.App{
|
||||
Name: stackName,
|
||||
Recipe: recipe.Get(recipeName),
|
||||
Server: SERVER,
|
||||
Env: env,
|
||||
}
|
||||
|
||||
r := recipe.Get(recipeName)
|
||||
|
||||
if err = processRecipeRepoVersion(r, upgradeVersion); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = deploy.MergeAbraShEnv(app.Recipe, app.Env); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
compose, deployOpts, err := createDeployConfig(r, stackName, app.Env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info(i18n.G("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(
|
||||
cl,
|
||||
deployOpts,
|
||||
compose,
|
||||
stackName,
|
||||
app.Server,
|
||||
true,
|
||||
f,
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func newKadabraApp(version, commit string) *cobra.Command {
|
||||
rootCmd := &cobra.Command{
|
||||
// translators: `kadabra` binary name
|
||||
Use: i18n.G("kadabra [cmd] [flags]"),
|
||||
Version: fmt.Sprintf("%s-%s", version, commit[:7]),
|
||||
// translators: Short description for `kababra` binary
|
||||
Short: i18n.G("The Co-op Cloud auto-updater 🤖 🚀"),
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
log.Logger.SetStyles(charmLog.DefaultStyles())
|
||||
charmLog.SetDefault(log.Logger)
|
||||
|
||||
if internal.Debug {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
log.SetOutput(os.Stderr)
|
||||
log.SetReportCaller(true)
|
||||
}
|
||||
|
||||
log.Debug(i18n.G("kadabra version %s, commit %s", version, commit))
|
||||
},
|
||||
}
|
||||
|
||||
rootCmd.PersistentFlags().BoolVarP(
|
||||
&internal.Debug,
|
||||
i18n.G("debug"),
|
||||
i18n.G("d"),
|
||||
false,
|
||||
i18n.G("show debug messages"),
|
||||
)
|
||||
|
||||
rootCmd.PersistentFlags().BoolVarP(
|
||||
&internal.NoInput,
|
||||
i18n.G("no-input"),
|
||||
i18n.G("n"),
|
||||
false,
|
||||
i18n.G("toggle non-interactive mode"),
|
||||
)
|
||||
|
||||
rootCmd.AddCommand(
|
||||
NotifyCommand,
|
||||
UpgradeCommand,
|
||||
)
|
||||
|
||||
return rootCmd
|
||||
}
|
||||
|
||||
// RunApp runs CLI abra app.
|
||||
func RunApp(version, commit string) {
|
||||
app := newKadabraApp(version, commit)
|
||||
|
||||
if err := app.Execute(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
includeMajorUpdates bool
|
||||
updateAll bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
NotifyCommand.Flags().BoolVarP(
|
||||
&includeMajorUpdates,
|
||||
"major",
|
||||
"m",
|
||||
false,
|
||||
"check for major updates",
|
||||
)
|
||||
|
||||
UpgradeCommand.Flags().BoolVarP(
|
||||
&internal.Chaos,
|
||||
i18n.G("chaos"),
|
||||
i18n.G("C"),
|
||||
false,
|
||||
i18n.G("ignore uncommitted recipes changes"),
|
||||
)
|
||||
|
||||
UpgradeCommand.Flags().BoolVarP(
|
||||
&includeMajorUpdates,
|
||||
i18n.G("major"),
|
||||
i18n.G("m"),
|
||||
false,
|
||||
i18n.G("check for major updates"),
|
||||
)
|
||||
|
||||
UpgradeCommand.Flags().BoolVarP(
|
||||
&updateAll,
|
||||
i18n.G("all"),
|
||||
i18n.G("a"),
|
||||
false,
|
||||
i18n.G("update all deployed apps"),
|
||||
)
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
// Package main provides the command-line entrypoint.
|
||||
package main
|
||||
|
||||
import (
|
||||
"coopcloud.tech/abra/cli/updater"
|
||||
)
|
||||
|
||||
// Version is the current version of Kadabra.
|
||||
var Version string
|
||||
|
||||
// Commit is the current git commit of Kadabra.
|
||||
var Commit string
|
||||
|
||||
func main() {
|
||||
if Version == "" {
|
||||
Version = "dev"
|
||||
}
|
||||
if Commit == "" {
|
||||
Commit = " "
|
||||
}
|
||||
|
||||
updater.RunApp(Version, Commit)
|
||||
}
|
||||
85
go.mod
85
go.mod
@ -8,12 +8,12 @@ require (
|
||||
coopcloud.tech/tagcmp v0.0.0-20250818180036-0ec1b205b5ca
|
||||
git.coopcloud.tech/toolshed/godotenv v1.5.2-0.20250103171850-4d0ca41daa5c
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7
|
||||
github.com/charmbracelet/bubbletea v1.3.6
|
||||
github.com/charmbracelet/bubbletea v1.3.10
|
||||
github.com/charmbracelet/lipgloss v1.1.0
|
||||
github.com/charmbracelet/log v0.4.2
|
||||
github.com/distribution/reference v0.6.0
|
||||
github.com/docker/cli v28.3.3+incompatible
|
||||
github.com/docker/docker v28.3.3+incompatible
|
||||
github.com/docker/cli v28.4.0+incompatible
|
||||
github.com/docker/docker v28.5.2+incompatible
|
||||
github.com/docker/go-units v0.5.0
|
||||
github.com/go-git/go-git/v5 v5.16.2
|
||||
github.com/google/go-cmp v0.7.0
|
||||
@ -22,7 +22,7 @@ require (
|
||||
github.com/moby/term v0.5.2
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/schollz/progressbar/v3 v3.18.0
|
||||
golang.org/x/term v0.34.0
|
||||
golang.org/x/term v0.35.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gotest.tools/v3 v3.5.2
|
||||
)
|
||||
@ -39,16 +39,18 @@ require (
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.3.2 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.10.1 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.10.2 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.2.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/platforms v0.2.1 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.5.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/docker/go-connections v0.6.0 // indirect
|
||||
@ -64,21 +66,21 @@ require (
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // 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/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/kevinburke/ssh_config v1.4.0 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // 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.19 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/miekg/pkcs11 v1.1.1 // indirect
|
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
|
||||
@ -95,42 +97,43 @@ require (
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/runc v1.1.13 // indirect
|
||||
github.com/opencontainers/runtime-spec v1.1.0 // indirect
|
||||
github.com/pjbgf/sha1cd v0.4.0 // indirect
|
||||
github.com/pjbgf/sha1cd v0.5.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.65.0 // indirect
|
||||
github.com/prometheus/common v0.66.1 // indirect
|
||||
github.com/prometheus/procfs v0.17.0 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/skeema/knownhosts v1.3.1 // indirect
|
||||
github.com/spf13/pflag v1.0.7 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // 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/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect
|
||||
go.opentelemetry.io/otel v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
|
||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.37.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
|
||||
golang.org/x/crypto v0.41.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6 // indirect
|
||||
golang.org/x/net v0.43.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250811230008-5f3141c8851a // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a // indirect
|
||||
google.golang.org/grpc v1.74.2 // indirect
|
||||
google.golang.org/protobuf v1.36.7 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.8.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.42.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250911091902-df9299821621 // indirect
|
||||
golang.org/x/net v0.44.0 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
golang.org/x/time v0.13.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 // indirect
|
||||
google.golang.org/grpc v1.75.1 // indirect
|
||||
google.golang.org/protobuf v1.36.9 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
@ -147,11 +150,15 @@ require (
|
||||
github.com/moby/patternmatcher v0.6.0 // indirect
|
||||
github.com/moby/sys/sequential v0.6.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/prometheus/client_golang v1.23.0 // indirect
|
||||
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||
github.com/sergi/go-diff v1.4.0 // indirect
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/spf13/cobra v1.10.1
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/theupdateframework/notary v0.7.0 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
golang.org/x/sys v0.35.0
|
||||
golang.org/x/sys v0.36.0
|
||||
)
|
||||
|
||||
replace github.com/docker/cli v28.4.0+incompatible => git.coopcloud.tech/toolshed/docker-cli v28.5.3-0.20260202112816-30df2d0b3a00+incompatible
|
||||
|
||||
replace github.com/spf13/cobra => github.com/decentral1se/cobra v1.10.2-i18n
|
||||
|
||||
197
go.sum
197
go.sum
@ -27,6 +27,8 @@ coopcloud.tech/tagcmp v0.0.0-20250818180036-0ec1b205b5ca/go.mod h1:ESVm0wQKcbcFi
|
||||
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=
|
||||
git.coopcloud.tech/toolshed/docker-cli v28.5.3-0.20260202112816-30df2d0b3a00+incompatible h1:YdW2uK5sHj545lGz/FrozPueINkQ7fUjlsNd8aYcqik=
|
||||
git.coopcloud.tech/toolshed/docker-cli v28.5.3-0.20260202112816-30df2d0b3a00+incompatible/go.mod h1:PY19bHY5R4DLmRuCrv4TR7etURn/+tSTFuam4FUTiD8=
|
||||
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=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
|
||||
@ -95,7 +97,6 @@ github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:C
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
|
||||
@ -133,20 +134,20 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU=
|
||||
github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc=
|
||||
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
||||
github.com/charmbracelet/colorprofile v0.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqITmrvb1uTIiI=
|
||||
github.com/charmbracelet/colorprofile v0.3.2/go.mod h1:mTD5XzNeWHj8oqHb+S1bssQb7vIHbepiebQ2kPKVKbI=
|
||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||
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.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
|
||||
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
|
||||
github.com/charmbracelet/x/ansi v0.10.2 h1:ith2ArZS0CJG30cIUfID1LXN7ZFXRCww6RUvAPA+Pzw=
|
||||
github.com/charmbracelet/x/ansi v0.10.2/go.mod h1:HbLdJjQH4UH4AqA2HpRWuWNluRE6zxJH/yteYEYCFa8=
|
||||
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/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
||||
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||
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=
|
||||
@ -164,6 +165,8 @@ github.com/cilium/ebpf v0.4.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJ
|
||||
github.com/cilium/ebpf v0.6.2/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs=
|
||||
github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY=
|
||||
github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||
github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA=
|
||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
@ -269,8 +272,6 @@ github.com/containers/ocicrypt v1.1.0/go.mod h1:b8AOe0YR67uU8OqfVNcznfFpAzu3rdgU
|
||||
github.com/containers/ocicrypt v1.1.1/go.mod h1:Dm55fwWm1YZAjYRaJ94z2mfZikIyIN4B0oB3dj3jFxY=
|
||||
github.com/containers/storage v1.38.2 h1:8bAIxnVBGKzMw5EWCivVj24bztQT6IkDp4uHiyhnzwE=
|
||||
github.com/containers/storage v1.38.2/go.mod h1:INP0RPLHWBxx+pTsO5uiHlDUGHDFvWZPWprAbAlQWPQ=
|
||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU=
|
||||
github.com/coreos/go-iptables v0.5.0/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU=
|
||||
github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc=
|
||||
@ -296,8 +297,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/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.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
|
||||
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
||||
github.com/cyphar/filepath-securejoin v0.5.0 h1:hIAhkRBMQ8nIeuVwcAoymp7MY4oherZdAxD+m0u9zaw=
|
||||
github.com/cyphar/filepath-securejoin v0.5.0/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
||||
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/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8=
|
||||
@ -305,27 +306,26 @@ github.com/d2g/hardwareaddr v0.0.0-20190221164911-e7d9fbe030e4/go.mod h1:bMl4RjI
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/decentral1se/cobra v1.10.2-i18n h1:XR+6AHHfnf4k5NM9f09oLMrEVwz3rkQIAIcqgL8R08g=
|
||||
github.com/decentral1se/cobra v1.10.2-i18n/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/decentral1se/passgen v1.0.1 h1:j2AxK/kHKxDHWZZfkJj8Wgae9+O+DYEqR5sjKthIYKA=
|
||||
github.com/decentral1se/passgen v1.0.1/go.mod h1:530V+lNoPhKtkrX2fIVsIfLhkl47CuiOM7HRgi7C+SU=
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
|
||||
github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0=
|
||||
github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
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/docker/cli v0.0.0-20191017083524-a8ff7f821017/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 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.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/docker v1.4.2-0.20190924003213-a8608b5b67c7/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 v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
|
||||
github.com/docker/docker v28.5.2+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.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
|
||||
github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=
|
||||
@ -439,12 +439,10 @@ github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zV
|
||||
github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||
github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
@ -520,17 +518,14 @@ github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
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.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/grpc-ecosystem/grpc-gateway/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/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
|
||||
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 v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
@ -545,7 +540,6 @@ github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVU
|
||||
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.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog=
|
||||
github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
@ -579,8 +573,8 @@ github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8/go.mod h1:vgyd7OREkbtVE
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
|
||||
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||
github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ=
|
||||
github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
@ -590,6 +584,8 @@ github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdY
|
||||
github.com/klauspost/compress v1.14.2/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
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.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
@ -611,10 +607,9 @@ github.com/leonelquinteros/gotext v1.7.2 h1:bDPndU8nt+/kRo1m4l/1OXiiy2v7Z7dfPQ9+
|
||||
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/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/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/magiconair/properties v1.5.3/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
@ -631,8 +626,8 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
|
||||
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-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
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.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
||||
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
|
||||
github.com/mattn/go-shellwords v1.0.6/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
|
||||
github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
|
||||
@ -650,7 +645,6 @@ github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WT
|
||||
github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4=
|
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
|
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v0.0.0-20150613213606-2caf8efc9366/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A=
|
||||
@ -702,7 +696,6 @@ github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+
|
||||
github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
|
||||
github.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
@ -755,11 +748,10 @@ github.com/opencontainers/selinux v1.8.0/go.mod h1:RScLhm78qiWa2gbVCcGkC7tCGdgk3
|
||||
github.com/opencontainers/selinux v1.8.2/go.mod h1:MUIHuUEvKB1wtJjQdOyYRgOnLD2xAPP8dBsCoU0KuF8=
|
||||
github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI=
|
||||
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
|
||||
github.com/pjbgf/sha1cd v0.4.0 h1:NXzbL1RvjTUi6kgYZCX3fPwwl27Q1LJndxtUDVfJGRY=
|
||||
github.com/pjbgf/sha1cd v0.4.0/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
|
||||
github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
|
||||
github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
|
||||
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/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@ -771,12 +763,11 @@ github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prY
|
||||
github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.0-pre1.0.20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
|
||||
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.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||
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_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
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-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
@ -785,16 +776,13 @@ github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6T
|
||||
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-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
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.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.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
|
||||
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
|
||||
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
|
||||
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
||||
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-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.0.0-20190522114515-bc1a522cf7b1/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
|
||||
@ -805,8 +793,6 @@ 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.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/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/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
@ -843,29 +829,19 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1
|
||||
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/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||
github.com/spf13/cast v0.0.0-20150508191742-4d07383ffe94/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.1/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/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.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
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 v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.0/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.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
|
||||
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v0.0.0-20150530192845-be5ff3e4840c/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
|
||||
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
|
||||
github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8=
|
||||
github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
@ -878,8 +854,8 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
|
||||
github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
|
||||
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI=
|
||||
@ -890,7 +866,6 @@ github.com/theupdateframework/notary v0.7.0 h1:QyagRZ7wlSpjT5N2qQAh/pN+DVqgekv4D
|
||||
github.com/theupdateframework/notary v0.7.0/go.mod h1:c9DRxcmhHmVLDay4/2fUYdISnHqbFDGRSlXPO0AhYWw=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||
github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
@ -919,7 +894,6 @@ github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQ
|
||||
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/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.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
@ -927,7 +901,6 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t
|
||||
github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs=
|
||||
github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA=
|
||||
github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg=
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
|
||||
@ -937,37 +910,41 @@ go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/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/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY=
|
||||
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||
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.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.37.0 h1:EtFWSnwW9hGObjkIdmlnWSydO+Qs8OwzfzXLUPg4xOc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h1:QjUEoiGCPkvFZ/MjK6ZZfNOS6mfVEVKYE99dFhuN2LI=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 h1:lwI4Dc5leUqENgGuQImwLo4WnuXFPetmPpkLi2IrX54=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0/go.mod h1:Kz/oCE7z5wuyhPxsXDuaPteSWqjSBD5YaSdbxZYGbGk=
|
||||
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/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
||||
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
|
||||
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.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||
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.opentelemetry.io/proto/otlp v1.8.0 h1:fRAZQDcAFHySxpJ1TwlA1cJ4tvcrw7nXl9xWWC8N5CE=
|
||||
go.opentelemetry.io/proto/otlp v1.8.0/go.mod h1:tIeYOeNBU4cvmPqpaji1P+KbB4Oloai8wN4rWzRrFF0=
|
||||
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/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
@ -986,8 +963,8 @@ golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWP
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/crypto v0.0.0-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.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||
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-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
@ -998,8 +975,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-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-20250813145105-42675adae3e6 h1:SbTAbRFnd5kjQXbczszQ0hdk3ctwYf3qBNH9jIsGclE=
|
||||
golang.org/x/exp v0.0.0-20250813145105-42675adae3e6/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
|
||||
golang.org/x/exp v0.0.0-20250911091902-df9299821621 h1:2id6c1/gto0kaHYyrixvknJ8tUK/Qs5IsmBtrc+FtgU=
|
||||
golang.org/x/exp v0.0.0-20250911091902-df9299821621/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk=
|
||||
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/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
@ -1063,8 +1040,8 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
|
||||
golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-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.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
|
||||
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
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-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@ -1082,8 +1059,6 @@ 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-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.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-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@ -1162,13 +1137,13 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
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-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
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/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
|
||||
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
|
||||
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.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@ -1178,16 +1153,16 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.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.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
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-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-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.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI=
|
||||
golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
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-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@ -1237,6 +1212,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
|
||||
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-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
@ -1281,10 +1258,10 @@ google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfG
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-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/googleapis/api v0.0.0-20250811230008-5f3141c8851a h1:DMCgtIAIQGZqJXMVzJF4MV8BlWoJh2ZuFiRdAleyr58=
|
||||
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-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/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 h1:8XJ4pajGwOlasW+L13MnEGA8W4115jJySQtVfS2/IBU=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4/go.mod h1:NnuHhy+bxcg30o7FnVAZbXsPHUDQ9qKWAQKCD7VxFtk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 h1:i8QOKZfYg6AbGVZzUAY3LrNWCKF8O6zFisU9Wl9RER4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ=
|
||||
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.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
@ -1304,8 +1281,8 @@ google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTp
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.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.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
|
||||
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
|
||||
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
|
||||
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
||||
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-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
@ -1319,8 +1296,8 @@ 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/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
|
||||
google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
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/cenkalti/backoff.v2 v2.2.1/go.mod h1:S0QdOvT2AlerfSBkp0O+dk+bbIMaNbEmVk876gPCthU=
|
||||
|
||||
@ -471,13 +471,6 @@ func GetAppStatuses(apps []App, MachineReadable bool) (map[string]map[string]str
|
||||
result["chaosVersion"] = chaosVersion
|
||||
}
|
||||
|
||||
labelKey = fmt.Sprintf("coop-cloud.%s.autoupdate", name)
|
||||
if autoUpdate, ok := service.Spec.Labels[labelKey]; ok {
|
||||
result["autoUpdate"] = autoUpdate
|
||||
} else {
|
||||
result["autoUpdate"] = "false"
|
||||
}
|
||||
|
||||
labelKey = fmt.Sprintf("coop-cloud.%s.version", name)
|
||||
if version, ok := service.Spec.Labels[labelKey]; ok {
|
||||
result["version"] = version
|
||||
@ -509,7 +502,11 @@ func GetAppComposeConfig(recipe string, opts stack.Deploy, appEnv envfile.AppEnv
|
||||
}
|
||||
|
||||
// ExposeAllEnv exposes all env variables to the app container
|
||||
func ExposeAllEnv(stackName string, compose *composetypes.Config, appEnv envfile.AppEnv) {
|
||||
func ExposeAllEnv(
|
||||
stackName string,
|
||||
compose *composetypes.Config,
|
||||
appEnv envfile.AppEnv,
|
||||
toDeployVersion string) {
|
||||
for _, service := range compose.Services {
|
||||
if service.Name == "app" {
|
||||
log.Debug(i18n.G("adding env vars to %s service config", stackName))
|
||||
@ -517,6 +514,11 @@ func ExposeAllEnv(stackName string, compose *composetypes.Config, appEnv envfile
|
||||
_, exists := service.Environment[k]
|
||||
if !exists {
|
||||
value := v
|
||||
if k == "TYPE" || k == "RECIPE" {
|
||||
// NOTE(d1): don't use the wrong version from the app env
|
||||
// since we are deploying a new version
|
||||
value = toDeployVersion
|
||||
}
|
||||
service.Environment[k] = &value
|
||||
log.Debug(i18n.G("%s: %s: %s", stackName, k, value))
|
||||
}
|
||||
@ -631,6 +633,11 @@ func (a App) WipeRecipeVersion() error {
|
||||
|
||||
// WriteRecipeVersion writes the recipe version to the app .env file.
|
||||
func (a App) WriteRecipeVersion(version string, dryRun bool) error {
|
||||
if version == config.UNKNOWN_DEFAULT {
|
||||
log.Debug(i18n.G("version is unknown, skipping env write"))
|
||||
return nil
|
||||
}
|
||||
|
||||
file, err := os.Open(a.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@ -224,3 +224,16 @@ func TestWriteRecipeVersionOverwrite(t *testing.T) {
|
||||
|
||||
assert.Equal(t, "foo", app.Recipe.EnvVersion)
|
||||
}
|
||||
|
||||
func TestWriteRecipeVersionUnknown(t *testing.T) {
|
||||
app, err := appPkg.GetApp(testPkg.ExpectedAppFiles, testPkg.AppName)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := app.WriteRecipeVersion(config.UNKNOWN_DEFAULT, false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.NotEqual(t, config.UNKNOWN_DEFAULT, app.Recipe.EnvVersion)
|
||||
}
|
||||
|
||||
@ -5,7 +5,6 @@ import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"coopcloud.tech/abra/pkg/envfile"
|
||||
"coopcloud.tech/abra/pkg/i18n"
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
composetypes "github.com/docker/cli/cli/compose/types"
|
||||
@ -56,23 +55,6 @@ func SetVersionLabel(compose *composetypes.Config, stackName string, version str
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
// the auto update process.
|
||||
func SetUpdateLabel(compose *composetypes.Config, stackName string, appEnv envfile.AppEnv) {
|
||||
for _, service := range compose.Services {
|
||||
if service.Name == "app" {
|
||||
enable_auto_update, exists := appEnv["ENABLE_AUTO_UPDATE"]
|
||||
if !exists {
|
||||
enable_auto_update = "false"
|
||||
}
|
||||
log.Debug(i18n.G("set label 'coop-cloud.%s.autoupdate' to %s for %s", stackName, enable_auto_update, stackName))
|
||||
labelKey := fmt.Sprintf("coop-cloud.%s.autoupdate", stackName)
|
||||
service.Deploy.Labels[labelKey] = enable_auto_update
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetLabel reads docker labels in the format of "coop-cloud.${STACK_NAME}.${LABEL}" from the local compose files
|
||||
func GetLabel(compose *composetypes.Config, stackName string, label string) string {
|
||||
for _, service := range compose.Services {
|
||||
|
||||
@ -6,9 +6,10 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
contextPkg "coopcloud.tech/abra/pkg/context"
|
||||
"coopcloud.tech/abra/pkg/i18n"
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
@ -35,13 +36,27 @@ func WithTimeout(timeout int) Opt {
|
||||
// New initiates a new Docker client. New client connections are validated so
|
||||
// that we ensure connections via SSH to the daemon can succeed. It takes into
|
||||
// account that you may only want the local client and not communicate via SSH.
|
||||
// For this use-case, please pass "default" as the contextName.
|
||||
// For this use-case, please pass "default" as the serverName.
|
||||
func New(serverName string, opts ...Opt) (*client.Client, error) {
|
||||
var clientOpts []client.Opt
|
||||
|
||||
ctx, err := GetContext(serverName)
|
||||
if err != nil {
|
||||
return nil, errors.New(i18n.G("unknown server, run \"abra server add %s\"?", serverName))
|
||||
serverDir := path.Join(config.SERVERS_DIR, serverName)
|
||||
if _, err := os.Stat(serverDir); err != nil {
|
||||
return nil, errors.New(i18n.G("server missing, run \"abra server add %s\"?", serverName))
|
||||
}
|
||||
|
||||
// NOTE(p4u1): when the docker context does not exist but the server folder
|
||||
// is there, let's create a new docker context.
|
||||
if err = CreateContext(serverName); err != nil {
|
||||
return nil, errors.New(i18n.G("server missing context, context creation failed: %s", err))
|
||||
}
|
||||
|
||||
ctx, err = GetContext(serverName)
|
||||
if err != nil {
|
||||
return nil, errors.New(i18n.G("server missing context, run \"abra server add %s\"?", serverName))
|
||||
}
|
||||
}
|
||||
|
||||
ctxEndpoint, err := contextPkg.GetContextEndpoint(ctx)
|
||||
@ -68,8 +83,7 @@ func New(serverName string, opts ...Opt) (*client.Client, error) {
|
||||
|
||||
httpClient := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: helper.Dialer,
|
||||
IdleConnTimeout: 30 * time.Second,
|
||||
DialContext: helper.Dialer,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -116,10 +116,7 @@ var (
|
||||
|
||||
DIRTY_DEFAULT = "+U"
|
||||
|
||||
NO_DOMAIN_DEFAULT = "N/A"
|
||||
NO_VERSION_DEFAULT = "N/A"
|
||||
NO_SECRETS_DEFAULT = "N/A"
|
||||
NO_VOLUMES_DEFAULT = "N/A"
|
||||
MISSING_DEFAULT = "-"
|
||||
|
||||
UNKNOWN_DEFAULT = "unknown"
|
||||
)
|
||||
|
||||
@ -165,7 +165,13 @@ func GetImagesForStack(cl *dockerClient.Client, app appPkg.App) (map[string]stri
|
||||
}
|
||||
|
||||
imageBaseName := reference.Path(imageParsed)
|
||||
imageTag := imageParsed.(reference.NamedTagged).Tag()
|
||||
namedTag, ok := imageParsed.(reference.NamedTagged)
|
||||
if !ok {
|
||||
// This is an image without a tag
|
||||
images[imageBaseName] = ""
|
||||
continue
|
||||
}
|
||||
imageTag := namedTag.Tag()
|
||||
|
||||
existingImageVersion, ok := images[imageBaseName]
|
||||
if !ok {
|
||||
@ -282,7 +288,13 @@ func GatherImagesForDeploy(cl *dockerClient.Client, app appPkg.App, compose *com
|
||||
}
|
||||
|
||||
imageBaseName := reference.Path(imageParsed)
|
||||
imageTag := imageParsed.(reference.NamedTagged).Tag()
|
||||
namedTag, ok := imageParsed.(reference.NamedTagged)
|
||||
if !ok {
|
||||
// This is an image without a tag
|
||||
newImages[imageBaseName] = ""
|
||||
continue
|
||||
}
|
||||
imageTag := namedTag.Tag()
|
||||
|
||||
existingImageVersion, ok := newImages[imageBaseName]
|
||||
if !ok {
|
||||
|
||||
@ -35,7 +35,7 @@ func Commit(repoPath, commitMessage string, dryRun bool) error {
|
||||
|
||||
if !dryRun {
|
||||
// NOTE(d1): `All: true` does not include untracked files
|
||||
_, err = commitWorktree.Commit(commitMessage, &git.CommitOptions{All: true})
|
||||
_, err := commitWorktree.Commit(commitMessage, &git.CommitOptions{All: true})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -20,8 +20,17 @@ var (
|
||||
Locale = DefaultLocale
|
||||
_, Mo = LoadLocale()
|
||||
G = Mo.Get
|
||||
GC = Mo.GetC
|
||||
)
|
||||
|
||||
func GetLocaleStr() string {
|
||||
locale := os.Getenv("LANG")
|
||||
if lastUnderscore := strings.LastIndex(locale, "_"); lastUnderscore != -1 {
|
||||
locale = locale[0:lastUnderscore]
|
||||
}
|
||||
return locale
|
||||
}
|
||||
|
||||
func LoadLocale() (string, *gotext.Mo) {
|
||||
entries, err := assetFS.ReadDir("locales")
|
||||
if err != nil {
|
||||
@ -37,11 +46,7 @@ func LoadLocale() (string, *gotext.Mo) {
|
||||
}
|
||||
}
|
||||
|
||||
locale := os.Getenv("LANG")
|
||||
if lastUnderscore := strings.LastIndex(locale, "_"); lastUnderscore != -1 {
|
||||
locale = locale[0:lastUnderscore]
|
||||
}
|
||||
|
||||
locale := GetLocaleStr()
|
||||
if locale != "" {
|
||||
if slices.Contains(linguas, locale) {
|
||||
Locale = locale
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -142,7 +142,7 @@ var LintRules = map[string][]LintRule{
|
||||
Function: LintAppService,
|
||||
},
|
||||
{
|
||||
Ref: "R015",
|
||||
Ref: "R016",
|
||||
Level: i18n.G("error"),
|
||||
Description: i18n.G("deploy labels stanza present"),
|
||||
HowToResolve: i18n.G("include \"deploy: labels: ...\" stanza"),
|
||||
@ -258,7 +258,7 @@ func LintAppService(recipe recipe.Recipe) (bool, error) {
|
||||
func LintTraefikEnabledSkipCondition(r recipe.Recipe) (bool, error) {
|
||||
sampleEnv, err := r.SampleEnv()
|
||||
if err != nil {
|
||||
return false, errors.New(i18n.G("unable to discover .env.sample for %s", r.Name))
|
||||
return false, errors.New(i18n.G(".env.sample for %s couldn't be read: %s", r.Name, err))
|
||||
}
|
||||
|
||||
if _, ok := sampleEnv["DOMAIN"]; !ok {
|
||||
|
||||
@ -15,7 +15,7 @@ import (
|
||||
func (r Recipe) SampleEnv() (map[string]string, error) {
|
||||
sampleEnv, err := envfile.ReadEnv(r.SampleEnvPath)
|
||||
if err != nil {
|
||||
return sampleEnv, errors.New(i18n.G("unable to discover .env.sample for %s", r.Name))
|
||||
return sampleEnv, errors.New(i18n.G(".env.sample for %s couldn't be read: %s", r.Name, err))
|
||||
}
|
||||
return sampleEnv, nil
|
||||
}
|
||||
|
||||
@ -32,6 +32,12 @@ func (r Recipe) Ensure(ctx EnsureContext) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// NOTE(d1): if we cannot parse the .env.sample then there is a
|
||||
// fundamental problem which requires solving right now
|
||||
if _, err := r.SampleEnv(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ctx.Chaos {
|
||||
return nil
|
||||
}
|
||||
@ -49,7 +55,7 @@ func (r Recipe) Ensure(ctx EnsureContext) error {
|
||||
if r.EnvVersion != "" && !ctx.IgnoreEnvVersion {
|
||||
log.Debug(i18n.G("ensuring env version %s", r.EnvVersion))
|
||||
if strings.Contains(r.EnvVersion, "+U") {
|
||||
return errors.New(i18n.G("can not redeploy chaos version (%s) without --chaos", r.EnvVersion))
|
||||
return errors.New(i18n.G(`cannot redeploy previous chaos version (%s), did you mean to use "--chaos"?`))
|
||||
}
|
||||
|
||||
if _, err := r.EnsureVersion(r.EnvVersion); err != nil {
|
||||
@ -403,15 +409,18 @@ func (r Recipe) GetRecipeVersions() (RecipeVersions, []string, error) {
|
||||
Branch: plumbing.ReferenceName(ref.Name()),
|
||||
}
|
||||
if err := worktree.Checkout(checkOutOpts); err != nil {
|
||||
log.Debug(i18n.G("failed to check out %s in %s", tag, r.Dir))
|
||||
return err
|
||||
log.Debug(i18n.G("failed to check out %s in %s: %s", tag, r.Dir, err))
|
||||
warnMsg = append(warnMsg, i18n.G("skipping tag %s: checkout failed: %s", tag, err))
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Debug(i18n.G("git checkout: %s in %s", ref.Name(), r.Dir))
|
||||
|
||||
config, err := r.GetComposeConfig(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
log.Debug(i18n.G("failed to get compose config for %s: %s", tag, err))
|
||||
warnMsg = append(warnMsg, i18n.G("skipping tag %s: invalid compose config: %s", tag, err))
|
||||
return nil
|
||||
}
|
||||
|
||||
versionMeta := make(map[string]ServiceMeta)
|
||||
@ -419,7 +428,9 @@ func (r Recipe) GetRecipeVersions() (RecipeVersions, []string, error) {
|
||||
|
||||
img, err := reference.ParseNormalizedNamed(service.Image)
|
||||
if err != nil {
|
||||
return err
|
||||
log.Debug(i18n.G("failed to parse image for %s in %s: %s", service.Name, tag, err))
|
||||
warnMsg = append(warnMsg, i18n.G("skipping tag %s: invalid image reference in service %s: %s", tag, service.Name, err))
|
||||
return nil
|
||||
}
|
||||
|
||||
path := reference.Path(img)
|
||||
@ -445,6 +456,7 @@ func (r Recipe) GetRecipeVersions() (RecipeVersions, []string, error) {
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Warn(i18n.G("GetRecipeVersions encountered error for %s: %s (collected %d versions)", r.Name, err, len(versions)))
|
||||
return versions, warnMsg, nil
|
||||
}
|
||||
|
||||
|
||||
@ -379,7 +379,7 @@ func ReadRecipeCatalogue(offline bool) (RecipeCatalogue, error) {
|
||||
|
||||
if !offline {
|
||||
if err := catalogue.EnsureUpToDate(); err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("unable to update catalogue: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -5,6 +5,8 @@ package secret
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
@ -39,6 +41,14 @@ type Secret struct {
|
||||
// variable. For Example:
|
||||
// SECRET_FOO=v1 # charset=default,special
|
||||
Charset string
|
||||
// Encoding comes from the encoding modifier at the secret version environment
|
||||
// variable. For Example:
|
||||
// SECRET_FOO=v1 # encoding=base64
|
||||
Encoding string
|
||||
// Prefix comes from the prefix modifier at the secret version environment
|
||||
// variable. For Example:
|
||||
// SECRET_FOO=v1 # prefix=base64:
|
||||
Prefix string
|
||||
// Whether or not to skip generation of the secret or not
|
||||
// For example: SECRET_FOO=v1 # generate=false
|
||||
SkipGenerate bool
|
||||
@ -50,6 +60,11 @@ type Secret struct {
|
||||
// Will have this remote name:
|
||||
// test_example_com_test_pass_two_v2
|
||||
RemoteName string
|
||||
|
||||
// LocalName iis the name of the secret in the recipe config. This is also
|
||||
// the name that you pass to `abra app secret insert` and is shown on `abra
|
||||
// app secret list`
|
||||
LocalName string
|
||||
}
|
||||
|
||||
// GeneratePassword generates passwords.
|
||||
@ -82,6 +97,17 @@ func GeneratePassphrase() (string, error) {
|
||||
return passphrases[0], nil
|
||||
}
|
||||
|
||||
// generateRandomBytes generates random bytes as a string
|
||||
func generateRandomBytes(length int) (string, error) {
|
||||
randomBytes := make([]byte, length)
|
||||
if _, err := rand.Read(randomBytes); err != nil {
|
||||
return "", errors.New(i18n.G("failed to generate random bytes: %w", err))
|
||||
}
|
||||
|
||||
// Return as string for consistent handling with other secret types
|
||||
return string(randomBytes), nil
|
||||
}
|
||||
|
||||
// ReadSecretsConfig reads secret names/versions from the recipe config. The
|
||||
// function generalises appEnv/composeFiles because some times you have an app
|
||||
// and some times you don't (as the caller). We need to be able to handle the
|
||||
@ -133,7 +159,12 @@ func ReadSecretsConfig(appEnvPath string, composeFiles []string, stackName strin
|
||||
|
||||
lastIdx := strings.LastIndex(secretConfig.Name, "_")
|
||||
secretVersion := secretConfig.Name[lastIdx+1:]
|
||||
value := Secret{Version: secretVersion, RemoteName: secretConfig.Name}
|
||||
|
||||
value := Secret{
|
||||
Version: secretVersion,
|
||||
RemoteName: secretConfig.Name,
|
||||
LocalName: secretId,
|
||||
}
|
||||
|
||||
if len(value.RemoteName) > config.MAX_DOCKER_SECRET_LENGTH {
|
||||
return nil, errors.New(i18n.G("secret %s is > %d chars when combined with %s", secretId, config.MAX_DOCKER_SECRET_LENGTH, stackName))
|
||||
@ -167,6 +198,8 @@ func ReadSecretsConfig(appEnvPath string, composeFiles []string, stackName strin
|
||||
}
|
||||
|
||||
value.Charset = resolveCharset(modifierValues["charset"])
|
||||
value.Encoding = resolveEncoding(value.Charset, modifierValues["encoding"], secretId)
|
||||
value.Prefix = modifierValues["prefix"]
|
||||
break
|
||||
}
|
||||
secretValues[secretId] = value
|
||||
@ -175,9 +208,45 @@ func ReadSecretsConfig(appEnvPath string, composeFiles []string, stackName strin
|
||||
return secretValues, nil
|
||||
}
|
||||
|
||||
// encodeSecret applies encoding to the generated secret value
|
||||
func encodeSecret(value, encoding string) string {
|
||||
switch strings.ToLower(encoding) {
|
||||
case "base64":
|
||||
return base64.StdEncoding.EncodeToString([]byte(value))
|
||||
default:
|
||||
return value // No encoding applied
|
||||
}
|
||||
}
|
||||
|
||||
// applyPrefix adds a prefix to the secret value
|
||||
func applyPrefix(value, prefix string) string {
|
||||
if prefix != "" {
|
||||
return prefix + value
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// resolveEncoding validates and resolves the encoding for a given charset and secretId
|
||||
func resolveEncoding(charset, encoding, secretId string) string {
|
||||
if charset == "bytes" {
|
||||
if encoding == "" {
|
||||
return "base64"
|
||||
} else if encoding != "base64" {
|
||||
log.Warnf(i18n.G("charset=bytes only supports encoding=base64, got encoding=%s for secret %s, defaulting to base64", encoding, secretId))
|
||||
return "base64"
|
||||
}
|
||||
}
|
||||
|
||||
return encoding
|
||||
}
|
||||
|
||||
// resolveCharset sets the passgen Alphabet required for a secret
|
||||
func resolveCharset(input string) string {
|
||||
switch strings.ToLower(input) {
|
||||
case "hex":
|
||||
return passgen.AlphabetNumericAmbiguous + "abcdef"
|
||||
case "bytes":
|
||||
return "bytes"
|
||||
case "special":
|
||||
return passgen.AlphabetSpecial
|
||||
case "safespecial":
|
||||
@ -212,12 +281,23 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server
|
||||
log.Debug(i18n.G("attempting to generate and store %s on %s", secret.RemoteName, server))
|
||||
|
||||
if secret.Length > 0 {
|
||||
password, err := GeneratePassword(uint(secret.Length), secret.Charset)
|
||||
var password string
|
||||
var err error
|
||||
|
||||
if secret.Charset == "bytes" {
|
||||
password, err = generateRandomBytes(secret.Length)
|
||||
} else {
|
||||
password, err = GeneratePassword(uint(secret.Length), secret.Charset)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
ch <- err
|
||||
return
|
||||
}
|
||||
|
||||
password = encodeSecret(password, secret.Encoding)
|
||||
password = applyPrefix(password, secret.Prefix)
|
||||
|
||||
if err := client.StoreSecret(cl, secret.RemoteName, password); err != nil {
|
||||
if strings.Contains(err.Error(), "AlreadyExists") {
|
||||
log.Warnf(i18n.G("%s already exists", secret.RemoteName))
|
||||
@ -238,6 +318,9 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server
|
||||
return
|
||||
}
|
||||
|
||||
passphrase = encodeSecret(passphrase, secret.Encoding)
|
||||
passphrase = applyPrefix(passphrase, secret.Prefix)
|
||||
|
||||
if err := client.StoreSecret(cl, secret.RemoteName, passphrase); err != nil {
|
||||
if strings.Contains(err.Error(), "AlreadyExists") {
|
||||
log.Warnf(i18n.G("%s already exists", secret.RemoteName))
|
||||
|
||||
@ -18,36 +18,80 @@ func TestReadSecretsConfig(t *testing.T) {
|
||||
assert.Equal(t, "v2", secretsFromConfig["test_pass_one"].Version)
|
||||
assert.Equal(t, 0, secretsFromConfig["test_pass_one"].Length)
|
||||
assert.Equal(t, "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789", secretsFromConfig["test_pass_one"].Charset)
|
||||
assert.Equal(t, "", secretsFromConfig["test_pass_one"].Encoding)
|
||||
assert.Equal(t, "", secretsFromConfig["test_pass_one"].Prefix)
|
||||
|
||||
// Has a length modifier
|
||||
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, 10, secretsFromConfig["test_pass_two"].Length)
|
||||
assert.Equal(t, "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789", secretsFromConfig["test_pass_two"].Charset)
|
||||
assert.Equal(t, "", secretsFromConfig["test_pass_two"].Encoding)
|
||||
assert.Equal(t, "", secretsFromConfig["test_pass_two"].Prefix)
|
||||
|
||||
// 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, "v2", secretsFromConfig["test_pass_three"].Version)
|
||||
assert.Equal(t, 0, secretsFromConfig["test_pass_three"].Length)
|
||||
assert.Equal(t, "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789", secretsFromConfig["test_pass_three"].Charset)
|
||||
assert.Equal(t, "", secretsFromConfig["test_pass_three"].Encoding)
|
||||
assert.Equal(t, "", secretsFromConfig["test_pass_three"].Prefix)
|
||||
|
||||
// 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)
|
||||
assert.Equal(t, "", secretsFromConfig["test_pass_four"].Encoding)
|
||||
assert.Equal(t, "", secretsFromConfig["test_pass_four"].Prefix)
|
||||
|
||||
// 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)
|
||||
assert.Equal(t, "", secretsFromConfig["test_pass_five"].Encoding)
|
||||
assert.Equal(t, "", secretsFromConfig["test_pass_five"].Prefix)
|
||||
|
||||
// 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)
|
||||
assert.Equal(t, "", secretsFromConfig["test_pass_six"].Encoding)
|
||||
assert.Equal(t, "", secretsFromConfig["test_pass_six"].Prefix)
|
||||
|
||||
// Has a length modifier and a charset=hex modifier
|
||||
assert.Equal(t, "test_example_com_test_pass_seven_v1", secretsFromConfig["test_pass_seven"].RemoteName)
|
||||
assert.Equal(t, "v1", secretsFromConfig["test_pass_seven"].Version)
|
||||
assert.Equal(t, 32, secretsFromConfig["test_pass_seven"].Length)
|
||||
assert.Equal(t, "0123456789abcdef", secretsFromConfig["test_pass_seven"].Charset)
|
||||
assert.Equal(t, "", secretsFromConfig["test_pass_seven"].Encoding)
|
||||
assert.Equal(t, "", secretsFromConfig["test_pass_seven"].Prefix)
|
||||
|
||||
// Has a length modifier and an encoding=base64 modifier
|
||||
assert.Equal(t, "test_example_com_test_pass_eight_v1", secretsFromConfig["test_pass_eight"].RemoteName)
|
||||
assert.Equal(t, "v1", secretsFromConfig["test_pass_eight"].Version)
|
||||
assert.Equal(t, 12, secretsFromConfig["test_pass_eight"].Length)
|
||||
assert.Equal(t, "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789", secretsFromConfig["test_pass_eight"].Charset)
|
||||
assert.Equal(t, "base64", secretsFromConfig["test_pass_eight"].Encoding)
|
||||
assert.Equal(t, "", secretsFromConfig["test_pass_eight"].Prefix)
|
||||
|
||||
// Has a length modifier and a prefix=base64: modifier
|
||||
assert.Equal(t, "test_example_com_test_pass_nine_v1", secretsFromConfig["test_pass_nine"].RemoteName)
|
||||
assert.Equal(t, "v1", secretsFromConfig["test_pass_nine"].Version)
|
||||
assert.Equal(t, 16, secretsFromConfig["test_pass_nine"].Length)
|
||||
assert.Equal(t, "abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789", secretsFromConfig["test_pass_nine"].Charset)
|
||||
assert.Equal(t, "", secretsFromConfig["test_pass_nine"].Encoding)
|
||||
assert.Equal(t, "base64:", secretsFromConfig["test_pass_nine"].Prefix)
|
||||
|
||||
// Has all modifiers: length, charset=bytes, and prefix=base64: (Laravel-style)
|
||||
assert.Equal(t, "test_example_com_test_pass_ten_v1", secretsFromConfig["test_pass_ten"].RemoteName)
|
||||
assert.Equal(t, "v1", secretsFromConfig["test_pass_ten"].Version)
|
||||
assert.Equal(t, 32, secretsFromConfig["test_pass_ten"].Length)
|
||||
assert.Equal(t, "bytes", secretsFromConfig["test_pass_ten"].Charset)
|
||||
assert.Equal(t, "base64", secretsFromConfig["test_pass_ten"].Encoding) // Defaults to base64 for bytes
|
||||
assert.Equal(t, "base64:", secretsFromConfig["test_pass_ten"].Prefix)
|
||||
}
|
||||
|
||||
func TestReadSecretsConfigWithLongDomain(t *testing.T) {
|
||||
@ -58,3 +102,48 @@ func TestReadSecretsConfigWithLongDomain(t *testing.T) {
|
||||
}
|
||||
assert.Contains(t, err.Error(), "is > 64 chars")
|
||||
}
|
||||
|
||||
func TestEncodeSecret(t *testing.T) {
|
||||
// base64 encoding
|
||||
input := "testpassword123"
|
||||
encoded := encodeSecret(input, "base64")
|
||||
expected := "dGVzdHBhc3N3b3JkMTIz"
|
||||
assert.Equal(t, expected, encoded)
|
||||
|
||||
// no encoding (default)
|
||||
noEncoding := encodeSecret(input, "")
|
||||
assert.Equal(t, input, noEncoding)
|
||||
|
||||
// unknown encoding (should return original)
|
||||
unknownEncoding := encodeSecret(input, "unknown")
|
||||
assert.Equal(t, input, unknownEncoding)
|
||||
}
|
||||
|
||||
func TestApplyPrefix(t *testing.T) {
|
||||
input := "testvalue"
|
||||
|
||||
// with prefix
|
||||
prefixed := applyPrefix(input, "base64:")
|
||||
assert.Equal(t, "base64:testvalue", prefixed)
|
||||
|
||||
// with empty prefix
|
||||
noPrefixed := applyPrefix(input, "")
|
||||
assert.Equal(t, input, noPrefixed)
|
||||
}
|
||||
|
||||
func TestGenerateRandomBytes(t *testing.T) {
|
||||
// random bytes generation with 32 bytes
|
||||
key, err := generateRandomBytes(32)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 32, len([]byte(key))) // Check raw byte length
|
||||
|
||||
// random bytes generation with 16 bytes
|
||||
key16, err := generateRandomBytes(16)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 16, len([]byte(key16))) // Check raw byte length
|
||||
|
||||
// that keys are different (randomness)
|
||||
key2, err := generateRandomBytes(32)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEqual(t, key, key2)
|
||||
}
|
||||
|
||||
@ -4,3 +4,7 @@ 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
|
||||
SECRET_TEST_PASS_SEVEN_VERSION=v1 # length=32 charset=hex
|
||||
SECRET_TEST_PASS_EIGHT_VERSION=v1 # length=12 encoding=base64
|
||||
SECRET_TEST_PASS_NINE_VERSION=v1 # length=16 prefix=base64:
|
||||
SECRET_TEST_PASS_TEN_VERSION=v1 # length=32 charset=bytes prefix=base64:
|
||||
|
||||
@ -11,6 +11,10 @@ services:
|
||||
- test_pass_four
|
||||
- test_pass_five
|
||||
- test_pass_six
|
||||
- test_pass_seven
|
||||
- test_pass_eight
|
||||
- test_pass_nine
|
||||
- test_pass_ten
|
||||
|
||||
secrets:
|
||||
test_pass_one:
|
||||
@ -31,3 +35,15 @@ secrets:
|
||||
test_pass_six:
|
||||
external: true
|
||||
name: ${STACK_NAME}_test_pass_six_${SECRET_TEST_PASS_SIX_VERSION}
|
||||
test_pass_seven:
|
||||
external: true
|
||||
name: ${STACK_NAME}_test_pass_seven_${SECRET_TEST_PASS_SEVEN_VERSION}
|
||||
test_pass_eight:
|
||||
external: true
|
||||
name: ${STACK_NAME}_test_pass_eight_${SECRET_TEST_PASS_EIGHT_VERSION}
|
||||
test_pass_nine:
|
||||
external: true
|
||||
name: ${STACK_NAME}_test_pass_nine_${SECRET_TEST_PASS_NINE_VERSION}
|
||||
test_pass_ten:
|
||||
external: true
|
||||
name: ${STACK_NAME}_test_pass_ten_${SECRET_TEST_PASS_TEN_VERSION}
|
||||
|
||||
@ -247,7 +247,7 @@ func waitOnTasks(ctx context.Context, client apiclient.APIClient, namespace stri
|
||||
}
|
||||
}
|
||||
|
||||
if terminalStatesReached == len(tasks) {
|
||||
if terminalStatesReached >= len(tasks) {
|
||||
log.Debug(i18n.G("all tasks reached terminal state"))
|
||||
break
|
||||
}
|
||||
|
||||
@ -201,6 +201,7 @@ func RunDeploy(
|
||||
appName string,
|
||||
serverName string,
|
||||
dontWait bool,
|
||||
noInput bool,
|
||||
filters filters.Args,
|
||||
) error {
|
||||
log.Info(i18n.G("initialising deployment"))
|
||||
@ -226,6 +227,7 @@ func RunDeploy(
|
||||
appName,
|
||||
serverName,
|
||||
dontWait,
|
||||
noInput,
|
||||
filters,
|
||||
)
|
||||
}
|
||||
@ -248,6 +250,7 @@ func deployCompose(
|
||||
appName string,
|
||||
serverName string,
|
||||
dontWait bool,
|
||||
noInput bool,
|
||||
filters filters.Args,
|
||||
) error {
|
||||
namespace := convert.NewNamespace(opts.Namespace)
|
||||
@ -311,6 +314,7 @@ func deployCompose(
|
||||
Services: serviceIDs,
|
||||
AppName: appName,
|
||||
ServerName: serverName,
|
||||
NoInput: noInput,
|
||||
Filters: filters,
|
||||
}
|
||||
|
||||
@ -561,6 +565,7 @@ func timestamp() string {
|
||||
type WaitOpts struct {
|
||||
AppName string
|
||||
Filters filters.Args
|
||||
NoInput bool
|
||||
NoLog bool
|
||||
Quiet bool
|
||||
ServerName string
|
||||
@ -570,7 +575,13 @@ type WaitOpts struct {
|
||||
func WaitOnServices(ctx context.Context, cl *dockerClient.Client, opts WaitOpts) error {
|
||||
timeout := time.Duration(WaitTimeout) * time.Second
|
||||
model := ui.DeployInitialModel(ctx, cl, opts.Services, opts.AppName, timeout, opts.Filters)
|
||||
tui := tea.NewProgram(model)
|
||||
|
||||
var tui *tea.Program
|
||||
if opts.NoInput {
|
||||
tui = tea.NewProgram(model, tea.WithoutRenderer(), tea.WithInput(nil))
|
||||
} else {
|
||||
tui = tea.NewProgram(model)
|
||||
}
|
||||
|
||||
if !opts.Quiet {
|
||||
log.Info(i18n.G("polling deployment status"))
|
||||
|
||||
5
scripts/cloud-init/README.md
Normal file
5
scripts/cloud-init/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
# cloud-init
|
||||
|
||||
This folder contains cloud-init files for installing Abra and its dependencies.
|
||||
|
||||
For more information, see <https://cloudinit.readthedocs.io/en/latest/index.html>
|
||||
49
scripts/cloud-init/cloud-init.yaml
Normal file
49
scripts/cloud-init/cloud-init.yaml
Normal file
@ -0,0 +1,49 @@
|
||||
#cloud-config
|
||||
|
||||
package_update: true
|
||||
package_upgrade: true
|
||||
package_reboot_if_required: true
|
||||
|
||||
# https://packages.debian.org/bookworm/docker.io
|
||||
packages:
|
||||
- ca-certificates
|
||||
- curl
|
||||
- docker.io
|
||||
- docker-compose
|
||||
# https://stackoverflow.com/a/74084180
|
||||
- apparmor
|
||||
|
||||
# https://docs.coopcloud.tech/operators/tutorial/#server-setup
|
||||
runcmd:
|
||||
- curl -fsSL https://install.abra.coopcloud.tech | env HOME=/root bash
|
||||
- docker swarm init
|
||||
- docker network create -d overlay proxy
|
||||
|
||||
write_files:
|
||||
# Add abra to PATH and set EDITOR
|
||||
- path: /etc/profile.d/custom_path.sh
|
||||
content: |
|
||||
export PATH=$PATH:$HOME/.local/bin
|
||||
export EDITOR=vim
|
||||
owner: root:root
|
||||
permissions: '0755'
|
||||
# Send container log to journald: https://docs.coopcloud.tech/operators/handbook/#how-do-i-persist-container-logs-after-they-go-away
|
||||
- path: /etc/docker/daemon.json
|
||||
content: |
|
||||
{
|
||||
"log-driver": "journald",
|
||||
"log-opts": {
|
||||
"labels":"com.docker.swarm.service.name"
|
||||
}
|
||||
}
|
||||
owner: root:root
|
||||
permissions: '0644'
|
||||
# Rotate logs
|
||||
- path: /etc/systemd/journald.conf
|
||||
content: |
|
||||
[Journal]
|
||||
Storage=persistent
|
||||
SystemMaxUse=5G
|
||||
MaxFileSec=1month
|
||||
owner: root:root
|
||||
permissions: '0644'
|
||||
@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
ABRA_VERSION="0.11.0-beta"
|
||||
ABRA_VERSION="0.13.0-beta"
|
||||
ABRA_RELEASE_URL="https://git.coopcloud.tech/api/v1/repos/toolshed/abra/releases/tags/$ABRA_VERSION"
|
||||
RC_VERSION="0.11.0-beta"
|
||||
RC_VERSION="0.13.0-beta"
|
||||
RC_VERSION_URL="https://git.coopcloud.tech/api/v1/repos/toolshed/abra/releases/tags/$RC_VERSION"
|
||||
|
||||
for arg in "$@"; do
|
||||
@ -14,15 +14,15 @@ done
|
||||
|
||||
function show_banner {
|
||||
echo ""
|
||||
echo " ____ ____ _ _ "
|
||||
echo " / ___|___ ___ _ __ / ___| | ___ _ _ __| |"
|
||||
echo " | | / _ \ _____ / _ \| '_ \ | | | |/ _ \| | | |/ _' |"
|
||||
echo " | |__| (_) |_____| (_) | |_) | | |___| | (_) | |_| | (_| |"
|
||||
echo " \____\___/ \___/| .__/ \____|_|\___/ \__,_|\__,_|"
|
||||
echo " |_|"
|
||||
echo " ____ ____ _ _ "
|
||||
echo " / ___|___ ___ _ __ / ___| | ___ _ _ __| |"
|
||||
echo " | | / _ \ ___ / _ \| '_ \ | | | |/ _ \| | | |/ _' |"
|
||||
echo " | |__| (_) |___| (_) | |_) | | |___| | (_) | |_| | (_| |"
|
||||
echo " \____\___/ \___/| .__/ \____|_|\___/ \__,_|\__,_|"
|
||||
echo " |_|"
|
||||
echo ""
|
||||
echo ""
|
||||
echo " === Public interest infrastructure === "
|
||||
echo " === Public interest infrastructure === "
|
||||
echo ""
|
||||
echo ""
|
||||
}
|
||||
@ -89,7 +89,7 @@ function install_abra_release {
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "$(tput setaf 3)WARNING: $HOME/.local/bin/ is not in \$PATH! If you want to run abra by just typing "abra" you should add it to your \$PATH! To do that run this once and restart your terminal:$(tput sgr0)"
|
||||
p=$HOME/.local/bin
|
||||
com="echo PATH=\$PATH:$p"
|
||||
com='echo PATH="$PATH:'"$p"'"'
|
||||
if [[ $SHELL =~ "bash" ]]; then
|
||||
echo "$com >> $HOME/.bashrc"
|
||||
elif [[ $SHELL =~ "fizsh" ]]; then
|
||||
|
||||
@ -51,7 +51,7 @@ echo "========================================================================"
|
||||
echo "BUILDING ABRA"
|
||||
echo "========================================================================"
|
||||
export PATH="/usr/lib/go-1.21/bin:$PATH"
|
||||
make build-abra
|
||||
make build
|
||||
echo "========================================================================"
|
||||
|
||||
echo "========================================================================"
|
||||
|
||||
@ -106,7 +106,7 @@ teardown(){
|
||||
|
||||
run $ABRA app check "$TEST_APP_DOMAIN" --chaos
|
||||
assert_failure
|
||||
assert_output --partial 'unable to discover .env.sample'
|
||||
assert_output --partial 'no such file or directory'
|
||||
}
|
||||
|
||||
@test "error if missing env var" {
|
||||
|
||||
@ -23,12 +23,24 @@ teardown(){
|
||||
_reset_recipe
|
||||
_undeploy_app
|
||||
_undeploy_app2 "gitea.$TEST_SERVER"
|
||||
_undeploy_app2 "zammad.$TEST_SERVER"
|
||||
|
||||
_reset_app
|
||||
_reset_tags
|
||||
|
||||
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
|
||||
if [[ -d "$ABRA_DIR/recipes/foo" ]]; then
|
||||
run rm -rf "$ABRA_DIR/recipes/foo"
|
||||
assert_not_exists "$ABRA_DIR/recipes/foo"
|
||||
fi
|
||||
|
||||
# NOTE(d1): give some extra space for the pure chaos that we are unleashing
|
||||
# on the CI machine with these deploy tests. the hope is to prevent
|
||||
# lock-ups and network failures which are common in flaky swarm
|
||||
# mode
|
||||
sleep 1
|
||||
}
|
||||
|
||||
@test "validate app argument" {
|
||||
@ -75,8 +87,10 @@ teardown(){
|
||||
assert_success
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
@test "bail if recipe lint errors and no --chaos" {
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" checkout main
|
||||
assert_success
|
||||
|
||||
# Break the recipe
|
||||
run sed -i '/traefik.enable=.*/d' "$ABRA_DIR/recipes/$TEST_RECIPE/compose.yml"
|
||||
assert_success
|
||||
@ -88,8 +102,8 @@ teardown(){
|
||||
assert_success
|
||||
|
||||
# Make a broken release
|
||||
run $ABRA recipe sync --patch "$TEST_RECIPE"
|
||||
run $ABRA recipe release --patch -n "$TEST_RECIPE"
|
||||
assert_success
|
||||
|
||||
# Make sure we deploy latest
|
||||
_wipe_env_version
|
||||
@ -175,7 +189,7 @@ teardown(){
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
@test "bail if env has a hash but no --chaos" {
|
||||
@test "do not bail if env version is a hash but no --chaos" {
|
||||
wantHash=$(_get_n_hash 3)
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" reset --hard HEAD~3
|
||||
@ -250,6 +264,7 @@ teardown(){
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" \
|
||||
--no-input --no-converge-checks --chaos
|
||||
assert_success
|
||||
assert_output --regexp "NEW DEPLOYMENT.*${_get_head_hash:0:8}"
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
@ -367,6 +382,21 @@ teardown(){
|
||||
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks
|
||||
assert_failure
|
||||
assert_output --partial "secret not generated"
|
||||
}
|
||||
|
||||
@test "error if secret not inserted" {
|
||||
run sed -i 's/COMPOSE_FILE="compose.yml"/COMPOSE_FILE="compose.yml:compose.skip_pass.yml"/g' \
|
||||
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||
assert_success
|
||||
|
||||
run sed -i 's/#SECRET_TEST_SKIP_PASS_VERSION=v1/SECRET_TEST_SKIP_PASS_VERSION=v1/g' \
|
||||
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||
assert_success
|
||||
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks
|
||||
assert_failure
|
||||
assert_output --partial "secret not inserted"
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
@ -527,7 +557,7 @@ teardown(){
|
||||
|
||||
# bats test_tags=slow
|
||||
@test "ignore timeout when not present in env" {
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks --debug
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks
|
||||
assert_success
|
||||
refute_output --partial "timeout: set to"
|
||||
}
|
||||
@ -538,6 +568,7 @@ teardown(){
|
||||
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||
assert_success
|
||||
|
||||
# NOTE(d1}: --debug required
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks --debug
|
||||
assert_success
|
||||
assert_output --partial "timeout: set to 120"
|
||||
@ -561,3 +592,113 @@ teardown(){
|
||||
assert_success
|
||||
refute_output --partial "IMAGES"
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
@test "re-deploy updates existing env vars" {
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input
|
||||
assert_success
|
||||
|
||||
run docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' \
|
||||
$(docker ps -f name="$TEST_APP_DOMAIN_$TEST_SERVER" -q)
|
||||
assert_success
|
||||
assert_output --partial "WITH_COMMENT=foo"
|
||||
|
||||
run sed -i 's/WITH_COMMENT=foo/WITH_COMMENT=bar/g' \
|
||||
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||
assert_success
|
||||
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --force
|
||||
assert_success
|
||||
|
||||
run docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' \
|
||||
$(docker ps -f name="$TEST_APP_DOMAIN_$TEST_SERVER" -q)
|
||||
assert_success
|
||||
refute_output --partial "WITH_COMMENT=foo"
|
||||
assert_output --partial "WITH_COMMENT=bar"
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
@test "deploy with udp and tcp on same port" {
|
||||
run sed -i 's/TYPE=abra-test-recipe:.*/TYPE=git.coopcloud.tech\/p4u1\/abra-test-recipe:030e8a1cb1a0f17281847b3e55d829220ad32c50/g' \
|
||||
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||
assert_success
|
||||
|
||||
run bash -c "printf '\nCOMPOSE_FILE=\"\$COMPOSE_FILE:compose.udp-and-tcp.yml\"' >> $ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||
assert_success
|
||||
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input
|
||||
assert_success
|
||||
|
||||
run docker service inspect --format '{{ range .Endpoint.Ports }}{{ .Protocol }}={{ .PublishedPort }}{{ end }}' \
|
||||
"${TEST_APP_DOMAIN//./_}_app"
|
||||
assert_success
|
||||
assert_output --partial "tcp=1312"
|
||||
assert_output --partial "udp=1312"
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
@test "does not crash when docker image has no tag" {
|
||||
run sed -i 's/TYPE=abra-test-recipe:.*/TYPE=git.coopcloud.tech\/p4u1\/abra-test-recipe:b29422d5a344ea45df271443182f775ea82b4da8/g' \
|
||||
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||
assert_success
|
||||
|
||||
run bash -c "printf '\nCOMPOSE_FILE=\"\$COMPOSE_FILE:compose.no-image-tag.yml\"' >> $ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||
assert_success
|
||||
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input
|
||||
assert_success
|
||||
}
|
||||
|
||||
|
||||
@test "does not use old recipe version when recipe is broken" {
|
||||
run $ABRA app new zammad \
|
||||
--no-input \
|
||||
--server "$TEST_SERVER" \
|
||||
--domain "zammad.$TEST_SERVER" \
|
||||
--secrets
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/servers/$TEST_SERVER/zammad.$TEST_SERVER.env"
|
||||
|
||||
# NOTE(d1): --no-converge-checks because the zammad recipe is a beast and we
|
||||
# mostly only care about the correct version being used
|
||||
run $ABRA app deploy "zammad.$TEST_SERVER" \
|
||||
--no-input --no-converge-checks
|
||||
assert_success
|
||||
refute_output --partial "1.0.0+6.3.1-95"
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
@test "unable to deploy borked tag" {
|
||||
_remove_tags
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag \
|
||||
-a "2.4.8_1" -m "feat: completely borked tag"
|
||||
assert_success
|
||||
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" "2.4.8_1" \
|
||||
--no-input --no-converge-checks --debug
|
||||
assert_failure
|
||||
assert_output --partial "unable to parse"
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
@test "app deploy with borked sample env gives useful error" {
|
||||
run $ABRA recipe new foo --no-input
|
||||
assert_success
|
||||
|
||||
run $ABRA app new foo \
|
||||
--no-input \
|
||||
--server "$TEST_SERVER" \
|
||||
--domain "foo.$TEST_SERVER" \
|
||||
--chaos
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/servers/$TEST_SERVER/foo.$TEST_SERVER.env"
|
||||
|
||||
run bash -c "printf '\nEVIL-VAR=EVIL' >> $ABRA_DIR/recipes/foo/.env.sample"
|
||||
assert_success
|
||||
|
||||
run $ABRA app deploy "foo.$TEST_SERVER" \
|
||||
--no-input --no-converge-checks --chaos
|
||||
assert_failure
|
||||
assert_output --partial "unexpected character"
|
||||
}
|
||||
|
||||
@ -127,3 +127,14 @@ teardown(){
|
||||
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||
assert_success
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
@test "new env version written to container env" {
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" --no-input
|
||||
assert_success
|
||||
|
||||
run docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' \
|
||||
$(docker ps -f name="$TEST_APP_DOMAIN_$TEST_SERVER" -q)
|
||||
assert_success
|
||||
assert_output --partial "$TEST_RECIIPE:0.1.0+1.20.0"
|
||||
}
|
||||
|
||||
@ -38,8 +38,8 @@ teardown(){
|
||||
assert_success
|
||||
|
||||
assert_output --partial 'NEW DEPLOY OVERVIEW'
|
||||
assert_output --partial 'CURRENT DEPLOYMENT N/A'
|
||||
assert_output --partial 'ENV VERSION N/A'
|
||||
assert_output --partial 'CURRENT DEPLOYMENT -'
|
||||
assert_output --partial 'ENV VERSION -'
|
||||
assert_output --partial "NEW DEPLOYMENT ${latestRelease}"
|
||||
assert_output --partial "IMAGES nginx: ${latestRelease##*+} (new)"
|
||||
assert_output --partial "CONFIGS test_conf: v1 (new)"
|
||||
@ -57,7 +57,7 @@ teardown(){
|
||||
assert_success
|
||||
|
||||
assert_output --partial 'NEW DEPLOY OVERVIEW'
|
||||
assert_output --partial "CURRENT DEPLOYMENT N/A"
|
||||
assert_output --partial "CURRENT DEPLOYMENT -"
|
||||
assert_output --partial "ENV VERSION ${latestRelease}"
|
||||
assert_output --partial "NEW DEPLOYMENT ${latestRelease}"
|
||||
assert_output --partial "IMAGES nginx: ${latestRelease##*+} (new)"
|
||||
@ -68,6 +68,13 @@ teardown(){
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "domain shown with https" {
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" \
|
||||
--no-input --no-converge-checks
|
||||
assert_success
|
||||
assert_output --partial "https://$TEST_DOMAIN"
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
@test "show changed config version on re-deploy" {
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" \
|
||||
@ -102,7 +109,7 @@ teardown(){
|
||||
assert_success
|
||||
|
||||
assert_output --partial 'NEW DEPLOY OVERVIEW'
|
||||
assert_output --partial "CURRENT DEPLOYMENT N/A"
|
||||
assert_output --partial "CURRENT DEPLOYMENT -"
|
||||
assert_output --partial "ENV VERSION 0.1.1+1.20.2"
|
||||
assert_output --partial "NEW DEPLOYMENT 0.1.1+1.20.2"
|
||||
|
||||
@ -125,7 +132,7 @@ teardown(){
|
||||
assert_success
|
||||
|
||||
assert_output --partial 'NEW DEPLOY OVERVIEW'
|
||||
assert_output --partial "CURRENT DEPLOYMENT N/A"
|
||||
assert_output --partial "CURRENT DEPLOYMENT -"
|
||||
assert_output --partial "ENV VERSION 0.1.1+1.20.2"
|
||||
assert_output --partial "NEW DEPLOYMENT ${latestRelease}"
|
||||
|
||||
@ -163,7 +170,7 @@ teardown(){
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "can not redeploy chaos version without --chaos" {
|
||||
@test "cannot redeploy chaos version without --chaos" {
|
||||
headHash=$(_get_head_hash)
|
||||
latestRelease=$(_latest_release)
|
||||
|
||||
@ -181,7 +188,7 @@ teardown(){
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" \
|
||||
--no-input --no-converge-checks --force --debug
|
||||
assert_failure
|
||||
assert_output --regexp 'can not redeploy chaos version .*' + "${headHash:0:8}+U"
|
||||
assert_output --regexp 'cannot redeploy previous chaos version .*' + "${headHash:0:8}+U"
|
||||
}
|
||||
|
||||
@test "deploy then force commit deploy" {
|
||||
@ -219,7 +226,7 @@ teardown(){
|
||||
assert_success
|
||||
|
||||
assert_output --partial 'NEW DEPLOY OVERVIEW'
|
||||
assert_output --partial "CURRENT DEPLOYMENT N/A"
|
||||
assert_output --partial "CURRENT DEPLOYMENT -"
|
||||
assert_output --partial "ENV VERSION ${latestRelease}"
|
||||
assert_output --partial "NEW DEPLOYMENT ${headHash:0:8}"
|
||||
|
||||
|
||||
@ -28,17 +28,17 @@ teardown(){
|
||||
}
|
||||
|
||||
@test "validate app argument" {
|
||||
run $ABRA app env
|
||||
run $ABRA app env list
|
||||
assert_failure
|
||||
|
||||
run $ABRA app env DOESNTEXIST
|
||||
run $ABRA app env list DOESNTEXIST
|
||||
assert_failure
|
||||
}
|
||||
|
||||
@test "show env version" {
|
||||
latestRelease=$(_latest_release)
|
||||
|
||||
run $ABRA app env "$TEST_APP_DOMAIN"
|
||||
run $ABRA app env list "$TEST_APP_DOMAIN"
|
||||
assert_success
|
||||
assert_output --partial "$latestRelease"
|
||||
}
|
||||
@ -48,7 +48,7 @@ teardown(){
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
|
||||
run $ABRA app env "$TEST_APP_DOMAIN"
|
||||
run $ABRA app env list "$TEST_APP_DOMAIN"
|
||||
assert_success
|
||||
|
||||
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
@ -57,3 +57,44 @@ teardown(){
|
||||
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
}
|
||||
|
||||
@test "app env pull explodes when no deployed app" {
|
||||
run $ABRA app env pull "$TEST_APP_DOMAIN" -s "$TEST_SERVER"
|
||||
assert_failure
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
@test "app env pull recreates app env when missing" {
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input
|
||||
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"
|
||||
|
||||
run $ABRA app env pull "$TEST_APP_DOMAIN" -s "$TEST_SERVER"
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
@test "app env pull recreates correct version" {
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" --no-input
|
||||
assert_success
|
||||
|
||||
run grep -q "TYPE=$TEST_RECIPE:0.1.0+1.20.0" \
|
||||
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||
assert_success
|
||||
|
||||
run 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"
|
||||
|
||||
run $ABRA app env pull "$TEST_APP_DOMAIN" -s "$TEST_SERVER"
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||
|
||||
run grep -q "TYPE=$TEST_RECIPE:0.1.0+1.20.0" \
|
||||
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||
assert_success
|
||||
}
|
||||
|
||||
@ -15,6 +15,7 @@ teardown_file(){
|
||||
_undeploy_app
|
||||
_rm_app
|
||||
_rm_server
|
||||
_reset_recipe
|
||||
|
||||
if [[ -d "$ABRA_DIR/servers/foo" ]]; then
|
||||
run rm -rf "$ABRA_DIR/servers/foo"
|
||||
@ -66,6 +67,16 @@ teardown(){
|
||||
assert_output --partial "$TEST_SERVER"
|
||||
assert_output --partial "$TEST_APP_DOMAIN"
|
||||
assert_output --partial "deployed"
|
||||
assert_output --partial "latest"
|
||||
|
||||
_remove_tags
|
||||
|
||||
run $ABRA app ls --status
|
||||
assert_success
|
||||
assert_output --partial "$TEST_SERVER"
|
||||
assert_output --partial "$TEST_APP_DOMAIN"
|
||||
assert_output --partial "deployed"
|
||||
assert_output --partial "latest"
|
||||
}
|
||||
|
||||
@test "filter by server" {
|
||||
@ -150,7 +161,7 @@ teardown(){
|
||||
--no-input --no-converge-checks --chaos
|
||||
assert_success
|
||||
|
||||
run $ABRA app ls --status
|
||||
run $ABRA app ls --status --chaos
|
||||
assert_success
|
||||
assert_output --partial "+U"
|
||||
|
||||
@ -159,23 +170,6 @@ teardown(){
|
||||
assert_not_exists "$ABRA_DIR/servers/foo.com"
|
||||
}
|
||||
|
||||
@test "list with status skips unknown servers" {
|
||||
if [[ ! -d "$ABRA_DIR/servers/foo" ]]; then
|
||||
run mkdir -p "$ABRA_DIR/servers/foo"
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/servers/foo"
|
||||
|
||||
run cp "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" \
|
||||
"$ABRA_DIR/servers/foo/$TEST_APP_DOMAIN.env"
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/servers/foo/$TEST_APP_DOMAIN.env"
|
||||
fi
|
||||
|
||||
run $ABRA app ls --status
|
||||
assert_success
|
||||
assert_output --partial "unknown server"
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
@test "list does not fail if missing .env" {
|
||||
_deploy_app
|
||||
@ -193,3 +187,19 @@ teardown(){
|
||||
<(jq -S "." <(echo '{}'))
|
||||
assert_success
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
@test "list ignores borked tags" {
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" \
|
||||
--no-input --no-converge-checks
|
||||
assert_success
|
||||
|
||||
# NOTE(d1): always upgradable tag which is also borked
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag \
|
||||
-a "100.100.100_1_2_3" -m "feat: completely borked tag"
|
||||
assert_success
|
||||
|
||||
run $ABRA app ls --status --debug
|
||||
assert_success
|
||||
assert_output --partial "unable to parse"
|
||||
}
|
||||
|
||||
@ -22,8 +22,15 @@ teardown(){
|
||||
_reset_recipe
|
||||
_reset_tags
|
||||
|
||||
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
if [[ -f "$ABRA_DIR/recipes/$TEST_RECIPE/foo" ]]; then
|
||||
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
fi
|
||||
|
||||
if [[ -f "$ABRA_DIR/servers/$TEST_SERVER/rauthy.$TEST_APP_DOMAIN.env" ]]; then
|
||||
run rm -rf "$ABRA_DIR/servers/$TEST_SERVER/rauthy.$TEST_APP_DOMAIN.env"
|
||||
assert_not_exists "$ABRA_DIR/servers/$TEST_SERVER/rauthy.$TEST_APP_DOMAIN.env"
|
||||
fi
|
||||
}
|
||||
|
||||
@test "create new app" {
|
||||
@ -56,6 +63,30 @@ teardown(){
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "ensure recipe is up-to-date" {
|
||||
run bash -c 'git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -l'
|
||||
assert_success
|
||||
assert_output --partial '0.3.5+1.21.0'
|
||||
|
||||
run bash -c 'git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -d 0.3.5+1.21.0'
|
||||
assert_success
|
||||
|
||||
run bash -c 'git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -l'
|
||||
assert_success
|
||||
refute_output --partial '0.3.5+1.21.0'
|
||||
|
||||
run $ABRA app new "$TEST_RECIPE" \
|
||||
--no-input \
|
||||
--server "$TEST_SERVER" \
|
||||
--domain "$TEST_APP_DOMAIN"
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||
|
||||
run grep -q "TYPE=$TEST_RECIPE:0.3.5+1.21.0" \
|
||||
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "create new app with version commit" {
|
||||
tagHash=$(_get_tag_hash "0.3.0+1.21.0")
|
||||
|
||||
@ -270,3 +301,32 @@ teardown(){
|
||||
assert_success
|
||||
refute_output --partial "requires secret generation"
|
||||
}
|
||||
|
||||
@test "do not warn about generation when generate=false" {
|
||||
run $ABRA app new --domain "$TEST_APP_DOMAIN" renovate "1.0.1+41-full"
|
||||
assert_success
|
||||
refute_output --partial "requires secret generation"
|
||||
}
|
||||
|
||||
@test "warn about insertion when generate=false" {
|
||||
run $ABRA app new --domain "$TEST_APP_DOMAIN" renovate "1.0.1+41-full"
|
||||
assert_success
|
||||
assert_output --partial "requires secret insertion"
|
||||
}
|
||||
|
||||
@test "warn about both insert/generate when generate=false/true" {
|
||||
run $ABRA app new rauthy "1.0.0+0.32.3" \
|
||||
--no-input \
|
||||
--server "$TEST_SERVER" \
|
||||
--domain "rauthy.$TEST_APP_DOMAIN"
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/servers/$TEST_SERVER/rauthy.$TEST_APP_DOMAIN.env"
|
||||
assert_output --partial "requires secret generation"
|
||||
assert_output --partial "requires secret insertion"
|
||||
}
|
||||
|
||||
@test "no warn about generation if already generated" {
|
||||
run $ABRA app new "$TEST_RECIPE" --domain "$TEST_APP_DOMAIN" --secrets
|
||||
assert_success
|
||||
refute_output --partial "requires secret generation"
|
||||
}
|
||||
|
||||
@ -181,7 +181,7 @@ teardown(){
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
@test "rollback chaos deployment is not possible" {
|
||||
@test "rollback chaos deployment is possible" {
|
||||
tagHash=$(_get_tag_hash "0.2.0+1.21.0")
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" checkout "$tagHash"
|
||||
assert_success
|
||||
@ -191,12 +191,13 @@ teardown(){
|
||||
assert_output --partial "${tagHash:0:8}"
|
||||
|
||||
run $ABRA app rollback "$TEST_APP_DOMAIN" "0.1.1+1.20.2" --no-input --no-converge-checks
|
||||
assert_failure
|
||||
assert_output --partial 'current deployment' + "${tagHash:0:8}" + 'is not a known version'
|
||||
assert_success
|
||||
assert_output --regexp "CURRENT DEPLOYMENT.*${tagHash:0:8}"
|
||||
assert_output --regexp "ENV VERSION.*${tagHash:0:8}"
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
@test "chaos commit rollback not possible" {
|
||||
@test "specific chaos commit rollback not possible" {
|
||||
_deploy_app
|
||||
|
||||
tagHash=$(_get_tag_hash "0.2.0+1.21.0")
|
||||
|
||||
@ -33,10 +33,29 @@ teardown(){
|
||||
assert_success
|
||||
|
||||
run $ABRA app rollback "$TEST_APP_DOMAIN" "0.1.0+1.20.0" \
|
||||
--no-input --no-converge-checks --debug
|
||||
--no-input --no-converge-checks
|
||||
assert_success
|
||||
|
||||
run grep -q "TYPE=abra-test-recipe:0.1.0+1.20.0" \
|
||||
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||
assert_success
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
@test "new env version written to container env" {
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.2.0+1.21.0" --no-input
|
||||
assert_success
|
||||
|
||||
run grep -q "TYPE=abra-test-recipe:0.2.0+1.21.0" \
|
||||
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||
assert_success
|
||||
|
||||
run $ABRA app rollback "$TEST_APP_DOMAIN" "0.1.0+1.20.0" \
|
||||
--no-input
|
||||
assert_success
|
||||
|
||||
run docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' \
|
||||
$(docker ps -f name="$TEST_APP_DOMAIN_$TEST_SERVER" -q)
|
||||
assert_success
|
||||
assert_output --partial "$TEST_RECIIPE:0.1.0+1.20.0"
|
||||
}
|
||||
|
||||
@ -75,7 +75,7 @@ teardown(){
|
||||
|
||||
assert_output --partial 'DOWNGRADE OVERVIEW'
|
||||
assert_output --partial 'CURRENT DEPLOYMENT 0.2.0+1.21.0'
|
||||
assert_output --partial 'ENV VERSION N/A'
|
||||
assert_output --partial 'ENV VERSION -'
|
||||
assert_output --partial 'NEW DEPLOYMENT 0.1.0+1.20.0'
|
||||
|
||||
run grep -q "TYPE=$TEST_RECIPE:${latestRelease}" \
|
||||
|
||||
@ -106,6 +106,7 @@ teardown(){
|
||||
|
||||
run $ABRA app undeploy "$TEST_APP_DOMAIN" --no-input
|
||||
assert_success
|
||||
assert_output --regexp "CURRENT DEPLOYMENT.*${_get_head_hash:0:8}"
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
|
||||
@ -36,7 +36,7 @@ teardown(){
|
||||
assert_output --partial 'UNDEPLOY OVERVIEW'
|
||||
assert_output --partial 'CURRENT DEPLOYMENT 0.1.0+1.20.0'
|
||||
assert_output --partial 'ENV VERSION 0.1.0+1.20.0'
|
||||
assert_output --partial 'NEW DEPLOYMENT N/A'
|
||||
assert_output --partial 'NEW DEPLOYMENT -'
|
||||
|
||||
run grep -q "TYPE=$TEST_RECIPE:0.1.0+1.20.0" \
|
||||
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||
@ -57,7 +57,7 @@ teardown(){
|
||||
assert_output --partial 'UNDEPLOY OVERVIEW'
|
||||
assert_output --partial "CURRENT DEPLOYMENT ${headHash:0:8}"
|
||||
assert_output --partial "ENV VERSION ${headHash:0:8}"
|
||||
assert_output --partial 'NEW DEPLOYMENT N/A'
|
||||
assert_output --partial 'NEW DEPLOYMENT -'
|
||||
|
||||
run grep -q "TYPE=$TEST_RECIPE:${headHash:0:8}" \
|
||||
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||
@ -81,7 +81,7 @@ teardown(){
|
||||
assert_output --partial 'UNDEPLOY OVERVIEW'
|
||||
assert_output --partial "CURRENT DEPLOYMENT ${headHash:0:8}+U"
|
||||
assert_output --partial "ENV VERSION ${headHash:0:8}+U"
|
||||
assert_output --partial 'NEW DEPLOYMENT N/A'
|
||||
assert_output --partial 'NEW DEPLOYMENT -'
|
||||
|
||||
run grep -q "TYPE=$TEST_RECIPE:${headHash:0:8}" \
|
||||
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||
|
||||
@ -256,7 +256,7 @@ teardown(){
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
@test "upgrade commit deployment not possible" {
|
||||
@test "specific version upgrade after chaos deploy" {
|
||||
tagHash=$(_get_tag_hash "0.1.0+1.20.0")
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" checkout "$tagHash"
|
||||
assert_success
|
||||
@ -266,20 +266,29 @@ teardown(){
|
||||
assert_output --partial "${tagHash:0:8}"
|
||||
|
||||
run $ABRA app upgrade "$TEST_APP_DOMAIN" "0.1.1+1.20.2" --no-input --no-converge-checks
|
||||
assert_failure
|
||||
assert_output --partial "not a known version"
|
||||
assert_success
|
||||
assert_output --regexp "CURRENT DEPLOYMENT.*${tagHash:0:8}"
|
||||
assert_output --regexp "ENV VERSION.*${tagHash:0:8}"
|
||||
assert_output --regexp "NEW DEPLOYMENT.*0\.1\.1\+1\.20\.2"
|
||||
}
|
||||
|
||||
@test "chaos commit upgrade not possible" {
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" --no-input --no-converge-checks
|
||||
# bats test_tags=slow
|
||||
@test "upgrade to latest after chaos deploy" {
|
||||
latestRelease=$(_latest_release)
|
||||
|
||||
tagHash=$(_get_tag_hash "0.1.0+1.20.0")
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" checkout "$tagHash"
|
||||
assert_success
|
||||
assert_output --partial '0.1.0+1.20.0'
|
||||
|
||||
tagHash=$(_get_tag_hash "0.2.0+1.21.0")
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks --chaos
|
||||
assert_success
|
||||
assert_output --partial "${tagHash:0:8}"
|
||||
|
||||
run $ABRA app upgrade "$TEST_APP_DOMAIN" "$tagHash" --no-input --no-converge-checks
|
||||
assert_failure
|
||||
assert_output --partial "not a known version"
|
||||
run $ABRA app upgrade "$TEST_APP_DOMAIN" --no-input --no-converge-checks
|
||||
assert_success
|
||||
assert_output --regexp "CURRENT DEPLOYMENT.*${tagHash:0:8}"
|
||||
assert_output --regexp "ENV VERSION.*${tagHash:0:8}"
|
||||
assert_output --partial "${latestRelease}"
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
|
||||
@ -40,3 +40,21 @@ teardown(){
|
||||
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||
assert_success
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
@test "new env version written to container env" {
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" --no-input
|
||||
assert_success
|
||||
|
||||
run grep -q "TYPE=abra-test-recipe:0.1.0+1.20.0" \
|
||||
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||
assert_success
|
||||
|
||||
run $ABRA app upgrade "$TEST_APP_DOMAIN" "0.2.0+1.21.0" --no-input
|
||||
assert_success
|
||||
|
||||
run docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' \
|
||||
$(docker ps -f name="$TEST_APP_DOMAIN_$TEST_SERVER" -q)
|
||||
assert_success
|
||||
assert_output --partial "$TEST_RECIIPE:0.2.0+1.21.0"
|
||||
}
|
||||
|
||||
@ -75,7 +75,7 @@ teardown(){
|
||||
|
||||
assert_output --partial 'UPGRADE OVERVIEW'
|
||||
assert_output --partial 'CURRENT DEPLOYMENT 0.2.0+1.21.0'
|
||||
assert_output --partial 'ENV VERSION N/A'
|
||||
assert_output --partial 'ENV VERSION -'
|
||||
assert_output --partial "NEW DEPLOYMENT $latestRelease"
|
||||
|
||||
run grep -q "TYPE=$TEST_RECIPE:${latestRelease}" \
|
||||
|
||||
@ -13,7 +13,6 @@ _common_setup() {
|
||||
load "$PWD/tests/integration/helpers/docker"
|
||||
|
||||
export ABRA="$PWD/abra"
|
||||
export KADABRA="$PWD/kadabra"
|
||||
|
||||
export TEST_APP_NAME="$(basename "${BATS_TEST_FILENAME//./_}")"
|
||||
export TEST_APP_DOMAIN="$TEST_APP_NAME.$TEST_SERVER"
|
||||
@ -21,4 +20,20 @@ _common_setup() {
|
||||
export TEST_RECIPE="abra-test-recipe"
|
||||
|
||||
_ensure_swarm
|
||||
_ensure_ssh_agent
|
||||
}
|
||||
|
||||
|
||||
_ensure_ssh_agent() {
|
||||
if ! command -v ssh-agent >/dev/null 2>&1
|
||||
then
|
||||
echo "ssh-agent is missing, please install it"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export SSH_AUTH_SOCK="$HOME/.ssh/ssh_auth_sock"
|
||||
if [ ! -S ~/.ssh/ssh_auth_sock ]; then
|
||||
eval `ssh-agent`
|
||||
ln -sf "$SSH_AUTH_SOCK" ~/.ssh/ssh_auth_sock
|
||||
fi
|
||||
}
|
||||
|
||||
@ -17,7 +17,12 @@ _remove_tags(){
|
||||
|
||||
run bash -c 'git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -l | wc -l'
|
||||
assert_success
|
||||
assert_output '0'
|
||||
# If this was done without the --regexp I get this error:
|
||||
# -- output differs --
|
||||
# expected : 0
|
||||
# actual : 0
|
||||
# --
|
||||
assert_output --regexp '[[:space:]]*0'
|
||||
}
|
||||
|
||||
_reset_tags() {
|
||||
|
||||
@ -5,11 +5,22 @@ _latest_release(){
|
||||
}
|
||||
|
||||
_fetch_recipe() {
|
||||
# clone first to a bare repo which will serve as origin-ssh
|
||||
# this enables simulating git push in recipe release
|
||||
if [[ ! -d "$ABRA_DIR/recipes/$TEST_RECIPE" ]]; then
|
||||
run mkdir -p "$ABRA_DIR/origin-recipes"
|
||||
assert_success
|
||||
|
||||
run git clone "https://git.coopcloud.tech/toolshed/$TEST_RECIPE" "$ABRA_DIR/origin-recipes/$TEST_RECIPE.git" --bare
|
||||
assert_success
|
||||
|
||||
run mkdir -p "$ABRA_DIR/recipes"
|
||||
assert_success
|
||||
|
||||
run git clone "https://git.coopcloud.tech/toolshed/$TEST_RECIPE" "$ABRA_DIR/recipes/$TEST_RECIPE"
|
||||
run git clone "$ABRA_DIR/origin-recipes/$TEST_RECIPE.git" "$ABRA_DIR/recipes/$TEST_RECIPE"
|
||||
assert_success
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" remote add origin-ssh "$ABRA_DIR/origin-recipes/$TEST_RECIPE.git"
|
||||
assert_success
|
||||
fi
|
||||
}
|
||||
@ -19,6 +30,10 @@ _reset_recipe(){
|
||||
assert_success
|
||||
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE"
|
||||
|
||||
run rm -rf "$ABRA_DIR/origin-recipes/$TEST_RECIPE.git"
|
||||
assert_success
|
||||
assert_not_exists "$ABRA_DIR/origin-recipes/$TEST_RECIPE.git"
|
||||
|
||||
_fetch_recipe
|
||||
}
|
||||
|
||||
|
||||
@ -53,7 +53,8 @@ teardown(){
|
||||
--domain "foobar.$TEST_SERVER"
|
||||
assert_success
|
||||
|
||||
run $ABRA app deploy "foobar.$TEST_SERVER" --no-input
|
||||
run $ABRA app deploy "foobar.$TEST_SERVER" \
|
||||
--no-input --no-converge-checks
|
||||
assert_success
|
||||
}
|
||||
|
||||
|
||||
@ -1,18 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
setup_file(){
|
||||
setup_file() {
|
||||
load "$PWD/tests/integration/helpers/common"
|
||||
_common_setup
|
||||
_add_server
|
||||
_new_app
|
||||
}
|
||||
|
||||
teardown_file(){
|
||||
teardown_file() {
|
||||
_rm_server
|
||||
_reset_recipe
|
||||
}
|
||||
|
||||
setup(){
|
||||
setup() {
|
||||
load "$PWD/tests/integration/helpers/common"
|
||||
_common_setup
|
||||
_set_git_author
|
||||
@ -21,6 +21,14 @@ setup(){
|
||||
teardown() {
|
||||
_reset_recipe
|
||||
_reset_tags
|
||||
if [[ -d "$ABRA_DIR/recipes/foobar" ]]; then
|
||||
run rm -rf "$ABRA_DIR/recipes/foobar"
|
||||
assert_success
|
||||
fi
|
||||
if [[ -d "$ABRA_DIR/origin-recipes/foobar.git" ]]; then
|
||||
run rm -rf "$ABRA_DIR/origin-recipes/foobar.git"
|
||||
assert_success
|
||||
fi
|
||||
}
|
||||
|
||||
@test "validate recipe argument" {
|
||||
@ -32,10 +40,10 @@ teardown() {
|
||||
}
|
||||
|
||||
@test "release patch bump" {
|
||||
run $ABRA recipe upgrade "$TEST_RECIPE" --no-input --patch
|
||||
run $ABRA recipe upgrade "$TEST_RECIPE" --no-input --patch --commit
|
||||
assert_success
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" diff
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" show
|
||||
assert_success
|
||||
assert_output --partial 'image: nginx:1.21.6'
|
||||
|
||||
@ -45,17 +53,9 @@ teardown() {
|
||||
-a "0.3.0+1.21.0" -m "fake: 0.3.0+1.21.0"
|
||||
assert_success
|
||||
|
||||
run $ABRA recipe sync "$TEST_RECIPE" --no-input --patch
|
||||
assert_success
|
||||
assert_output --partial 'synced label'
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" diff
|
||||
assert_success
|
||||
assert_output --partial 'coop-cloud.${STACK_NAME}.version=0.3.1+1.21.6'
|
||||
|
||||
run $ABRA recipe release "$TEST_RECIPE" --no-input --patch
|
||||
assert_success
|
||||
assert_output --partial 'no -p/--publish passed, not publishing'
|
||||
assert_output --partial 'INFO new release published:'
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag --list
|
||||
assert_success
|
||||
@ -63,10 +63,10 @@ teardown() {
|
||||
}
|
||||
|
||||
@test "release minor bump" {
|
||||
run $ABRA recipe upgrade "$TEST_RECIPE" --no-input --minor
|
||||
run $ABRA recipe upgrade "$TEST_RECIPE" --no-input --minor --commit
|
||||
assert_success
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" diff
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" show
|
||||
assert_success
|
||||
assert_output --regexp 'image: nginx:1.2.*'
|
||||
|
||||
@ -76,54 +76,144 @@ teardown() {
|
||||
-a "0.3.0+1.21.0" -m "fake: 0.3.0+1.21.0"
|
||||
assert_success
|
||||
|
||||
run $ABRA recipe sync "$TEST_RECIPE" --no-input --minor
|
||||
assert_success
|
||||
assert_output --partial 'synced label'
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" diff
|
||||
assert_success
|
||||
assert_output --regexp 'coop-cloud\.\$\{STACK_NAME\}\.version=0\.4\.0\+1\.2.*'
|
||||
|
||||
run $ABRA recipe release "$TEST_RECIPE" --no-input --minor
|
||||
assert_success
|
||||
assert_output --partial 'no -p/--publish passed, not publishing'
|
||||
assert_output --partial 'INFO new release published:'
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag --list
|
||||
assert_success
|
||||
assert_output --regexp '0\.4\.0\+1\.2.*'
|
||||
}
|
||||
|
||||
@test "unknown files not committed" {
|
||||
run $ABRA recipe upgrade "$TEST_RECIPE" --no-input --patch
|
||||
@test "release with unstaged changes" {
|
||||
run bash -c 'echo "# unstaged changes" >> "$ABRA_DIR/recipes/$TEST_RECIPE/compose.yml"'
|
||||
assert_success
|
||||
|
||||
run bash -c 'echo "unstaged changes" >> "$ABRA_DIR/recipes/$TEST_RECIPE/foo"'
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" diff --quiet
|
||||
assert_failure
|
||||
|
||||
run $ABRA recipe release "$TEST_RECIPE" --no-input --patch
|
||||
assert_success
|
||||
assert_output --partial 'no -p/--publish passed, not publishing'
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" rm foo
|
||||
assert_failure
|
||||
assert_output --partial "fatal: pathspec 'foo' did not match any files"
|
||||
assert_output --partial "working directory not clean"
|
||||
}
|
||||
|
||||
@test "release with staged changes" {
|
||||
run bash -c 'echo "# staged changes" >> "$ABRA_DIR/recipes/$TEST_RECIPE/compose.yml"'
|
||||
assert_success
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" add compose.yml
|
||||
assert_success
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" diff --quiet --cached
|
||||
assert_failure
|
||||
|
||||
run $ABRA recipe release "$TEST_RECIPE" --no-input --patch
|
||||
assert_failure
|
||||
assert_output --partial "working directory not clean"
|
||||
}
|
||||
|
||||
@test "release with next release note" {
|
||||
_mkfile "$ABRA_DIR/recipes/$TEST_RECIPE/release/next" "those are some release notes for the next release"
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" checkout main
|
||||
assert_success
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" add release/next
|
||||
assert_success
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" commit -m "added some release notes"
|
||||
assert_success
|
||||
|
||||
run $ABRA recipe release "$TEST_RECIPE" --no-input --minor
|
||||
run $ABRA recipe release "$TEST_RECIPE" --no-input --minor
|
||||
assert_success
|
||||
assert_output --partial 'no -p/--publish passed, not publishing'
|
||||
assert_output --partial 'new release published:'
|
||||
|
||||
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/release/next"
|
||||
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/release/0.4.0+1.21.0"
|
||||
assert_file_contains "$ABRA_DIR/recipes/$TEST_RECIPE/release/0.4.0+1.21.0" "those are some release notes for the next release"
|
||||
}
|
||||
|
||||
@test "recipe release conflict fails" {
|
||||
tagHash=$(_get_tag_hash "0.2.0+1.21.0")
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" checkout "$tagHash"
|
||||
assert_success
|
||||
|
||||
run sed -i "s/nginx:1.21.0/nginx:1.29.1/g" "$ABRA_DIR/recipes/$TEST_RECIPE/compose.yml"
|
||||
assert_success
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" diff
|
||||
assert_success
|
||||
assert_output --regexp 'nginx:1.29.1'
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" commit -am "updated nginx"
|
||||
assert_success
|
||||
|
||||
run $ABRA recipe release "$TEST_RECIPE" --no-input "0.2.0+1.29.1"
|
||||
assert_failure
|
||||
assert_output --partial '0.2.0+... conflicts with a previous release: 0.2.0+1.21.0'
|
||||
}
|
||||
|
||||
@test "error if recipe release --no-input and no initial version" {
|
||||
_remove_tags
|
||||
|
||||
run $ABRA recipe release "$TEST_RECIPE" --no-input --patch
|
||||
assert_failure
|
||||
assert_output --partial 'unable to continue'
|
||||
assert_output --partial 'initial version'
|
||||
}
|
||||
|
||||
@test "recipe release without input fails with prompt" {
|
||||
run $ABRA recipe new foobar
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/recipes/foobar"
|
||||
|
||||
run $ABRA recipe release foobar --no-input --patch
|
||||
assert_failure
|
||||
assert_output --partial "input required for initial version"
|
||||
}
|
||||
|
||||
@test "release new recipe: fail without input" {
|
||||
run $ABRA recipe new foobar
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/recipes/foobar"
|
||||
|
||||
run bash -c "$ABRA recipe release foobar --no-input"
|
||||
assert_failure
|
||||
assert_output --partial 'unable to continue, input required for initial version'
|
||||
}
|
||||
|
||||
# note: piping 0.1.0 from stdin is not testable right now because release notes also wants input
|
||||
# survey lib used for prompts breaks multi-line stdin for multi-prompt
|
||||
@test "release new recipe: development release" {
|
||||
run $ABRA recipe new foobar
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/recipes/foobar"
|
||||
|
||||
# fake origin
|
||||
git clone "$ABRA_DIR/recipes/foobar" "$ABRA_DIR/origin-recipes/foobar.git" --bare
|
||||
assert_success
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/foobar" remote add origin-ssh "$ABRA_DIR/origin-recipes/foobar.git"
|
||||
assert_success
|
||||
|
||||
run bash -c "$ABRA recipe release foobar 0.1.0+1.2.0 --no-input"
|
||||
assert_success
|
||||
assert_output --regexp 'coop-cloud\.\$\{STACK_NAME\}\.version=0\.1\.0\+1\.2.*'
|
||||
}
|
||||
|
||||
@test "release newly created recipe with no version label" {
|
||||
run $ABRA recipe new foobar
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/recipes/foobar"
|
||||
|
||||
run sed -i 's/- "coop-cloud.${STACK_NAME}.version="/#- "coop-cloud.${STACK_NAME}.version="/g' \
|
||||
"$ABRA_DIR/recipes/foobar/compose.yml"
|
||||
assert_success
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/foobar" commit -am "updated nginx"
|
||||
assert_success
|
||||
|
||||
run bash -c "echo 0.1.0 | $ABRA recipe release foobar --patch"
|
||||
assert_failure
|
||||
assert_output --partial "automagic insertion not supported yet"
|
||||
}
|
||||
|
||||
@ -1,128 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
setup_file(){
|
||||
load "$PWD/tests/integration/helpers/common"
|
||||
_common_setup
|
||||
_add_server
|
||||
_new_app
|
||||
}
|
||||
|
||||
teardown_file(){
|
||||
_rm_server
|
||||
}
|
||||
|
||||
setup(){
|
||||
load "$PWD/tests/integration/helpers/common"
|
||||
_common_setup
|
||||
}
|
||||
|
||||
teardown(){
|
||||
_reset_recipe
|
||||
_reset_tags
|
||||
}
|
||||
|
||||
@test "validate recipe argument" {
|
||||
run $ABRA recipe sync --no-input
|
||||
assert_failure
|
||||
|
||||
run $ABRA recipe sync DOESNTEXIST --no-input
|
||||
assert_failure
|
||||
}
|
||||
|
||||
@test "allow unstaged changes" {
|
||||
run echo "unstaged changes" >> "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
assert_success
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
|
||||
assert_success
|
||||
assert_output --partial 'foo'
|
||||
|
||||
run $ABRA recipe sync "$TEST_RECIPE" --no-input --patch
|
||||
assert_success
|
||||
|
||||
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
assert_equal "$(_git_status)" "M compose.yml ?? foo"
|
||||
|
||||
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
assert_success
|
||||
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
}
|
||||
|
||||
@test "detect unstaged label changes" {
|
||||
run $ABRA recipe fetch "$TEST_RECIPE"
|
||||
assert_success
|
||||
|
||||
run $ABRA recipe sync "$TEST_RECIPE" --patch
|
||||
assert_success
|
||||
|
||||
run $ABRA recipe sync "$TEST_RECIPE" --patch
|
||||
assert_success
|
||||
assert_output --partial 'is already set, nothing to do?'
|
||||
}
|
||||
|
||||
@test "sync patch label bump" {
|
||||
run $ABRA recipe upgrade "$TEST_RECIPE" --no-input --patch
|
||||
assert_success
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" diff
|
||||
assert_success
|
||||
assert_output --partial 'image: nginx:1.21.6'
|
||||
|
||||
# NOTE(d1): ensure the latest tag is the one we expect
|
||||
_remove_tags
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag \
|
||||
-a "0.3.0+1.21.0" -m "fake: 0.3.0+1.21.0"
|
||||
assert_success
|
||||
|
||||
run $ABRA recipe sync "$TEST_RECIPE" --no-input --patch
|
||||
assert_success
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" diff
|
||||
assert_success
|
||||
assert_output --regexp 'coop-cloud\.\$\{STACK_NAME\}\.version=0\.3\.1\+1\.2.*'
|
||||
}
|
||||
|
||||
@test "sync minor label bump" {
|
||||
run $ABRA recipe upgrade "$TEST_RECIPE" --no-input --minor
|
||||
assert_success
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" diff
|
||||
assert_success
|
||||
assert_output --regexp 'image: nginx:1.2.*'
|
||||
|
||||
# NOTE(d1): ensure the latest tag is the one we expect
|
||||
_remove_tags
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag \
|
||||
-a "0.3.0+1.21.0" -m "fake: 0.3.0+1.21.0"
|
||||
assert_success
|
||||
|
||||
run $ABRA recipe sync "$TEST_RECIPE" --no-input --minor
|
||||
assert_success
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" diff
|
||||
assert_success
|
||||
assert_output --regexp 'coop-cloud\.\$\{STACK_NAME\}\.version=0\.4\.0\+1\.2.*'
|
||||
}
|
||||
|
||||
@test "error if --no-input and no initial version" {
|
||||
_remove_tags
|
||||
|
||||
run $ABRA recipe sync "$TEST_RECIPE" --no-input --patch
|
||||
assert_failure
|
||||
assert_output --partial 'unable to continue'
|
||||
assert_output --partial 'initial version'
|
||||
}
|
||||
|
||||
@test "output label sync only once" {
|
||||
run $ABRA recipe upgrade "$TEST_RECIPE" --no-input --minor
|
||||
assert_success
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" diff
|
||||
assert_success
|
||||
assert_output --regexp 'image: nginx:1.2.*'
|
||||
|
||||
run $ABRA recipe sync "$TEST_RECIPE" --no-input --minor
|
||||
assert_success
|
||||
assert_line --index 0 --partial 'synced label'
|
||||
refute_line --index 1 --partial 'synced label'
|
||||
}
|
||||
@ -106,3 +106,37 @@ teardown(){
|
||||
assert_success
|
||||
assert_output --regexp 'image: nginx:1.2.*'
|
||||
}
|
||||
|
||||
@test "upgrade and commit" {
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" rev-list --count HEAD
|
||||
assert_success
|
||||
expected_count="$((output + 1))"
|
||||
|
||||
run $ABRA recipe upgrade "$TEST_RECIPE" --no-input --minor --commit
|
||||
assert_success
|
||||
assert_output --partial 'committed changes as'
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" diff --quiet
|
||||
assert_success
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" rev-list --count HEAD
|
||||
assert_success
|
||||
assert_output "$expected_count"
|
||||
}
|
||||
|
||||
@test "upgrade nothing, skip commit" {
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" rev-list --count HEAD
|
||||
assert_success
|
||||
expected_count="$output"
|
||||
|
||||
run $ABRA recipe upgrade "$TEST_RECIPE" --no-input --commit
|
||||
assert_success
|
||||
assert_output --partial "no changes, skip creating commit"
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" diff --quiet
|
||||
assert_success
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" rev-list --count HEAD
|
||||
assert_success
|
||||
assert_output "$expected_count"
|
||||
}
|
||||
|
||||
@ -12,11 +12,7 @@ setup_suite(){
|
||||
fi
|
||||
|
||||
if [[ ! -f "$PWD/abra" ]]; then
|
||||
make build-abra
|
||||
fi
|
||||
|
||||
if [[ ! -f "$PWD/kadabra" ]]; then
|
||||
make build-kadabra
|
||||
make
|
||||
fi
|
||||
|
||||
if [[ -d "$ABRA_DIR" ]]; then
|
||||
|
||||
4
vendor/github.com/charmbracelet/bubbletea/.golangci.yml
generated
vendored
4
vendor/github.com/charmbracelet/bubbletea/.golangci.yml
generated
vendored
@ -26,6 +26,10 @@ linters:
|
||||
- whitespace
|
||||
- wrapcheck
|
||||
exclusions:
|
||||
rules:
|
||||
- text: '(slog|log)\.\w+'
|
||||
linters:
|
||||
- noctx
|
||||
generated: lax
|
||||
presets:
|
||||
- common-false-positives
|
||||
|
||||
2
vendor/github.com/charmbracelet/bubbletea/LICENSE
generated
vendored
2
vendor/github.com/charmbracelet/bubbletea/LICENSE
generated
vendored
@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020-2023 Charmbracelet, Inc
|
||||
Copyright (c) 2020-2025 Charmbracelet, Inc
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
4
vendor/github.com/charmbracelet/bubbletea/README.md
generated
vendored
4
vendor/github.com/charmbracelet/bubbletea/README.md
generated
vendored
@ -9,7 +9,7 @@
|
||||
<br>
|
||||
<a href="https://github.com/charmbracelet/bubbletea/releases"><img src="https://img.shields.io/github/release/charmbracelet/bubbletea.svg" alt="Latest Release"></a>
|
||||
<a href="https://pkg.go.dev/github.com/charmbracelet/bubbletea?tab=doc"><img src="https://godoc.org/github.com/charmbracelet/bubbletea?status.svg" alt="GoDoc"></a>
|
||||
<a href="https://github.com/charmbracelet/bubbletea/actions"><img src="https://github.com/charmbracelet/bubbletea/actions/workflows/build.yml/badge.svg" alt="Build Status"></a>
|
||||
<a href="https://github.com/charmbracelet/bubbletea/actions"><img src="https://github.com/charmbracelet/bubbletea/actions/workflows/build.yml/badge.svg?branch=main" alt="Build Status"></a>
|
||||
</p>
|
||||
|
||||
The fun, functional and stateful way to build terminal apps. A Go framework
|
||||
@ -395,6 +395,6 @@ of days past.
|
||||
|
||||
Part of [Charm](https://charm.sh).
|
||||
|
||||
<a href="https://charm.sh/"><img alt="The Charm logo" src="https://stuff.charm.sh/charm-badge.jpg" width="400"></a>
|
||||
<a href="https://charm.sh/"><img alt="The Charm logo" src="https://stuff.charm.sh/charm-banner-next.jpg" width="400"></a>
|
||||
|
||||
Charm热爱开源 • Charm loves open source • نحنُ نحب المصادر المفتوحة
|
||||
|
||||
38
vendor/github.com/charmbracelet/bubbletea/commands.go
generated
vendored
38
vendor/github.com/charmbracelet/bubbletea/commands.go
generated
vendored
@ -13,6 +13,27 @@ import (
|
||||
// return tea.Batch(someCommand, someOtherCommand)
|
||||
// }
|
||||
func Batch(cmds ...Cmd) Cmd {
|
||||
return compactCmds[BatchMsg](cmds)
|
||||
}
|
||||
|
||||
// BatchMsg is a message used to perform a bunch of commands concurrently with
|
||||
// no ordering guarantees. You can send a BatchMsg with Batch.
|
||||
type BatchMsg []Cmd
|
||||
|
||||
// Sequence runs the given commands one at a time, in order. Contrast this with
|
||||
// Batch, which runs commands concurrently.
|
||||
func Sequence(cmds ...Cmd) Cmd {
|
||||
return compactCmds[sequenceMsg](cmds)
|
||||
}
|
||||
|
||||
// sequenceMsg is used internally to run the given commands in order.
|
||||
type sequenceMsg []Cmd
|
||||
|
||||
// compactCmds ignores any nil commands in cmds, and returns the most direct
|
||||
// command possible. That is, considering the non-nil commands, if there are
|
||||
// none it returns nil, if there is exactly one it returns that command
|
||||
// directly, else it returns the non-nil commands as type T.
|
||||
func compactCmds[T ~[]Cmd](cmds []Cmd) Cmd {
|
||||
var validCmds []Cmd //nolint:prealloc
|
||||
for _, c := range cmds {
|
||||
if c == nil {
|
||||
@ -27,26 +48,11 @@ func Batch(cmds ...Cmd) Cmd {
|
||||
return validCmds[0]
|
||||
default:
|
||||
return func() Msg {
|
||||
return BatchMsg(validCmds)
|
||||
return T(validCmds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BatchMsg is a message used to perform a bunch of commands concurrently with
|
||||
// no ordering guarantees. You can send a BatchMsg with Batch.
|
||||
type BatchMsg []Cmd
|
||||
|
||||
// Sequence runs the given commands one at a time, in order. Contrast this with
|
||||
// Batch, which runs commands concurrently.
|
||||
func Sequence(cmds ...Cmd) Cmd {
|
||||
return func() Msg {
|
||||
return sequenceMsg(cmds)
|
||||
}
|
||||
}
|
||||
|
||||
// sequenceMsg is used internally to run the given commands in order.
|
||||
type sequenceMsg []Cmd
|
||||
|
||||
// Every is a command that ticks in sync with the system clock. So, if you
|
||||
// wanted to tick with the system clock every second, minute or hour you
|
||||
// could use this. It's also handy for having different things tick in sync.
|
||||
|
||||
2
vendor/github.com/charmbracelet/bubbletea/inputreader_windows.go
generated
vendored
2
vendor/github.com/charmbracelet/bubbletea/inputreader_windows.go
generated
vendored
@ -108,7 +108,7 @@ func prepareConsole(input windows.Handle, modes ...uint32) (originalMode uint32,
|
||||
return originalMode, nil
|
||||
}
|
||||
|
||||
// cancelMixin represents a goroutine-safe cancelation status.
|
||||
// cancelMixin represents a goroutine-safe cancellation status.
|
||||
type cancelMixin struct {
|
||||
unsafeCanceled bool
|
||||
lock sync.Mutex
|
||||
|
||||
16
vendor/github.com/charmbracelet/bubbletea/key_windows.go
generated
vendored
16
vendor/github.com/charmbracelet/bubbletea/key_windows.go
generated
vendored
@ -109,12 +109,12 @@ func peekAndReadConsInput(con *conInputReader) ([]coninput.InputRecord, error) {
|
||||
return events, nil
|
||||
}
|
||||
|
||||
// Convert i to unit32 or panic if it cannot be converted. Check satisifes lint G115.
|
||||
// Convert i to unit32 or panic if it cannot be converted. Check satisfies lint G115.
|
||||
func intToUint32OrDie(i int) uint32 {
|
||||
if i < 0 {
|
||||
panic("cannot convert numEvents " + fmt.Sprint(i) + " to uint32")
|
||||
}
|
||||
return uint32(i)
|
||||
return uint32(i) //nolint:gosec
|
||||
}
|
||||
|
||||
// Keeps peeking until there is data or the input is cancelled.
|
||||
@ -158,16 +158,16 @@ func mouseEventButton(p, s coninput.ButtonState) (button MouseButton, action Mou
|
||||
return button, action
|
||||
}
|
||||
|
||||
switch {
|
||||
case btn == coninput.FROM_LEFT_1ST_BUTTON_PRESSED: // left button
|
||||
switch btn {
|
||||
case coninput.FROM_LEFT_1ST_BUTTON_PRESSED: // left button
|
||||
button = MouseButtonLeft
|
||||
case btn == coninput.RIGHTMOST_BUTTON_PRESSED: // right button
|
||||
case coninput.RIGHTMOST_BUTTON_PRESSED: // right button
|
||||
button = MouseButtonRight
|
||||
case btn == coninput.FROM_LEFT_2ND_BUTTON_PRESSED: // middle button
|
||||
case coninput.FROM_LEFT_2ND_BUTTON_PRESSED: // middle button
|
||||
button = MouseButtonMiddle
|
||||
case btn == coninput.FROM_LEFT_3RD_BUTTON_PRESSED: // unknown (possibly mouse backward)
|
||||
case coninput.FROM_LEFT_3RD_BUTTON_PRESSED: // unknown (possibly mouse backward)
|
||||
button = MouseButtonBackward
|
||||
case btn == coninput.FROM_LEFT_4TH_BUTTON_PRESSED: // unknown (possibly mouse forward)
|
||||
case coninput.FROM_LEFT_4TH_BUTTON_PRESSED: // unknown (possibly mouse forward)
|
||||
button = MouseButtonForward
|
||||
}
|
||||
|
||||
|
||||
2
vendor/github.com/charmbracelet/bubbletea/screen.go
generated
vendored
2
vendor/github.com/charmbracelet/bubbletea/screen.go
generated
vendored
@ -131,7 +131,7 @@ func EnableBracketedPaste() Msg {
|
||||
type enableBracketedPasteMsg struct{}
|
||||
|
||||
// DisableBracketedPaste is a special command that tells the Bubble Tea program
|
||||
// to accept bracketed paste input.
|
||||
// to stop processing bracketed paste input.
|
||||
//
|
||||
// Note that bracketed paste will be automatically disabled when the
|
||||
// program quits.
|
||||
|
||||
2
vendor/github.com/charmbracelet/bubbletea/standard_renderer.go
generated
vendored
2
vendor/github.com/charmbracelet/bubbletea/standard_renderer.go
generated
vendored
@ -277,7 +277,7 @@ func (r *standardRenderer) flush() {
|
||||
// using the full terminal window.
|
||||
buf.WriteString(ansi.CursorPosition(0, len(newLines)))
|
||||
} else {
|
||||
buf.WriteString(ansi.CursorBackward(r.width))
|
||||
buf.WriteByte('\r')
|
||||
}
|
||||
|
||||
_, _ = r.out.Write(buf.Bytes())
|
||||
|
||||
109
vendor/github.com/charmbracelet/bubbletea/tea.go
generated
vendored
109
vendor/github.com/charmbracelet/bubbletea/tea.go
generated
vendored
@ -24,7 +24,6 @@ import (
|
||||
|
||||
"github.com/charmbracelet/x/term"
|
||||
"github.com/muesli/cancelreader"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
// ErrProgramPanic is returned by [Program.Run] when the program recovers from a panic.
|
||||
@ -73,7 +72,7 @@ const (
|
||||
customInput
|
||||
)
|
||||
|
||||
// String implements the stringer interface for [inputType]. It is inteded to
|
||||
// String implements the stringer interface for [inputType]. It is intended to
|
||||
// be used in testing.
|
||||
func (i inputType) String() string {
|
||||
return [...]string{
|
||||
@ -220,7 +219,7 @@ func Suspend() Msg {
|
||||
// You can send this message with [Suspend()].
|
||||
type SuspendMsg struct{}
|
||||
|
||||
// ResumeMsg can be listen to to do something once a program is resumed back
|
||||
// ResumeMsg can be listen to do something once a program is resumed back
|
||||
// from a suspend state.
|
||||
type ResumeMsg struct{}
|
||||
|
||||
@ -472,42 +471,12 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
|
||||
p.exec(msg.cmd, msg.fn)
|
||||
|
||||
case BatchMsg:
|
||||
for _, cmd := range msg {
|
||||
select {
|
||||
case <-p.ctx.Done():
|
||||
return model, nil
|
||||
case cmds <- cmd:
|
||||
}
|
||||
}
|
||||
go p.execBatchMsg(msg)
|
||||
continue
|
||||
|
||||
case sequenceMsg:
|
||||
go func() {
|
||||
// Execute commands one at a time, in order.
|
||||
for _, cmd := range msg {
|
||||
if cmd == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
msg := cmd()
|
||||
if batchMsg, ok := msg.(BatchMsg); ok {
|
||||
g, _ := errgroup.WithContext(p.ctx)
|
||||
for _, cmd := range batchMsg {
|
||||
cmd := cmd
|
||||
g.Go(func() error {
|
||||
p.Send(cmd())
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
//nolint:errcheck,gosec
|
||||
g.Wait() // wait for all commands from batch msg to finish
|
||||
continue
|
||||
}
|
||||
|
||||
p.Send(msg)
|
||||
}
|
||||
}()
|
||||
go p.execSequenceMsg(msg)
|
||||
continue
|
||||
|
||||
case setWindowTitleMsg:
|
||||
p.SetWindowTitle(string(msg))
|
||||
@ -535,6 +504,74 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Program) execSequenceMsg(msg sequenceMsg) {
|
||||
if !p.startupOptions.has(withoutCatchPanics) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
p.recoverFromGoPanic(r)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Execute commands one at a time, in order.
|
||||
for _, cmd := range msg {
|
||||
if cmd == nil {
|
||||
continue
|
||||
}
|
||||
msg := cmd()
|
||||
switch msg := msg.(type) {
|
||||
case BatchMsg:
|
||||
p.execBatchMsg(msg)
|
||||
case sequenceMsg:
|
||||
p.execSequenceMsg(msg)
|
||||
default:
|
||||
p.Send(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Program) execBatchMsg(msg BatchMsg) {
|
||||
if !p.startupOptions.has(withoutCatchPanics) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
p.recoverFromGoPanic(r)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Execute commands one at a time.
|
||||
var wg sync.WaitGroup
|
||||
for _, cmd := range msg {
|
||||
if cmd == nil {
|
||||
continue
|
||||
}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
if !p.startupOptions.has(withoutCatchPanics) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
p.recoverFromGoPanic(r)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
msg := cmd()
|
||||
switch msg := msg.(type) {
|
||||
case BatchMsg:
|
||||
p.execBatchMsg(msg)
|
||||
case sequenceMsg:
|
||||
p.execSequenceMsg(msg)
|
||||
default:
|
||||
p.Send(msg)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait() // wait for all commands from batch msg to finish
|
||||
}
|
||||
|
||||
// Run initializes the program and runs its event loops, blocking until it gets
|
||||
// terminated by either [Program.Quit], [Program.Kill], or its signal handler.
|
||||
// Returns the final model.
|
||||
|
||||
2
vendor/github.com/charmbracelet/bubbletea/tty_windows.go
generated
vendored
2
vendor/github.com/charmbracelet/bubbletea/tty_windows.go
generated
vendored
@ -56,7 +56,7 @@ func (p *Program) initInput() (err error) {
|
||||
|
||||
// Open the Windows equivalent of a TTY.
|
||||
func openInputTTY() (*os.File, error) {
|
||||
f, err := os.OpenFile("CONIN$", os.O_RDWR, 0o644)
|
||||
f, err := os.OpenFile("CONIN$", os.O_RDWR, 0o644) //nolint:gosec
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error opening file: %w", err)
|
||||
}
|
||||
|
||||
24
vendor/github.com/charmbracelet/x/ansi/inband.go
generated
vendored
Normal file
24
vendor/github.com/charmbracelet/x/ansi/inband.go
generated
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
package ansi
|
||||
|
||||
import "fmt"
|
||||
|
||||
// InBandResize encodes an in-band terminal resize event sequence.
|
||||
//
|
||||
// CSI 48 ; height_cells ; widht_cells ; height_pixels ; width_pixels t
|
||||
//
|
||||
// See https://gist.github.com/rockorager/e695fb2924d36b2bcf1fff4a3704bd83
|
||||
func InBandResize(heightCells, widthCells, heightPixels, widthPixels int) string {
|
||||
if heightCells < 0 {
|
||||
heightCells = 0
|
||||
}
|
||||
if widthCells < 0 {
|
||||
widthCells = 0
|
||||
}
|
||||
if heightPixels < 0 {
|
||||
heightPixels = 0
|
||||
}
|
||||
if widthPixels < 0 {
|
||||
widthPixels = 0
|
||||
}
|
||||
return fmt.Sprintf("\x1b[48;%d;%d;%d;%dt", heightCells, widthCells, heightPixels, widthPixels)
|
||||
}
|
||||
34
vendor/github.com/charmbracelet/x/ansi/palette.go
generated
vendored
Normal file
34
vendor/github.com/charmbracelet/x/ansi/palette.go
generated
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
package ansi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
)
|
||||
|
||||
// SetPalette sets the palette color for the given index. The index is a 16
|
||||
// color index between 0 and 15. The color is a 24-bit RGB color.
|
||||
//
|
||||
// OSC P n rrggbb BEL
|
||||
//
|
||||
// Where n is the color index in hex (0-f), and rrggbb is the color in
|
||||
// hexadecimal format (e.g., ff0000 for red).
|
||||
//
|
||||
// This sequence is specific to the Linux Console and may not work in other
|
||||
// terminal emulators.
|
||||
//
|
||||
// See https://man7.org/linux/man-pages/man4/console_codes.4.html
|
||||
func SetPalette(i int, c color.Color) string {
|
||||
if c == nil || i < 0 || i > 15 {
|
||||
return ""
|
||||
}
|
||||
r, g, b, _ := c.RGBA()
|
||||
return fmt.Sprintf("\x1b]P%x%02x%02x%02x\x07", i, r>>8, g>>8, b>>8)
|
||||
}
|
||||
|
||||
// ResetPalette resets the color palette to the default values.
|
||||
//
|
||||
// This sequence is specific to the Linux Console and may not work in other
|
||||
// terminal emulators.
|
||||
//
|
||||
// See https://man7.org/linux/man-pages/man4/console_codes.4.html
|
||||
const ResetPalette = "\x1b]R\x07"
|
||||
49
vendor/github.com/charmbracelet/x/ansi/progress.go
generated
vendored
Normal file
49
vendor/github.com/charmbracelet/x/ansi/progress.go
generated
vendored
Normal file
@ -0,0 +1,49 @@
|
||||
package ansi
|
||||
|
||||
import "strconv"
|
||||
|
||||
// ResetProgressBar is a sequence that resets the progress bar to its default
|
||||
// state (hidden).
|
||||
//
|
||||
// OSC 9 ; 4 ; 0 BEL
|
||||
//
|
||||
// See: https://learn.microsoft.com/en-us/windows/terminal/tutorials/progress-bar-sequences
|
||||
const ResetProgressBar = "\x1b]9;4;0\x07"
|
||||
|
||||
// SetProgressBar returns a sequence for setting the progress bar to a specific
|
||||
// percentage (0-100) in the "default" state.
|
||||
//
|
||||
// OSC 9 ; 4 ; 1 Percentage BEL
|
||||
//
|
||||
// See: https://learn.microsoft.com/en-us/windows/terminal/tutorials/progress-bar-sequences
|
||||
func SetProgressBar(percentage int) string {
|
||||
return "\x1b]9;4;1;" + strconv.Itoa(min(max(0, percentage), 100)) + "\x07"
|
||||
}
|
||||
|
||||
// SetErrorProgressBar returns a sequence for setting the progress bar to a
|
||||
// specific percentage (0-100) in the "Error" state..
|
||||
//
|
||||
// OSC 9 ; 4 ; 2 Percentage BEL
|
||||
//
|
||||
// See: https://learn.microsoft.com/en-us/windows/terminal/tutorials/progress-bar-sequences
|
||||
func SetErrorProgressBar(percentage int) string {
|
||||
return "\x1b]9;4;2;" + strconv.Itoa(min(max(0, percentage), 100)) + "\x07"
|
||||
}
|
||||
|
||||
// SetIndeterminateProgressBar is a sequence that sets the progress bar to the
|
||||
// indeterminate state.
|
||||
//
|
||||
// OSC 9 ; 4 ; 3 BEL
|
||||
//
|
||||
// See: https://learn.microsoft.com/en-us/windows/terminal/tutorials/progress-bar-sequences
|
||||
const SetIndeterminateProgressBar = "\x1b]9;4;3\x07"
|
||||
|
||||
// SetWarningProgressBar is a sequence that sets the progress bar to the
|
||||
// "Warning" state.
|
||||
//
|
||||
// OSC 9 ; 4 ; 4 Percentage BEL
|
||||
//
|
||||
// See: https://learn.microsoft.com/en-us/windows/terminal/tutorials/progress-bar-sequences
|
||||
func SetWarningProgressBar(percentage int) string {
|
||||
return "\x1b]9;4;4;" + strconv.Itoa(min(max(0, percentage), 100)) + "\x07"
|
||||
}
|
||||
6
vendor/github.com/charmbracelet/x/ansi/winop.go
generated
vendored
6
vendor/github.com/charmbracelet/x/ansi/winop.go
generated
vendored
@ -8,16 +8,22 @@ import (
|
||||
const (
|
||||
// ResizeWindowWinOp is a window operation that resizes the terminal
|
||||
// window.
|
||||
//
|
||||
// Deprecated: Use constant number directly with [WindowOp].
|
||||
ResizeWindowWinOp = 4
|
||||
|
||||
// RequestWindowSizeWinOp is a window operation that requests a report of
|
||||
// the size of the terminal window in pixels. The response is in the form:
|
||||
// CSI 4 ; height ; width t
|
||||
//
|
||||
// Deprecated: Use constant number directly with [WindowOp].
|
||||
RequestWindowSizeWinOp = 14
|
||||
|
||||
// RequestCellSizeWinOp is a window operation that requests a report of
|
||||
// the size of the terminal cell size in pixels. The response is in the form:
|
||||
// CSI 6 ; height ; width t
|
||||
//
|
||||
// Deprecated: Use constant number directly with [WindowOp].
|
||||
RequestCellSizeWinOp = 16
|
||||
)
|
||||
|
||||
|
||||
21
vendor/github.com/clipperhouse/uax29/v2/LICENSE
generated
vendored
Normal file
21
vendor/github.com/clipperhouse/uax29/v2/LICENSE
generated
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Matt Sherman
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
82
vendor/github.com/clipperhouse/uax29/v2/graphemes/README.md
generated
vendored
Normal file
82
vendor/github.com/clipperhouse/uax29/v2/graphemes/README.md
generated
vendored
Normal file
@ -0,0 +1,82 @@
|
||||
An implementation of grapheme cluster boundaries from [Unicode text segmentation](https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries) (UAX 29), for Unicode version 15.0.0.
|
||||
|
||||
## Quick start
|
||||
|
||||
```
|
||||
go get "github.com/clipperhouse/uax29/v2/graphemes"
|
||||
```
|
||||
|
||||
```go
|
||||
import "github.com/clipperhouse/uax29/v2/graphemes"
|
||||
|
||||
text := "Hello, 世界. Nice dog! 👍🐶"
|
||||
|
||||
tokens := graphemes.FromString(text)
|
||||
|
||||
for tokens.Next() { // Next() returns true until end of data
|
||||
fmt.Println(tokens.Value()) // Do something with the current grapheme
|
||||
}
|
||||
```
|
||||
|
||||
[](https://pkg.go.dev/github.com/clipperhouse/uax29/v2/graphemes)
|
||||
|
||||
_A grapheme is a “single visible character”, which might be a simple as a single letter, or a complex emoji that consists of several Unicode code points._
|
||||
|
||||
## Conformance
|
||||
|
||||
We use the Unicode [test suite](https://unicode.org/reports/tr41/tr41-26.html#Tests29). Status:
|
||||
|
||||

|
||||
|
||||
## APIs
|
||||
|
||||
### If you have a `string`
|
||||
|
||||
```go
|
||||
text := "Hello, 世界. Nice dog! 👍🐶"
|
||||
|
||||
tokens := graphemes.FromString(text)
|
||||
|
||||
for tokens.Next() { // Next() returns true until end of data
|
||||
fmt.Println(tokens.Value()) // Do something with the current grapheme
|
||||
}
|
||||
```
|
||||
|
||||
### If you have an `io.Reader`
|
||||
|
||||
`FromReader` embeds a [`bufio.Scanner`](https://pkg.go.dev/bufio#Scanner), so just use those methods.
|
||||
|
||||
```go
|
||||
r := getYourReader() // from a file or network maybe
|
||||
tokens := graphemes.FromReader(r)
|
||||
|
||||
for tokens.Scan() { // Scan() returns true until error or EOF
|
||||
fmt.Println(tokens.Text()) // Do something with the current grapheme
|
||||
}
|
||||
|
||||
if tokens.Err() != nil { // Check the error
|
||||
log.Fatal(tokens.Err())
|
||||
}
|
||||
```
|
||||
|
||||
### If you have a `[]byte`
|
||||
|
||||
```go
|
||||
b := []byte("Hello, 世界. Nice dog! 👍🐶")
|
||||
|
||||
tokens := graphemes.FromBytes(b)
|
||||
|
||||
for tokens.Next() { // Next() returns true until end of data
|
||||
fmt.Println(tokens.Value()) // Do something with the current grapheme
|
||||
}
|
||||
```
|
||||
|
||||
### Performance
|
||||
|
||||
On a Mac M2 laptop, we see around 200MB/s, or around 100 million graphemes per second. You should see ~constant memory, and no allocations.
|
||||
|
||||
### Invalid inputs
|
||||
|
||||
Invalid UTF-8 input is considered undefined behavior. We test to ensure that bad inputs will not cause pathological outcomes, such as a panic or infinite loop. Callers should expect “garbage-in, garbage-out”.
|
||||
|
||||
Your pipeline should probably include a call to [`utf8.Valid()`](https://pkg.go.dev/unicode/utf8#Valid).
|
||||
28
vendor/github.com/clipperhouse/uax29/v2/graphemes/iterator.go
generated
vendored
Normal file
28
vendor/github.com/clipperhouse/uax29/v2/graphemes/iterator.go
generated
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
package graphemes
|
||||
|
||||
import "github.com/clipperhouse/uax29/v2/internal/iterators"
|
||||
|
||||
type Iterator[T iterators.Stringish] struct {
|
||||
*iterators.Iterator[T]
|
||||
}
|
||||
|
||||
var (
|
||||
splitFuncString = splitFunc[string]
|
||||
splitFuncBytes = splitFunc[[]byte]
|
||||
)
|
||||
|
||||
// FromString returns an iterator for the grapheme clusters in the input string.
|
||||
// Iterate while Next() is true, and access the grapheme via Value().
|
||||
func FromString(s string) Iterator[string] {
|
||||
return Iterator[string]{
|
||||
iterators.New(splitFuncString, s),
|
||||
}
|
||||
}
|
||||
|
||||
// FromBytes returns an iterator for the grapheme clusters in the input bytes.
|
||||
// Iterate while Next() is true, and access the grapheme via Value().
|
||||
func FromBytes(b []byte) Iterator[[]byte] {
|
||||
return Iterator[[]byte]{
|
||||
iterators.New(splitFuncBytes, b),
|
||||
}
|
||||
}
|
||||
25
vendor/github.com/clipperhouse/uax29/v2/graphemes/reader.go
generated
vendored
Normal file
25
vendor/github.com/clipperhouse/uax29/v2/graphemes/reader.go
generated
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
// Package graphemes implements Unicode grapheme cluster boundaries: https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries
|
||||
package graphemes
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
)
|
||||
|
||||
type Scanner struct {
|
||||
*bufio.Scanner
|
||||
}
|
||||
|
||||
// FromReader returns a Scanner, to split graphemes per
|
||||
// https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries.
|
||||
//
|
||||
// It embeds a [bufio.Scanner], so you can use its methods.
|
||||
//
|
||||
// Iterate through graphemes by calling Scan() until false, then check Err().
|
||||
func FromReader(r io.Reader) *Scanner {
|
||||
sc := bufio.NewScanner(r)
|
||||
sc.Split(SplitFunc)
|
||||
return &Scanner{
|
||||
Scanner: sc,
|
||||
}
|
||||
}
|
||||
174
vendor/github.com/clipperhouse/uax29/v2/graphemes/splitfunc.go
generated
vendored
Normal file
174
vendor/github.com/clipperhouse/uax29/v2/graphemes/splitfunc.go
generated
vendored
Normal file
@ -0,0 +1,174 @@
|
||||
package graphemes
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
|
||||
"github.com/clipperhouse/uax29/v2/internal/iterators"
|
||||
)
|
||||
|
||||
// is determines if lookup intersects propert(ies)
|
||||
func (lookup property) is(properties property) bool {
|
||||
return (lookup & properties) != 0
|
||||
}
|
||||
|
||||
const _Ignore = _Extend
|
||||
|
||||
// SplitFunc is a bufio.SplitFunc implementation of Unicode grapheme cluster segmentation, for use with bufio.Scanner.
|
||||
//
|
||||
// See https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries.
|
||||
var SplitFunc bufio.SplitFunc = splitFunc[[]byte]
|
||||
|
||||
func splitFunc[T iterators.Stringish](data T, atEOF bool) (advance int, token T, err error) {
|
||||
var empty T
|
||||
if len(data) == 0 {
|
||||
return 0, empty, nil
|
||||
}
|
||||
|
||||
// These vars are stateful across loop iterations
|
||||
var pos int
|
||||
var lastExIgnore property = 0 // "last excluding ignored categories"
|
||||
var lastLastExIgnore property = 0 // "last one before that"
|
||||
var regionalIndicatorCount int
|
||||
|
||||
// Rules are usually of the form Cat1 × Cat2; "current" refers to the first property
|
||||
// to the right of the ×, from which we look back or forward
|
||||
|
||||
current, w := lookup(data[pos:])
|
||||
if w == 0 {
|
||||
if !atEOF {
|
||||
// Rune extends past current data, request more
|
||||
return 0, empty, nil
|
||||
}
|
||||
pos = len(data)
|
||||
return pos, data[:pos], nil
|
||||
}
|
||||
|
||||
// https://unicode.org/reports/tr29/#GB1
|
||||
// Start of text always advances
|
||||
pos += w
|
||||
|
||||
for {
|
||||
eot := pos == len(data) // "end of text"
|
||||
|
||||
if eot {
|
||||
if !atEOF {
|
||||
// Token extends past current data, request more
|
||||
return 0, empty, nil
|
||||
}
|
||||
|
||||
// https://unicode.org/reports/tr29/#GB2
|
||||
break
|
||||
}
|
||||
|
||||
/*
|
||||
We've switched the evaluation order of GB1↓ and GB2↑. It's ok:
|
||||
because we've checked for len(data) at the top of this function,
|
||||
sot and eot are mutually exclusive, order doesn't matter.
|
||||
*/
|
||||
|
||||
// Rules are usually of the form Cat1 × Cat2; "current" refers to the first property
|
||||
// to the right of the ×, from which we look back or forward
|
||||
|
||||
// Remember previous properties to avoid lookups/lookbacks
|
||||
last := current
|
||||
if !last.is(_Ignore) {
|
||||
lastLastExIgnore = lastExIgnore
|
||||
lastExIgnore = last
|
||||
}
|
||||
|
||||
current, w = lookup(data[pos:])
|
||||
if w == 0 {
|
||||
if atEOF {
|
||||
// Just return the bytes, we can't do anything with them
|
||||
pos = len(data)
|
||||
break
|
||||
}
|
||||
// Rune extends past current data, request more
|
||||
return 0, empty, nil
|
||||
}
|
||||
|
||||
// Optimization: no rule can possibly apply
|
||||
if current|last == 0 { // i.e. both are zero
|
||||
break
|
||||
}
|
||||
|
||||
// https://unicode.org/reports/tr29/#GB3
|
||||
if current.is(_LF) && last.is(_CR) {
|
||||
pos += w
|
||||
continue
|
||||
}
|
||||
|
||||
// https://unicode.org/reports/tr29/#GB4
|
||||
// https://unicode.org/reports/tr29/#GB5
|
||||
if (current | last).is(_Control | _CR | _LF) {
|
||||
break
|
||||
}
|
||||
|
||||
// https://unicode.org/reports/tr29/#GB6
|
||||
if current.is(_L|_V|_LV|_LVT) && last.is(_L) {
|
||||
pos += w
|
||||
continue
|
||||
}
|
||||
|
||||
// https://unicode.org/reports/tr29/#GB7
|
||||
if current.is(_V|_T) && last.is(_LV|_V) {
|
||||
pos += w
|
||||
continue
|
||||
}
|
||||
|
||||
// https://unicode.org/reports/tr29/#GB8
|
||||
if current.is(_T) && last.is(_LVT|_T) {
|
||||
pos += w
|
||||
continue
|
||||
}
|
||||
|
||||
// https://unicode.org/reports/tr29/#GB9
|
||||
if current.is(_Extend | _ZWJ) {
|
||||
pos += w
|
||||
continue
|
||||
}
|
||||
|
||||
// https://unicode.org/reports/tr29/#GB9a
|
||||
if current.is(_SpacingMark) {
|
||||
pos += w
|
||||
continue
|
||||
}
|
||||
|
||||
// https://unicode.org/reports/tr29/#GB9b
|
||||
if last.is(_Prepend) {
|
||||
pos += w
|
||||
continue
|
||||
}
|
||||
|
||||
// https://unicode.org/reports/tr29/#GB9c
|
||||
// TODO(clipperhouse):
|
||||
// It appears to be added in Unicode 15.1.0:
|
||||
// https://unicode.org/versions/Unicode15.1.0/#Migration
|
||||
// This package currently supports Unicode 15.0.0, so
|
||||
// out of scope for now
|
||||
|
||||
// https://unicode.org/reports/tr29/#GB11
|
||||
if current.is(_ExtendedPictographic) && last.is(_ZWJ) && lastLastExIgnore.is(_ExtendedPictographic) {
|
||||
pos += w
|
||||
continue
|
||||
}
|
||||
|
||||
// https://unicode.org/reports/tr29/#GB12
|
||||
// https://unicode.org/reports/tr29/#GB13
|
||||
if (current & last).is(_RegionalIndicator) {
|
||||
regionalIndicatorCount++
|
||||
|
||||
odd := regionalIndicatorCount%2 == 1
|
||||
if odd {
|
||||
pos += w
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// If we fall through all the above rules, it's a grapheme cluster break
|
||||
break
|
||||
}
|
||||
|
||||
// Return token
|
||||
return pos, data[:pos], nil
|
||||
}
|
||||
1409
vendor/github.com/clipperhouse/uax29/v2/graphemes/trie.go
generated
vendored
Normal file
1409
vendor/github.com/clipperhouse/uax29/v2/graphemes/trie.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
85
vendor/github.com/clipperhouse/uax29/v2/internal/iterators/iterator.go
generated
vendored
Normal file
85
vendor/github.com/clipperhouse/uax29/v2/internal/iterators/iterator.go
generated
vendored
Normal file
@ -0,0 +1,85 @@
|
||||
package iterators
|
||||
|
||||
type Stringish interface {
|
||||
[]byte | string
|
||||
}
|
||||
|
||||
type SplitFunc[T Stringish] func(T, bool) (int, T, error)
|
||||
|
||||
// Iterator is a generic iterator for words that are either []byte or string.
|
||||
// Iterate while Next() is true, and access the word via Value().
|
||||
type Iterator[T Stringish] struct {
|
||||
split SplitFunc[T]
|
||||
data T
|
||||
start int
|
||||
pos int
|
||||
}
|
||||
|
||||
// New creates a new Iterator for the given data and SplitFunc.
|
||||
func New[T Stringish](split SplitFunc[T], data T) *Iterator[T] {
|
||||
return &Iterator[T]{
|
||||
split: split,
|
||||
data: data,
|
||||
}
|
||||
}
|
||||
|
||||
// SetText sets the text for the iterator to operate on, and resets all state.
|
||||
func (iter *Iterator[T]) SetText(data T) {
|
||||
iter.data = data
|
||||
iter.start = 0
|
||||
iter.pos = 0
|
||||
}
|
||||
|
||||
// Split sets the SplitFunc for the Iterator.
|
||||
func (iter *Iterator[T]) Split(split SplitFunc[T]) {
|
||||
iter.split = split
|
||||
}
|
||||
|
||||
// Next advances the iterator to the next token. It returns false when there
|
||||
// are no remaining tokens or an error occurred.
|
||||
func (iter *Iterator[T]) Next() bool {
|
||||
if iter.pos == len(iter.data) {
|
||||
return false
|
||||
}
|
||||
if iter.pos > len(iter.data) {
|
||||
panic("SplitFunc advanced beyond the end of the data")
|
||||
}
|
||||
|
||||
iter.start = iter.pos
|
||||
|
||||
advance, _, err := iter.split(iter.data[iter.pos:], true)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if advance <= 0 {
|
||||
panic("SplitFunc returned a zero or negative advance")
|
||||
}
|
||||
|
||||
iter.pos += advance
|
||||
if iter.pos > len(iter.data) {
|
||||
panic("SplitFunc advanced beyond the end of the data")
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Value returns the current token.
|
||||
func (iter *Iterator[T]) Value() T {
|
||||
return iter.data[iter.start:iter.pos]
|
||||
}
|
||||
|
||||
// Start returns the byte position of the current token in the original data.
|
||||
func (iter *Iterator[T]) Start() int {
|
||||
return iter.start
|
||||
}
|
||||
|
||||
// End returns the byte position after the current token in the original data.
|
||||
func (iter *Iterator[T]) End() int {
|
||||
return iter.pos
|
||||
}
|
||||
|
||||
// Reset resets the iterator to the beginning of the data.
|
||||
func (iter *Iterator[T]) Reset() {
|
||||
iter.start = 0
|
||||
iter.pos = 0
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user