Compare commits
189 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e04a1e15c4 | |||
|
9d401202b4
|
|||
|
6504be6403
|
|||
| d4944dbf35 | |||
|
8d8d4f799d
|
|||
| 0633f24d1b | |||
|
2e062899c7
|
|||
|
fbd7275f03
|
|||
| cedf185e97 | |||
|
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
|
||||
|
||||
+9
-25
@@ -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
|
||||
@@ -40,31 +49,6 @@ steps:
|
||||
depends_on:
|
||||
- make check
|
||||
|
||||
- name: fetch
|
||||
image: docker:git
|
||||
commands:
|
||||
- git fetch --tags
|
||||
depends_on:
|
||||
- make check
|
||||
- make test
|
||||
when:
|
||||
event: tag
|
||||
|
||||
- name: release
|
||||
image: goreleaser/goreleaser:v2.5.1
|
||||
environment:
|
||||
GITEA_TOKEN:
|
||||
from_secret: goreleaser_gitea_token
|
||||
volumes:
|
||||
- name: deps
|
||||
path: /go
|
||||
commands:
|
||||
- goreleaser release
|
||||
depends_on:
|
||||
- fetch
|
||||
when:
|
||||
event: tag
|
||||
|
||||
- name: publish image
|
||||
image: plugins/docker
|
||||
settings:
|
||||
|
||||
+1
-1
@@ -4,6 +4,6 @@
|
||||
.envrc
|
||||
.vscode/
|
||||
/abra
|
||||
/kadabra
|
||||
/bin
|
||||
dist/
|
||||
tests/integration/.bats
|
||||
|
||||
+5
-25
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -96,3 +86,6 @@ build-mo:
|
||||
for lang in $(POFILES); do \
|
||||
msgfmt $$lang -o $$(echo $$lang | sed 's/.po/.mo/g') --statistics; \
|
||||
done
|
||||
|
||||
release:
|
||||
@goreleaser release --clean
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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"),
|
||||
)
|
||||
|
||||
+67
-14
@@ -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
|
||||
|
||||
+302
-9
@@ -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()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
+18
-9
@@ -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))
|
||||
|
||||
+32
-2
@@ -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 {
|
||||
|
||||
+2
-1
@@ -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"),
|
||||
)
|
||||
|
||||
+11
-3
@@ -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)
|
||||
|
||||
+3
-3
@@ -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"),
|
||||
)
|
||||
|
||||
+9
-3
@@ -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))
|
||||
}
|
||||
},
|
||||
|
||||
+10
-3
@@ -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)
|
||||
|
||||
+17
-9
@@ -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{
|
||||
|
||||
+1
-1
@@ -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"),
|
||||
)
|
||||
|
||||
+212
-236
@@ -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,21 +97,6 @@ your private key and enter your passphrase beforehand.
|
||||
log.Fatal(i18n.G("main app service version for %s is empty?", recipe.Name))
|
||||
}
|
||||
|
||||
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"))
|
||||
}
|
||||
|
||||
repo, err := git.PlainOpen(recipe.Dir)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
@@ -118,16 +107,13 @@ 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)
|
||||
}
|
||||
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()
|
||||
@@ -135,58 +121,195 @@ your private key and enter your passphrase beforehand.
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var tagString string
|
||||
if len(args) == 2 {
|
||||
tagString = args[1]
|
||||
}
|
||||
|
||||
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) {
|
||||
var err error
|
||||
tagString, err = getLabelVersion(recipe, false)
|
||||
|
||||
catl, err := recipePkg.ReadRecipeCatalogue(false)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
isClean, err := gitPkg.IsClean(recipe.Dir)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
changesTable, err := formatter.CreateTable()
|
||||
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 {
|
||||
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 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)
|
||||
}
|
||||
if tagString == "" {
|
||||
var lastGitTag tagcmp.Tag
|
||||
iter, err := repo.Tags()
|
||||
if err != nil {
|
||||
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 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
|
||||
}
|
||||
if cleanErr := cleanCommit(recipe, preCommitHead); cleanErr != nil {
|
||||
log.Fatal(cleanErr)
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
if err := createReleaseFromTag(recipe, tagString, mainAppVersion); err != nil {
|
||||
if cleanErr := cleanTag(recipe, tagString); cleanErr != nil {
|
||||
log.Fatal(i18n.G("unable to clean up tag after failed release attempt: %s", cleanErr))
|
||||
}
|
||||
if resetErr := resetCommit(recipe, preCommitHead); resetErr != nil {
|
||||
log.Fatal(i18n.G("unable to reset commit after failed release attempt: %s", resetErr))
|
||||
}
|
||||
log.Error(err)
|
||||
log.Fatal(i18n.G("release failed. any changes made have been reverted"))
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// 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 +353,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)
|
||||
@@ -238,23 +361,14 @@ func createReleaseFromTag(recipe recipe.Recipe, tagString, mainAppVersion string
|
||||
return err
|
||||
}
|
||||
|
||||
tag, err := tagcmp.Parse(tagString)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if tag.MissingMinor {
|
||||
tag.Minor = "0"
|
||||
tag.MissingMinor = false
|
||||
}
|
||||
|
||||
if tag.MissingPatch {
|
||||
tag.Patch = "0"
|
||||
tag.MissingPatch = false
|
||||
}
|
||||
|
||||
if tagString == "" {
|
||||
tagString = fmt.Sprintf("%s+%s", tag.String(), mainAppVersion)
|
||||
mainService := "app"
|
||||
label := fmt.Sprintf("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))
|
||||
}
|
||||
|
||||
if err := addReleaseNotes(recipe, tagString); err != nil {
|
||||
@@ -293,7 +407,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 +435,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 +492,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 +544,30 @@ 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 {
|
||||
// resetCommit hard resets to the state before release was started.
|
||||
// This will only remove changes made by the release process due to requiring
|
||||
// a clean working directory.
|
||||
func resetCommit(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))
|
||||
@@ -574,18 +578,18 @@ func cleanCommit(recipe recipe.Recipe, head *plumbing.Reference) error {
|
||||
return errors.New(i18n.G("unable to open work tree in %s: %s", recipe.Dir, err))
|
||||
}
|
||||
|
||||
opts := &git.ResetOptions{Commit: head.Hash(), Mode: git.MixedReset}
|
||||
opts := &git.ResetOptions{Commit: head.Hash(), Mode: git.HardReset}
|
||||
if err := worktree.Reset(opts); err != nil {
|
||||
return errors.New(i18n.G("unable to soft reset %s: %s", recipe.Dir, err))
|
||||
return errors.New(i18n.G("unable to hard reset %s: %s", recipe.Dir, err))
|
||||
}
|
||||
|
||||
log.Debug(i18n.G("removed freshly created commit"))
|
||||
log.Debug(i18n.G("reset commit to pre-release state"))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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 +606,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 +649,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"),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
)
|
||||
}
|
||||
+38
-6
@@ -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"),
|
||||
)
|
||||
}
|
||||
|
||||
+17
-13
@@ -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,
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+1
-1
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
+15
-8
@@ -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 {
|
||||
|
||||
+19
-5
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
+1
-4
@@ -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"
|
||||
)
|
||||
|
||||
+14
-2
@@ -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 {
|
||||
|
||||
+1
-1
@@ -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
|
||||
}
|
||||
|
||||
+10
-5
@@ -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
|
||||
|
||||
+742
-633
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+2418
-1891
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -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 {
|
||||
|
||||
+1
-1
@@ -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
|
||||
}
|
||||
|
||||
+17
-5
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+85
-2
@@ -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}
|
||||
|
||||
@@ -138,7 +138,7 @@ func loadConfigFile(filename string) (*composetypes.ConfigFile, error) {
|
||||
|
||||
config, err := loader.ParseYAML(bytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("%s: %s", filename, err)
|
||||
}
|
||||
|
||||
return &composetypes.ConfigFile{
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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>
|
||||
@@ -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'
|
||||
+10
-10
@@ -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,194 @@ 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"
|
||||
}
|
||||
|
||||
@test "push during release fails" {
|
||||
tagHash=$(_get_tag_hash "0.2.0+1.21.0")
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" checkout "$tagHash"
|
||||
assert_success
|
||||
|
||||
run $ABRA recipe upgrade "$TEST_RECIPE" --no-input --patch --commit
|
||||
assert_success
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" show
|
||||
assert_success
|
||||
assert_output --partial 'image: nginx:1.21.6'
|
||||
|
||||
wantHash="$(_get_current_hash)"
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" remote set-url origin-ssh "$ABRA_DIR/does/not/exist"
|
||||
assert_success
|
||||
|
||||
run $ABRA recipe release "$TEST_RECIPE" --no-input --patch
|
||||
assert_failure
|
||||
assert_output --partial 'failed to publish new release:'
|
||||
assert_output --partial 'any changes made have been reverted'
|
||||
|
||||
assert_equal "$wantHash" "$(_get_current_hash)"
|
||||
|
||||
assert_equal "$(_git_status)" ""
|
||||
}
|
||||
|
||||
@test "release, fail, release: works" {
|
||||
tagHash=$(_get_tag_hash "0.2.0+1.21.0")
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" checkout "$tagHash"
|
||||
assert_success
|
||||
|
||||
run $ABRA recipe upgrade "$TEST_RECIPE" --no-input --patch --commit
|
||||
assert_success
|
||||
|
||||
# NOTE(d1): fake broken remote so the release fails
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" remote set-url origin-ssh "$ABRA_DIR/does/not/exist"
|
||||
assert_success
|
||||
|
||||
run $ABRA recipe release "$TEST_RECIPE" --no-input --patch
|
||||
assert_failure
|
||||
|
||||
# NOTE(d1): correct remote so release can proceed
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" remote set-url origin-ssh "$ABRA_DIR/origin-recipes/$TEST_RECIPE.git"
|
||||
assert_success
|
||||
|
||||
run $ABRA recipe release "$TEST_RECIPE" --no-input --patch
|
||||
assert_success
|
||||
}
|
||||
|
||||
@@ -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
@@ -26,6 +26,10 @@ linters:
|
||||
- whitespace
|
||||
- wrapcheck
|
||||
exclusions:
|
||||
rules:
|
||||
- text: '(slog|log)\.\w+'
|
||||
linters:
|
||||
- noctx
|
||||
generated: lax
|
||||
presets:
|
||||
- common-false-positives
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+2
-2
@@ -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 • نحنُ نحب المصادر المفتوحة
|
||||
|
||||
+22
-16
@@ -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.
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+8
-8
@@ -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
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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.
|
||||
|
||||
+1
-1
@@ -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())
|
||||
|
||||
+73
-36
@@ -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.
|
||||
|
||||
+1
-1
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user