Compare commits

..

1 Commits

Author SHA1 Message Date
6da0aa1224 chore: make deps
Some checks failed
continuous-integration/drone/push Build is failing
2026-01-17 00:13:07 +01:00
694 changed files with 83337 additions and 49293 deletions

View File

@ -3,7 +3,7 @@ kind: pipeline
name: coopcloud.tech/abra
steps:
- name: make check
image: golang:1.26
image: golang:1.24
commands:
- make check
@ -22,7 +22,7 @@ steps:
- tag
- name: xgettext-go status
image: golang:1.26-alpine3.22
image: golang:1.24-alpine3.22
commands:
- apk add patchutils git make
- cd /drone/src
@ -38,14 +38,42 @@ steps:
- tag
- name: make test
image: golang:1.26
image: golang:1.24
environment:
ABRA_DIR: /root/.abra_test
ABRA_DIR: $HOME/.abra
CATL_URL: https://git.coopcloud.tech/toolshed/recipes-catalogue-json.git
commands:
- mkdir -p $HOME/.abra
- git clone $CATL_URL $HOME/.abra/catalogue
- make test
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:

View File

@ -1,6 +1,4 @@
---
version: 2
gitea_urls:
api: https://git.coopcloud.tech/api/v1
download: https://git.coopcloud.tech/
@ -28,9 +26,6 @@ builds:
- 5
- 6
- 7
flags:
- -v
- -trimpath
ldflags:
- "-X 'main.Commit={{ .Commit }}'"
- "-X 'main.Version={{ .Version }}'"

View File

@ -10,17 +10,14 @@
- cassowary
- chasqui
- codegod100
- cyrnel
- decentral1se
- fauno
- frando
- iexos
- jade
- kawaiipunk
- knoflook
- mayel
- moritz
- namnatulco
- p4u1
- rix
- roxxers

View File

@ -1,5 +1,5 @@
# Build image
FROM golang:1.26-alpine AS build
FROM golang:1.24-alpine AS build
ENV GOPRIVATE=coopcloud.tech
@ -15,7 +15,8 @@ WORKDIR /app
RUN CGO_ENABLED=0 make build
FROM alpine:3.22
# Release image ("slim")
FROM alpine:3.19.1
RUN apk add --no-cache \
ca-certificates \

View File

@ -2,10 +2,9 @@ ABRA := ./cmd/abra
XGETTEXT := ./bin/xgettext-go
COMMIT := $(shell git rev-list -1 HEAD)
GOPATH := $(shell go env GOPATH)
GOVERSION := 1.26
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)
@ -23,7 +22,7 @@ install:
@go install -gcflags=$(GCFLAGS) -ldflags=$(LDFLAGS) $(ABRA)
build:
@go build $(BFLAGS) -gcflags=$(GCFLAGS) -ldflags=$(DIST_LDFLAGS) $(ABRA)
@go build -v -gcflags=$(GCFLAGS) -ldflags=$(DIST_LDFLAGS) $(ABRA)
build-docker:
@docker run -it -v $(PWD):/abra golang:$(GOVERSION) \
@ -40,7 +39,7 @@ check:
(echo "gofmt: formatting issue - run 'make format' to resolve" && exit 1)
test:
@go test ./... -cover -v -p 1
@go test ./... -cover -v
find-tests:
@find . -name "*_test.go"
@ -86,6 +85,3 @@ build-mo:
for lang in $(POFILES); do \
msgfmt $$lang -o $$(echo $$lang | sed 's/.po/.mo/g') --statistics; \
done
release:
@goreleaser release --clean

View File

@ -10,7 +10,6 @@ import (
"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"
@ -108,15 +107,6 @@ checkout as-is. Recipe commit hashes are also supported as values for
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))
}
if !internal.Chaos {
isChaosCommit, err := app.Recipe.EnsureVersion(toDeployVersion)
if err != nil {
@ -291,21 +281,13 @@ checkout as-is. Recipe commit hashes are also supported as values for
}
func getLatestVersionOrCommit(app appPkg.App) (string, error) {
recipeVersions, warnings, err := app.Recipe.GetRecipeVersions()
versions, err := app.Recipe.Tags()
if err != nil {
return "", err
}
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
}
if len(versions) > 0 && !internal.Chaos {
return versions[len(versions)-1], nil
}
head, err := app.Recipe.Head()

View File

@ -113,10 +113,13 @@ 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)
if err := app.Recipe.EnsureUpToDate(); err != nil {
log.Warnf(
"Failed to ensure repo is up to date for recipe: %s err: %s",
app.Recipe.Name,
err,
)
}
status := i18n.G("unknown")
version := i18n.G("unknown")
chaos := i18n.G("unknown")
@ -329,14 +332,6 @@ 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) {

View File

@ -72,10 +72,6 @@ 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"))
}
@ -102,14 +98,10 @@ var AppNewCommand = &cobra.Command{
var recipeVersions recipePkg.RecipeVersions
if recipeVersion == "" {
var err error
var warnings []string
recipeVersions, warnings, err = recipe.GetRecipeVersions()
recipeVersions, _, err = recipe.GetRecipeVersions()
if err != nil {
log.Fatal(err)
}
for _, warning := range warnings {
log.Warn(warning)
}
}
if len(recipeVersions) > 0 {
@ -118,8 +110,6 @@ 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)
}

View File

@ -238,7 +238,7 @@ beforehand. See "abra app backup" for more.`),
if upgradeReleaseNotes == "" {
upgradeWarnMessages = append(
upgradeWarnMessages,
i18n.G("no release notes for upgrading from %s to %s", deployMeta.Version, chosenUpgrade),
fmt.Sprintf("no release notes available for %s", chosenUpgrade),
)
}

View File

@ -127,7 +127,7 @@ func DeployOverview(
}
response := false
prompt := &survey.Confirm{Message: i18n.G("proceed?")}
prompt := &survey.Confirm{Message: "proceed?"}
if err := survey.AskOne(prompt, &response); err != nil {
return err
}

View File

@ -14,7 +14,7 @@ import (
gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2"
"github.com/distribution/reference"
@ -23,9 +23,6 @@ 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")
@ -53,7 +50,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.
This command will publish your new release to git.coopcloud.tech. This
Publish your new release to git.coopcloud.tech with "--publish/-p". This
requires that you have permission to git push to these repositories and have
your SSH keys configured on your account. Enable ssh-agent and make sure to add
your private key and enter your passphrase beforehand.
@ -63,13 +60,12 @@ your private key and enter your passphrase beforehand.
Example: ` # publish release
eval ` + "`ssh-agent`" + `
ssh-add ~/.ssh/id_ed25519
abra recipe release gitea`,
abra recipe release gitea -p`,
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()
@ -97,6 +93,21 @@ 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)
@ -107,13 +118,16 @@ your private key and enter your passphrase beforehand.
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))
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)
}
}
tags, err := recipe.Tags()
@ -121,195 +135,67 @@ 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) {
catl, err := recipePkg.ReadRecipeCatalogue(false)
if err != nil {
log.Fatal(err)
}
changesTable, err := formatter.CreateTable()
if err != nil {
log.Fatal(err)
}
latestRelease := tags[len(tags)-1]
latestRecipeVersion, err := getLatestVersion(recipe, catl)
if err != nil && err != errEmptyVersionsInCatalogue {
log.Fatal(err)
}
changesTable.Headers(i18n.G("SERVICE"), latestRelease, i18n.G("PROPOSED CHANGES"))
allRecipeVersions := catl[recipe.Name].Versions
for _, recipeVersion := range allRecipeVersions {
if serviceVersions, ok := recipeVersion[latestRecipeVersion]; ok {
for serviceName := range serviceVersions {
serviceMeta := serviceVersions[serviceName]
existingImageTag := fmt.Sprintf("%s:%s", serviceMeta.Image, serviceMeta.Tag)
newImageTag := fmt.Sprintf("%s:%s", serviceMeta.Image, imagesTmp[serviceMeta.Image])
if existingImageTag == newImageTag {
continue
}
changesTable.Row([]string{serviceName, existingImageTag, newImageTag}...)
}
}
}
changeOverview := changesTable.Render()
if err := internal.PromptBumpType("", latestRelease, changeOverview); err != nil {
log.Fatal(err)
}
}
if tagString == "" {
var lastGitTag tagcmp.Tag
iter, err := repo.Tags()
if err != nil {
log.Fatal(err)
}
if err := iter.ForEach(func(ref *plumbing.Reference) error {
obj, err := repo.TagObject(ref.Hash())
if err != nil {
log.Fatal(i18n.G("tag at commit %s is unannotated or otherwise broken", ref.Hash()))
return err
}
tagcmpTag, err := tagcmp.Parse(obj.Name)
if err != nil {
return err
}
if (lastGitTag == tagcmp.Tag{}) {
lastGitTag = tagcmpTag
} else if tagcmpTag.IsGreaterThan(lastGitTag) {
lastGitTag = tagcmpTag
}
return nil
}); err != nil {
log.Fatal(err)
}
// bumpType is used to decide what part of the tag should be incremented
bumpType := btoi(internal.Major)*4 + btoi(internal.Minor)*2 + btoi(internal.Patch)
if bumpType != 0 {
// a bitwise check if the number is a power of 2
if (bumpType & (bumpType - 1)) != 0 {
log.Fatal(i18n.G("you can only use one version flag: --major, --minor or --patch"))
}
}
newTag := lastGitTag
if bumpType > 0 {
if internal.Patch {
now, err := strconv.Atoi(newTag.Patch)
if err != nil {
log.Fatal(err)
}
newTag.Patch = strconv.Itoa(now + 1)
} else if internal.Minor {
now, err := strconv.Atoi(newTag.Minor)
if err != nil {
log.Fatal(err)
}
newTag.Patch = "0"
newTag.Minor = strconv.Itoa(now + 1)
} else if internal.Major {
now, err := strconv.Atoi(newTag.Major)
if err != nil {
log.Fatal(err)
}
newTag.Patch = "0"
newTag.Minor = "0"
newTag.Major = strconv.Itoa(now + 1)
}
}
newTag.Metadata = mainAppVersion
log.Debug(i18n.G("choosing %s as new version for %s", newTag.String(), recipe.Name))
tagString = newTag.String()
}
if _, err := tagcmp.Parse(tagString); err != nil {
log.Fatal(i18n.G("invalid version %s specified", tagString))
labelVersion, err := getLabelVersion(recipe, false)
if err != nil {
log.Fatal(err)
}
for _, tag := range tags {
previousTagLeftHand := strings.Split(tag, "+")[0]
newTagStringLeftHand := strings.Split(tagString, "+")[0]
newTagStringLeftHand := strings.Split(labelVersion, "+")[0]
if previousTagLeftHand == newTagStringLeftHand {
log.Fatal(i18n.G("%s+... conflicts with a previous release: %s", newTagStringLeftHand, tag))
}
}
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"))
if tagString == "" && (!internal.Major && !internal.Minor && !internal.Patch) {
tagString = labelVersion
}
isClean, err := gitPkg.IsClean(recipe.Dir)
if err != nil {
log.Fatal(err)
}
if !isClean {
log.Info(i18n.G("%s currently has these unstaged changes 👇", recipe.Name))
if err := gitPkg.DiffUnstaged(recipe.Dir); err != nil {
log.Fatal(err)
}
}
if len(tags) > 0 {
log.Warn(i18n.G("previous git tags detected, assuming new semver release"))
if err := createReleaseFromPreviousTag(tagString, mainAppVersion, recipe, tags); err != nil {
if cleanErr := cleanTag(recipe, tagString); cleanErr != nil {
log.Fatal(cleanErr)
}
if cleanErr := cleanCommit(recipe, preCommitHead); cleanErr != nil {
log.Fatal(cleanErr)
}
log.Fatal(err)
}
} else {
log.Warn(i18n.G("no tag specified and no previous tag available for %s, assuming initial release", recipe.Name))
if err := createReleaseFromTag(recipe, tagString, mainAppVersion); err != nil {
if cleanErr := cleanTag(recipe, tagString); cleanErr != nil {
log.Fatal(cleanErr)
}
if cleanErr := cleanCommit(recipe, preCommitHead); cleanErr != nil {
log.Fatal(cleanErr)
}
log.Fatal(err)
}
}
return
},
}
// GetImageVersions retrieves image versions for a recipe
func GetImageVersions(recipe recipePkg.Recipe) (map[string]string, error) {
func GetImageVersions(recipe recipe.Recipe) (map[string]string, error) {
services := make(map[string]string)
config, err := recipe.GetComposeConfig(nil)
@ -353,7 +239,7 @@ func GetImageVersions(recipe recipePkg.Recipe) (map[string]string, error) {
}
// createReleaseFromTag creates a new release based on a supplied recipe version string
func createReleaseFromTag(recipe recipePkg.Recipe, tagString, mainAppVersion string) error {
func createReleaseFromTag(recipe recipe.Recipe, tagString, mainAppVersion string) error {
var err error
repo, err := git.PlainOpen(recipe.Dir)
@ -361,14 +247,23 @@ func createReleaseFromTag(recipe recipePkg.Recipe, tagString, mainAppVersion str
return err
}
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))
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)
}
if err := addReleaseNotes(recipe, tagString); err != nil {
@ -407,7 +302,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 recipePkg.Recipe, tag string) error {
func addReleaseNotes(recipe recipe.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 {
@ -492,7 +387,7 @@ func addReleaseNotes(recipe recipePkg.Recipe, tag string) error {
return nil
}
func commitRelease(recipe recipePkg.Recipe, tag string) error {
func commitRelease(recipe recipe.Recipe, tag string) error {
if internal.Dry {
log.Debug(i18n.G("dry run: no changes committed"))
return nil
@ -544,30 +439,140 @@ func tagRelease(tagString string, repo *git.Repository) error {
return nil
}
func pushRelease(recipe recipePkg.Recipe, tagString string) error {
func pushRelease(recipe recipe.Recipe, tagString string) error {
if internal.Dry {
log.Info(i18n.G("dry run: no changes published"))
return nil
}
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 && !internal.NoInput {
prompt := &survey.Confirm{
Message: i18n.G("publish new release?"),
}
if err := survey.AskOne(prompt, &publish); err != nil {
return err
}
}
if err := recipe.Push(internal.Dry); err != nil {
return err
}
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"))
}
url := fmt.Sprintf("%s/src/tag/%s", recipe.GitURL, tagString)
log.Info(i18n.G("new release published: %s", url))
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
}
// 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 {
func createReleaseFromPreviousTag(tagString, mainAppVersion string, recipe recipe.Recipe, tags []string) error {
repo, err := git.PlainOpen(recipe.Dir)
if 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()))
}
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 {
repo, err := git.PlainOpen(recipe.Dir)
if err != nil {
return errors.New(i18n.G("unable to open repo in %s: %s", recipe.Dir, err))
@ -578,18 +583,18 @@ func resetCommit(recipe recipePkg.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.HardReset}
opts := &git.ResetOptions{Commit: head.Hash(), Mode: git.MixedReset}
if err := worktree.Reset(opts); err != nil {
return errors.New(i18n.G("unable to hard reset %s: %s", recipe.Dir, err))
return errors.New(i18n.G("unable to soft reset %s: %s", recipe.Dir, err))
}
log.Debug(i18n.G("reset commit to pre-release state"))
log.Debug(i18n.G("removed freshly created commit"))
return nil
}
// cleanTag removes a freshly created tag
func cleanTag(recipe recipePkg.Recipe, tag string) error {
func cleanTag(recipe recipe.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))
@ -606,17 +611,37 @@ func cleanTag(recipe recipePkg.Recipe, tag string) error {
return nil
}
func getLatestVersion(recipe recipePkg.Recipe, catl recipePkg.RecipeCatalogue) (string, error) {
versions, err := recipePkg.GetRecipeCatalogueVersions(recipe.Name, catl)
func getLabelVersion(recipe recipe.Recipe, prompt bool) (string, error) {
initTag, err := recipe.GetVersionLabelLocal()
if err != nil {
return "", err
}
if len(versions) > 0 {
return versions[len(versions)-1], nil
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))
}
return "", errEmptyVersionsInCatalogue
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
}
var (
publish bool
)
func init() {
RecipeReleaseCommand.Flags().BoolVarP(
&internal.Dry,
@ -649,4 +674,12 @@ 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"),
)
}

313
cli/recipe/sync.go Normal file
View File

@ -0,0 +1,313 @@
package recipe
import (
"errors"
"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"
)
// Errors
var emptyVersionsInCatalogue = errors.New(i18n.G("catalogue versions list is unexpectedly empty"))
// 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)
}
changesTable, err := formatter.CreateTable()
if err != nil {
log.Fatal(err)
}
latestRelease := tags[len(tags)-1]
latestRecipeVersion, err := getLatestVersion(recipe, catl)
if err != nil && err != emptyVersionsInCatalogue {
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 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"),
)
}
func getLatestVersion(recipe recipePkg.Recipe, catl recipePkg.RecipeCatalogue) (string, error) {
versions, err := recipePkg.GetRecipeCatalogueVersions(recipe.Name, catl)
if err != nil {
return "", err
}
if len(versions) > 0 {
return versions[len(versions)-1], nil
}
return "", emptyVersionsInCatalogue
}

View File

@ -11,7 +11,7 @@ func TestGetLatestVersionReturnsErrorWhenVersionsIsEmpty(t *testing.T) {
recipe := recipePkg.Recipe{}
catalogue := recipePkg.RecipeCatalogue{}
_, err := getLatestVersion(recipe, catalogue)
assert.Equal(t, err, errEmptyVersionsInCatalogue)
assert.Equal(t, err, emptyVersionsInCatalogue)
}
func TestGetLatestVersionReturnsLastVersion(t *testing.T) {

View File

@ -62,8 +62,7 @@ interface.`),
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) {
@ -336,37 +335,12 @@ interface.`),
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
createCommit bool
allTags bool
)
func init() {
@ -409,12 +383,4 @@ func init() {
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"),
)
}

View File

@ -187,20 +187,28 @@ Config:
rootCmd.PersistentFlags().BoolVarP(
&internal.Debug,
i18n.G("debug"),
i18n.G("d"),
"debug",
"d",
false,
i18n.G("show debug messages"),
)
rootCmd.PersistentFlags().BoolVarP(
&internal.NoInput,
i18n.G("no-input"),
i18n.G("n"),
"no-input",
"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"),
@ -209,14 +217,6 @@ 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,6 +245,7 @@ Config:
recipe.RecipeNewCommand,
recipe.RecipeReleaseCommand,
recipe.RecipeResetCommand,
recipe.RecipeSyncCommand,
recipe.RecipeUpgradeCommand,
recipe.RecipeVersionCommand,
)

93
go.mod
View File

@ -1,6 +1,6 @@
module coopcloud.tech/abra
go 1.26.0
go 1.24.2
require (
coopcloud.tech/tagcmp v0.0.0-20250818180036-0ec1b205b5ca
@ -8,19 +8,20 @@ require (
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0
github.com/charmbracelet/log v1.0.0
github.com/charmbracelet/log v0.4.2
github.com/distribution/reference v0.6.0
github.com/docker/cli v28.4.0+incompatible
github.com/docker/cli v29.1.5+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.17.2
github.com/go-git/go-git/v5 v5.16.4
github.com/google/go-cmp v0.7.0
github.com/leonelquinteros/gotext v1.7.2
github.com/moby/moby/client v0.2.1
github.com/moby/sys/signal v0.7.1
github.com/moby/term v0.5.2
github.com/pkg/errors v0.9.1
github.com/schollz/progressbar/v3 v3.19.0
golang.org/x/term v0.41.0
golang.org/x/term v0.39.0
gopkg.in/yaml.v3 v3.0.1
gotest.tools/v3 v3.5.2
)
@ -30,20 +31,21 @@ require (
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/BurntSushi/toml v1.6.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.4.1 // indirect
github.com/ProtonMail/go-crypto v1.3.0 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/charmbracelet/colorprofile v0.4.3 // indirect
github.com/charmbracelet/x/ansi v0.11.6 // indirect
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
github.com/charmbracelet/colorprofile v0.4.1 // indirect
github.com/charmbracelet/x/ansi v0.11.4 // indirect
github.com/charmbracelet/x/cellbuf v0.0.14 // indirect
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.11.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/cloudflare/circl v1.6.3 // indirect
github.com/clipperhouse/displaywidth v0.7.0 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
github.com/cloudflare/circl v1.6.2 // 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
@ -60,31 +62,31 @@ require (
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.8.0 // indirect
github.com/go-git/go-billy/v5 v5.7.0 // indirect
github.com/go-logfmt/logfmt v0.6.1 // indirect
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.5.0 // 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.28.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.4 // 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.6.0 // indirect
github.com/klauspost/compress v1.18.5 // indirect
github.com/kevinburke/ssh_config v1.4.0 // indirect
github.com/klauspost/compress v1.18.3 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/lucasb-eyer/go-colorful v1.4.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.21 // 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
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/go-archive v0.1.0 // indirect
github.com/moby/go-archive v0.2.0 // indirect
github.com/moby/moby/api v1.53.0-rc.1 // indirect
github.com/moby/sys/atomicwriter v0.1.0 // indirect
github.com/moby/sys/user v0.4.0 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
@ -100,7 +102,7 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.20.1 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sirupsen/logrus v1.9.4 // indirect
@ -111,27 +113,27 @@ require (
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect
go.opentelemetry.io/otel v1.42.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect
go.opentelemetry.io/otel v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 // indirect
go.opentelemetry.io/otel/metric v1.42.0 // indirect
go.opentelemetry.io/otel/sdk v1.42.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.42.0 // indirect
go.opentelemetry.io/otel/trace v1.42.0 // indirect
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/sdk v1.39.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.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.49.0 // indirect
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect
golang.org/x/net v0.52.0 // indirect
golang.org/x/text v0.35.0 // indirect
golang.org/x/time v0.15.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/grpc v1.80.0 // indirect
golang.org/x/crypto v0.47.0 // indirect
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/text v0.33.0 // indirect
golang.org/x/time v0.14.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260114163908-3f89685c29c3 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260114163908-3f89685c29c3 // indirect
google.golang.org/grpc v1.78.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
@ -146,18 +148,13 @@ require (
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/hashicorp/go-retryablehttp v0.7.8
github.com/moby/patternmatcher v0.6.1 // indirect
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.2 // indirect
github.com/sergi/go-diff v1.4.0 // indirect
github.com/spf13/cobra v1.10.1
github.com/spf13/cobra v1.10.2
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.42.0
golang.org/x/sys v0.40.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

289
go.sum
View File

@ -27,8 +27,6 @@ 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=
@ -81,13 +79,12 @@ github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb0
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/ProtonMail/go-crypto v1.4.1 h1:9RfcZHqEQUvP8RzecWEUafnZVtEvrBVL9BiF67IQOfM=
github.com/ProtonMail/go-crypto v1.4.1/go.mod h1:e1OaTyu5SYVrO9gKOEhTc+5UcXtTUa+P3uLudwcgPqo=
github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
@ -97,6 +94,7 @@ 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=
@ -105,14 +103,12 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiE
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
github.com/beorn7/perks v0.0.0-20150223135152-b965b613227f/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bitly/go-hostpool v0.1.0/go.mod h1:4gOCgp6+NZnVqlKyZ/iBZFTAJKembaVENUpMkpg42fw=
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA=
github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
@ -121,7 +117,6 @@ github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dR
github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s=
github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
github.com/bugsnag/bugsnag-go v1.0.5-0.20150529004307-13fd6b8acda0/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8=
github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50=
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
@ -130,22 +125,25 @@ github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyY
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
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.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
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 v1.0.0 h1:HVVVMmfOorfj3BA9i8X8UL69Hoz9lI0PYwXfJvOdRc4=
github.com/charmbracelet/log v1.0.0/go.mod h1:uYgY3SmLpwJWxmlrPwXvzVYujxis1vAKRV/0VQB7yWA=
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
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.11.3 h1:6DcVaqWI82BBVM/atTyq6yBoRLZFBsnoDoX9GCu2YOI=
github.com/charmbracelet/x/ansi v0.11.3/go.mod h1:yI7Zslym9tCJcedxz5+WBq+eUGMJT0bM06Fqy1/Y4dI=
github.com/charmbracelet/x/ansi v0.11.4 h1:6G65PLu6HjmE858CnTUQY1LXT3ZUWwfvqEROLF8vqHI=
github.com/charmbracelet/x/ansi v0.11.4/go.mod h1:/5AZ+UfWExW3int5H5ugnsG/PWjNcSQcwYsHBlPFQN4=
github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4=
github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA=
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.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
@ -165,13 +163,16 @@ 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/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.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.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/clipperhouse/displaywidth v0.6.2 h1:ZDpTkFfpHOKte4RG5O/BOyf3ysnvFswpyYrV7z2uAKo=
github.com/clipperhouse/displaywidth v0.6.2/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
github.com/clipperhouse/displaywidth v0.7.0 h1:QNv1GYsnLX9QBrcWUtMlogpTXuM5FVnBwKWp1O5NwmE=
github.com/clipperhouse/displaywidth v0.7.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/cloudflare/circl v1.6.2 h1:hL7VBpHHKzrV5WTfHCaBsgx/HGbBYlgrwvNXEVDYYsQ=
github.com/cloudflare/circl v1.6.2/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
@ -274,6 +275,8 @@ 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=
@ -295,8 +298,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6N
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
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.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
@ -308,18 +311,22 @@ 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 v29.1.3+incompatible h1:+kz9uDWgs+mAaIZojWfFt4d53/jv0ZUOOoSh5ZnH36c=
github.com/docker/cli v29.1.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v29.1.4+incompatible h1:AI8fwZhqsAsrqZnVv9h6lbexeW/LzNTasf6A4vcNN8M=
github.com/docker/cli v29.1.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v29.1.5+incompatible h1:GckbANUt3j+lsnQ6eCcQd70mNSOismSHWt8vk2AX8ao=
github.com/docker/cli v29.1.5+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=
@ -329,10 +336,10 @@ github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r
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.4 h1:76ItO69/AP/V4yT9V4uuuItG0B1N8hvt0T0c0NN/DzI=
github.com/docker/docker-credential-helpers v0.9.4/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY=
github.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0=
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c/go.mod h1:CADgU4DSXK5QUlFslkQu2yW2TKzFZcXq/leZfM0UH5Q=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
@ -351,7 +358,6 @@ github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZ
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dvsekhvalnov/jose2go v0.0.0-20170216131308-f21a8cedbbae/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM=
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
@ -367,7 +373,6 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.m
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0=
github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
@ -389,12 +394,12 @@ github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDzZG0=
github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY=
github.com/go-git/go-billy/v5 v5.7.0 h1:83lBUJhGWhYp0ngzCMSgllhUSuoHP1iEWYjsPl9nwqM=
github.com/go-git/go-billy/v5 v5.7.0/go.mod h1:/1IUejTKH8xipsAcdfcSAlUlo2J7lkYV8GTKxAT/L3E=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.17.2 h1:B+nkdlxdYrvyFK4GPXVU8w1U+YkbsgciIR7f2sZJ104=
github.com/go-git/go-git/v5 v5.17.2/go.mod h1:pW/VmeqkanRFqR6AljLcs7EA7FbZaN5MQqO7oZADXpo=
github.com/go-git/go-git/v5 v5.16.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y=
github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@ -423,8 +428,9 @@ github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8
github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw=
@ -435,16 +441,15 @@ github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5x
github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/googleapis v1.2.0/go.mod h1:Njal3psf3qN6dwBtQfUmBZh2ybovJ0tlu3o/AC7HYjU=
github.com/gogo/googleapis v1.4.0/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c=
github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
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/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=
@ -476,7 +481,6 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/certificate-transparency-go v1.0.10-0.20180222191210-5ab67e519c93/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@ -514,21 +518,23 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m
github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
github.com/gorilla/mux v1.7.0/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
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 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.4 h1:kEISI/Gx67NzH3nJxAmY/dGac80kKZgZt134u7Y/k1s=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.4/go.mod h1:6Nz966r3vQYCqIzWsuEl9d7cf7mRhtDmm++sOxlnfxI=
github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
@ -542,6 +548,7 @@ 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=
@ -557,9 +564,6 @@ github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLf
github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jinzhu/gorm v0.0.0-20170222002820-5409931a1bb8/go.mod h1:Vla75njaFJ8clLU1W44h34PjIkijhjHIYnZxMqCdxqo=
github.com/jinzhu/inflection v0.0.0-20170102125226-1c35d901db3d/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52CupLJyoVwB10FQ/IQlF1pdL8=
@ -571,12 +575,11 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U=
github.com/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.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY=
github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
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=
@ -584,8 +587,10 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.14.2/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
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=
@ -607,11 +612,10 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leonelquinteros/gotext v1.7.2 h1:bDPndU8nt+/kRo1m4l/1OXiiy2v7Z7dfPQ9+YP7G1Mc=
github.com/leonelquinteros/gotext v1.7.2/go.mod h1:9/haCkm5P7Jay1sxKDGJ5WIg4zkz8oZKw4ekNpALob8=
github.com/lib/pq v0.0.0-20150723085316-0dad96c0b94f/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo=
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/magiconair/properties v1.5.3/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
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.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=
@ -628,35 +632,35 @@ 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.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w=
github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
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=
github.com/mattn/go-sqlite3 v1.6.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88Jz2VyhSmden33/aXg4oVIY=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/miekg/pkcs11 v1.0.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU=
github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
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/mapstructure v0.0.0-20150613213606-2caf8efc9366/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
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=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo=
github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8=
github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU=
github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=
github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U=
github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/moby/api v1.53.0-rc.1 h1:M5SUwRbTrNy+plCTiV6gn4ZiN/Csynk0imIsUmOgHGI=
github.com/moby/moby/api v1.53.0-rc.1/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
github.com/moby/moby/client v0.2.1 h1:1Grh1552mvv6i+sYOdY+xKKVTvzJegcVMhuXocyDz/k=
github.com/moby/moby/client v0.2.1/go.mod h1:O+/tw5d4a1Ha/ZA/tPxIZJapJRUS6LNZ1wiVRxYHyUE=
github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A=
@ -699,6 +703,7 @@ 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=
@ -750,7 +755,7 @@ github.com/opencontainers/selinux v1.6.0/go.mod h1:VVGKuOLlE7v4PJyT6h7mNWvq1rzqi
github.com/opencontainers/selinux v1.8.0/go.mod h1:RScLhm78qiWa2gbVCcGkC7tCGdgk3ogry1nUQF8Evvo=
github.com/opencontainers/selinux v1.8.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.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
@ -764,8 +769,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
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=
@ -779,13 +784,18 @@ 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.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc=
github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
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=
@ -794,8 +804,9 @@ github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+Gx
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
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=
@ -803,6 +814,8 @@ github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6L
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww=
github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@ -824,6 +837,8 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg=
@ -832,11 +847,16 @@ 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/jwalterweatherman v0.0.0-20141219030609-3d60171a6431/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
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.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
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=
@ -844,7 +864,7 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
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=
@ -865,10 +885,9 @@ github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtse
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I=
github.com/tchap/go-patricia v2.3.0+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I=
github.com/theupdateframework/notary v0.7.0 h1:QyagRZ7wlSpjT5N2qQAh/pN+DVqgekv4DzbAiAiEL3c=
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=
@ -897,6 +916,7 @@ 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=
@ -904,6 +924,7 @@ 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=
@ -915,59 +936,58 @@ 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.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.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0 h1:MdKucPl/HbzckWWEisiNqMPhRrAOQX8r4jTuGr636gk=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.42.0/go.mod h1:RolT8tWtfHcjajEH5wFIZ4Dgh5jpPdFXYV9pTAk/qjc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0/go.mod h1:J2pvYM5NGHofZ2/Ru6zw/TNWnEQp5crgyDeSrYpXkAw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 h1:zWWrB1U6nqhS/k6zYB74CjRpuiitRtLLi68VcgmOEto=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0/go.mod h1:2qXPNBX1OVRC0IwOnfo1ljoid+RD0QK3443EaqVlsOU=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0 h1:cEf8jF6WbuGQWUVcqgyWtTR0kOOAWY1DYZ+UhvdmQPw=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0/go.mod h1:k1lzV5n5U3HkGvTCJHraTAGJ7MqsgL1wrGwTj1Isfiw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c=
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.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=
go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=
go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
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.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
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=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
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.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
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=
@ -978,8 +998,10 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-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-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA=
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ=
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
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=
@ -1043,8 +1065,10 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-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.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
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=
@ -1140,13 +1164,16 @@ 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.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
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.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
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=
@ -1156,16 +1183,18 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.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.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
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.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.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=
@ -1215,8 +1244,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.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
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=
@ -1261,12 +1290,15 @@ 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-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b h1:uA40e2M6fYRBf0+8uN5mLlqUtV192iiksiICIBkYJ1E=
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:Xa7le7qx2vmqB/SzWUBa7KdMjpdpAHlh5QCSnjessQk=
google.golang.org/genproto/googleapis/api v0.0.0-20260114163908-3f89685c29c3 h1:X9z6obt+cWRX8XjDVOn+SZWhWe5kZHm46TThU9j+jss=
google.golang.org/genproto/googleapis/api v0.0.0-20260114163908-3f89685c29c3/go.mod h1:dd646eSK+Dk9kxVBl1nChEOhJPtMXriCcVb4x3o6J+E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260114163908-3f89685c29c3 h1:C4WAdL+FbjnGlpp2S+HMVhBeCq2Lcib4xZqfPNF6OoQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260114163908-3f89685c29c3/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
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=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@ -1284,8 +1316,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.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
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=
@ -1303,7 +1335,6 @@ google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBN
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
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=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@ -1318,7 +1349,6 @@ gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKW
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.1/go.mod h1:WbjuEoo1oadwzQ4apSDU+JTvmllEHtsNHS6y7vFc7iw=
gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
@ -1338,6 +1368,7 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
@ -1379,6 +1410,8 @@ k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6/go.mod h1:UuqjUnNftUyPE5H
k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM=
k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk=
k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=

View File

@ -10,83 +10,46 @@ import (
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/recipe"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/test"
testPkg "coopcloud.tech/abra/pkg/test"
"github.com/docker/docker/api/types/filters"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
)
var (
expectedAppEnv = envfile.AppEnv{
"DOMAIN": test.AppName,
"RECIPE": test.RecipeName,
}
expectedApp = appPkg.App{
Name: test.AppName,
Recipe: recipePkg.Get(expectedAppEnv["RECIPE"]),
Domain: expectedAppEnv["DOMAIN"],
Env: expectedAppEnv,
Path: expectedAppFile.Path,
Server: expectedAppFile.Server,
}
expectedAppFile = appPkg.AppFile{
Path: test.AppEnvPath,
Server: test.ServerName,
}
expectedAppFiles = map[string]appPkg.AppFile{
test.AppName: expectedAppFile,
}
)
func TestNewApp(t *testing.T) {
app, err := appPkg.NewApp(expectedAppEnv, test.AppName, expectedAppFile)
app, err := appPkg.NewApp(testPkg.ExpectedAppEnv, testPkg.AppName, testPkg.ExpectedAppFile)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(app, expectedApp) {
t.Fatalf("did not get expected app type. Expected: %s; Got: %s", app, expectedApp)
if !reflect.DeepEqual(app, testPkg.ExpectedApp) {
t.Fatalf("did not get expected app type. Expected: %s; Got: %s", app, testPkg.ExpectedApp)
}
}
func TestReadAppEnvFile(t *testing.T) {
test.Setup()
t.Cleanup(func() { test.Teardown() })
app, err := appPkg.ReadAppEnvFile(expectedAppFile, test.AppName)
app, err := appPkg.ReadAppEnvFile(testPkg.ExpectedAppFile, testPkg.AppName)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(app, expectedApp) {
t.Fatalf("did not get expected app type. Expected: %s; Got: %s", app, expectedApp)
if !reflect.DeepEqual(app, testPkg.ExpectedApp) {
t.Fatalf("did not get expected app type. Expected: %s; Got: %s", app, testPkg.ExpectedApp)
}
}
func TestGetApp(t *testing.T) {
test.Setup()
t.Cleanup(func() { test.Teardown() })
app, err := appPkg.GetApp(expectedAppFiles, test.AppName)
app, err := appPkg.GetApp(testPkg.ExpectedAppFiles, testPkg.AppName)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(app, expectedApp) {
t.Fatalf("did not get expected app type. Expected: %s; Got: %s", app, expectedApp)
if !reflect.DeepEqual(app, testPkg.ExpectedApp) {
t.Fatalf("did not get expected app type. Expected: %s; Got: %s", app, testPkg.ExpectedApp)
}
}
func TestGetComposeFiles(t *testing.T) {
test.Setup()
t.Cleanup(func() { test.Teardown() })
r := recipe.Get(test.AbraTestRecipe)
if err := r.EnsureExists(); err != nil {
r := recipe.Get("abra-test-recipe")
err := r.EnsureExists()
if err != nil {
t.Fatal(err)
}
@ -131,10 +94,7 @@ func TestGetComposeFiles(t *testing.T) {
}
func TestGetComposeFilesError(t *testing.T) {
test.Setup()
t.Cleanup(func() { test.Teardown() })
r := recipe.Get(test.AbraTestRecipe)
r := recipe.Get("abra-test-recipe")
err := r.EnsureExists()
if err != nil {
t.Fatal(err)
@ -226,32 +186,26 @@ func TestFilters(t *testing.T) {
func compareFilter(t *testing.T, f1 filters.Args, f2 map[string]map[string]bool) {
t.Helper()
j1, err := f1.MarshalJSON()
if err != nil {
t.Error(err)
}
j2, err := json.Marshal(f2)
if err != nil {
t.Error(err)
}
if diff := cmp.Diff(string(j2), string(j1)); diff != "" {
t.Errorf("filters mismatch (-want +got):\n%s", diff)
}
}
func TestWriteRecipeVersionOverwrite(t *testing.T) {
test.Setup()
t.Cleanup(func() { test.Teardown() })
app, err := appPkg.GetApp(expectedAppFiles, test.AppName)
app, err := appPkg.GetApp(testPkg.ExpectedAppFiles, testPkg.AppName)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
defer t.Cleanup(func() {
if err := app.WipeRecipeVersion(); err != nil {
t.Fatal(err)
}
@ -263,7 +217,7 @@ func TestWriteRecipeVersionOverwrite(t *testing.T) {
t.Fatal(err)
}
app, err = appPkg.GetApp(expectedAppFiles, test.AppName)
app, err = appPkg.GetApp(testPkg.ExpectedAppFiles, testPkg.AppName)
if err != nil {
t.Fatal(err)
}
@ -272,10 +226,7 @@ func TestWriteRecipeVersionOverwrite(t *testing.T) {
}
func TestWriteRecipeVersionUnknown(t *testing.T) {
test.Setup()
t.Cleanup(func() { test.Teardown() })
app, err := appPkg.GetApp(expectedAppFiles, test.AppName)
app, err := appPkg.GetApp(testPkg.ExpectedAppFiles, testPkg.AppName)
if err != nil {
t.Fatal(err)
}

View File

@ -4,7 +4,6 @@ import (
"testing"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/test"
testPkg "coopcloud.tech/abra/pkg/test"
stack "coopcloud.tech/abra/pkg/upstream/stack"
@ -12,8 +11,8 @@ import (
)
func TestGetTimeoutFromLabel(t *testing.T) {
test.Setup()
t.Cleanup(func() { test.Teardown() })
testPkg.MkServerAppRecipe()
defer testPkg.RmServerAppRecipe()
tests := []struct {
configuredTimeout string
@ -26,7 +25,7 @@ func TestGetTimeoutFromLabel(t *testing.T) {
}
for _, test := range tests {
app, err := appPkg.GetApp(expectedAppFiles, testPkg.AppName)
app, err := appPkg.GetApp(testPkg.ExpectedAppFiles, testPkg.AppName)
if err != nil {
t.Fatal(err)
}

View File

@ -8,6 +8,7 @@ import (
"os"
"path"
"strings"
"time"
"coopcloud.tech/abra/pkg/config"
contextPkg "coopcloud.tech/abra/pkg/context"
@ -83,7 +84,8 @@ func New(serverName string, opts ...Opt) (*client.Client, error) {
httpClient := &http.Client{
Transport: &http.Transport{
DialContext: helper.Dialer,
DialContext: helper.Dialer,
IdleConnTimeout: 30 * time.Second,
},
}

View File

@ -10,9 +10,8 @@ import (
func TestFindAbraConfig(t *testing.T) {
wd, err := os.Getwd()
if err != nil {
t.Fatal(err)
log.Fatal(err)
}
tests := []struct {
Dir string
Config string
@ -52,9 +51,8 @@ func TestFindAbraConfig(t *testing.T) {
func TestLoadAbraConfigGetAbraDir(t *testing.T) {
wd, err := os.Getwd()
if err != nil {
t.Fatal(err)
log.Fatal(err)
}
t.Setenv("ABRA_DIR", "")
t.Run("default", func(t *testing.T) {
@ -69,7 +67,7 @@ func TestLoadAbraConfigGetAbraDir(t *testing.T) {
t.Cleanup(func() { os.Chdir(wd) })
err = os.Chdir(filepath.Join(wd, "testdata/abraconfig1"))
if err != nil {
t.Fatal(err)
log.Fatal(err)
}
cfg := LoadAbraConfig()
@ -83,7 +81,7 @@ func TestLoadAbraConfigGetAbraDir(t *testing.T) {
t.Cleanup(func() { os.Chdir(wd) })
err := os.Chdir(filepath.Join(wd, "testdata/abraconfig2"))
if err != nil {
t.Fatal(err)
log.Fatal(err)
}
cfg := LoadAbraConfig()
@ -106,9 +104,8 @@ func TestLoadAbraConfigGetAbraDir(t *testing.T) {
func TestLoadAbraConfigServersDir(t *testing.T) {
wd, err := os.Getwd()
if err != nil {
t.Fatal(err)
log.Fatal(err)
}
t.Setenv("ABRA_DIR", "")
t.Run("default", func(t *testing.T) {
@ -123,7 +120,7 @@ func TestLoadAbraConfigServersDir(t *testing.T) {
t.Cleanup(func() { os.Chdir(wd) })
err = os.Chdir(filepath.Join(wd, "testdata/abraconfig1"))
if err != nil {
t.Fatal(err)
log.Fatal(err)
}
cfg := LoadAbraConfig()

View File

@ -2,13 +2,18 @@ package context
import (
"errors"
"os"
"coopcloud.tech/abra/pkg/i18n"
"github.com/docker/cli/cli/command"
dConfig "github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/context"
"github.com/docker/cli/cli/context/docker"
"github.com/docker/cli/cli/context/store"
contextStore "github.com/docker/cli/cli/context/store"
cliflags "github.com/docker/cli/cli/flags"
dopts "github.com/docker/cli/opts"
"github.com/moby/moby/client"
)
func NewDefaultDockerContextStore() *command.ContextStoreWithDefault {
@ -21,13 +26,107 @@ func NewDefaultDockerContextStore() *command.ContextStoreWithDefault {
dockerContextStore := &command.ContextStoreWithDefault{
Store: store,
Resolver: func() (*command.DefaultContext, error) {
return command.ResolveDefaultContext(opts, storeConfig)
return resolveDefaultContext(opts, storeConfig)
},
}
return dockerContextStore
}
// resolveDefaultContext creates a Metadata for the current CLI invocation parameters
func resolveDefaultContext(opts *cliflags.ClientOptions, config store.Config) (*command.DefaultContext, error) {
contextTLSData := store.ContextTLSData{
Endpoints: make(map[string]store.EndpointTLSData),
}
contextMetadata := store.Metadata{
Endpoints: make(map[string]any),
Metadata: command.DockerContext{
Description: "",
},
Name: "default",
}
dockerEP, err := resolveDefaultDockerEndpoint(opts)
if err != nil {
return nil, err
}
contextMetadata.Endpoints[docker.DockerEndpoint] = dockerEP.EndpointMeta
if dockerEP.TLSData != nil {
contextTLSData.Endpoints[docker.DockerEndpoint] = *dockerEP.TLSData.ToStoreTLSData()
}
if err := config.ForeachEndpointType(func(n string, get store.TypeGetter) error {
if n == docker.DockerEndpoint { // handled above
return nil
}
ep := get()
if i, ok := ep.(command.EndpointDefaultResolver); ok {
meta, tls, err := i.ResolveDefault()
if err != nil {
return err
}
if meta == nil {
return nil
}
contextMetadata.Endpoints[n] = meta
if tls != nil {
contextTLSData.Endpoints[n] = *tls
}
}
// Nothing to be done
return nil
}); err != nil {
return nil, err
}
return &command.DefaultContext{Meta: contextMetadata, TLS: contextTLSData}, nil
}
// Resolve the Docker endpoint for the default context (based on config, env vars and CLI flags)
func resolveDefaultDockerEndpoint(opts *cliflags.ClientOptions) (docker.Endpoint, error) {
// defaultToTLS determines whether we should use a TLS host as default
// if nothing was configured by the user.
defaultToTLS := opts.TLSOptions != nil
host, err := getServerHost(opts.Hosts, defaultToTLS)
if err != nil {
return docker.Endpoint{}, err
}
var (
skipTLSVerify bool
tlsData *context.TLSData
)
if opts.TLSOptions != nil {
skipTLSVerify = opts.TLSOptions.InsecureSkipVerify
tlsData, err = context.TLSDataFromFiles(opts.TLSOptions.CAFile, opts.TLSOptions.CertFile, opts.TLSOptions.KeyFile)
if err != nil {
return docker.Endpoint{}, err
}
}
return docker.Endpoint{
EndpointMeta: docker.EndpointMeta{
Host: host,
SkipTLSVerify: skipTLSVerify,
},
TLSData: tlsData,
}, nil
}
func getServerHost(hosts []string, defaultToTLS bool) (string, error) {
switch len(hosts) {
case 0:
return dopts.ParseHost(defaultToTLS, os.Getenv(client.EnvOverrideHost))
case 1:
return dopts.ParseHost(defaultToTLS, hosts[0])
default:
return "", errors.New("specify only one -H")
}
}
func GetContextEndpoint(ctx contextStore.Metadata) (string, error) {
endpointmeta, ok := ctx.Endpoints["docker"].(context.EndpointMetaBase)
if !ok {

View File

@ -165,13 +165,7 @@ func GetImagesForStack(cl *dockerClient.Client, app appPkg.App) (map[string]stri
}
imageBaseName := reference.Path(imageParsed)
namedTag, ok := imageParsed.(reference.NamedTagged)
if !ok {
// This is an image without a tag
images[imageBaseName] = ""
continue
}
imageTag := namedTag.Tag()
imageTag := imageParsed.(reference.NamedTagged).Tag()
existingImageVersion, ok := images[imageBaseName]
if !ok {
@ -288,13 +282,7 @@ func GatherImagesForDeploy(cl *dockerClient.Client, app appPkg.App, compose *com
}
imageBaseName := reference.Path(imageParsed)
namedTag, ok := imageParsed.(reference.NamedTagged)
if !ok {
// This is an image without a tag
newImages[imageBaseName] = ""
continue
}
imageTag := namedTag.Tag()
imageTag := imageParsed.(reference.NamedTagged).Tag()
existingImageVersion, ok := newImages[imageBaseName]
if !ok {

View File

@ -10,73 +10,48 @@ import (
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/abra/pkg/test"
testPkg "coopcloud.tech/abra/pkg/test"
"github.com/stretchr/testify/assert"
)
var (
expectedAppEnv = envfile.AppEnv{
"DOMAIN": test.AppName,
"RECIPE": test.RecipeName,
}
expectedAppFile = appPkg.AppFile{
Path: test.AppEnvPath,
Server: test.ServerName,
}
expectedAppFiles = map[string]appPkg.AppFile{
test.AppName: expectedAppFile,
}
)
func TestGetAllFoldersInDirectory(t *testing.T) {
folders, err := config.GetAllFoldersInDirectory(test.TestDir)
folders, err := config.GetAllFoldersInDirectory(testPkg.TestDir)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(folders, test.TFolders) {
t.Fatalf("did not get expected folders. Expected: (%s), Got: (%s)", strings.Join(test.TFolders, ","), strings.Join(folders, ","))
if !reflect.DeepEqual(folders, testPkg.TFolders) {
t.Fatalf("did not get expected folders. Expected: (%s), Got: (%s)", strings.Join(testPkg.TFolders, ","), strings.Join(folders, ","))
}
}
func TestGetAllFilesInDirectory(t *testing.T) {
files, err := config.GetAllFilesInDirectory(test.TestDir)
files, err := config.GetAllFilesInDirectory(testPkg.TestDir)
if err != nil {
t.Fatal(err)
}
var fileNames []string
for _, file := range files {
fileNames = append(fileNames, file.Name())
}
if !reflect.DeepEqual(fileNames, test.TFiles) {
t.Fatalf("did not get expected files. Expected: (%s), Got: (%s)", strings.Join(test.TFiles, ","), strings.Join(fileNames, ","))
if !reflect.DeepEqual(fileNames, testPkg.TFiles) {
t.Fatalf("did not get expected files. Expected: (%s), Got: (%s)", strings.Join(testPkg.TFiles, ","), strings.Join(fileNames, ","))
}
}
func TestReadEnv(t *testing.T) {
test.Setup()
t.Cleanup(func() { test.Teardown() })
env, err := envfile.ReadEnv(expectedAppFile.Path)
env, err := envfile.ReadEnv(testPkg.ExpectedAppFile.Path)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(env, expectedAppEnv) {
if !reflect.DeepEqual(env, testPkg.ExpectedAppEnv) {
t.Fatal("did not get expected application settings")
}
}
func TestReadAbraShEnvVars(t *testing.T) {
test.Setup()
t.Cleanup(func() { test.Teardown() })
r := recipe.Get(test.AbraTestRecipe)
if err := r.EnsureExists(); err != nil {
r := recipe.Get("abra-test-recipe")
err := r.EnsureExists()
if err != nil {
t.Fatal(err)
}
@ -103,11 +78,9 @@ func TestReadAbraShEnvVars(t *testing.T) {
}
func TestReadAbraShCmdNames(t *testing.T) {
test.Setup()
t.Cleanup(func() { test.Teardown() })
r := recipe.Get(test.AbraTestRecipe)
if err := r.EnsureExists(); err != nil {
r := recipe.Get("abra-test-recipe")
err := r.EnsureExists()
if err != nil {
t.Fatal(err)
}
@ -129,11 +102,9 @@ func TestReadAbraShCmdNames(t *testing.T) {
}
func TestCheckEnv(t *testing.T) {
test.Setup()
t.Cleanup(func() { test.Teardown() })
r := recipe.Get(test.AbraTestRecipe)
if err := r.EnsureExists(); err != nil {
r := recipe.Get("abra-test-recipe")
err := r.EnsureExists()
if err != nil {
t.Fatal(err)
}
@ -164,11 +135,9 @@ func TestCheckEnv(t *testing.T) {
}
func TestCheckEnvError(t *testing.T) {
test.Setup()
t.Cleanup(func() { test.Teardown() })
r := recipe.Get(test.AbraTestRecipe)
if err := r.EnsureExists(); err != nil {
r := recipe.Get("abra-test-recipe")
err := r.EnsureExists()
if err != nil {
t.Fatal(err)
}
@ -201,11 +170,9 @@ func TestCheckEnvError(t *testing.T) {
}
func TestEnvVarCommentsRemoved(t *testing.T) {
test.Setup()
t.Cleanup(func() { test.Teardown() })
r := recipe.Get(test.AbraTestRecipe)
if err := r.EnsureExists(); err != nil {
r := recipe.Get("abra-test-recipe")
err := r.EnsureExists()
if err != nil {
t.Fatal(err)
}
@ -234,11 +201,9 @@ func TestEnvVarCommentsRemoved(t *testing.T) {
}
func TestEnvVarModifiersIncluded(t *testing.T) {
test.Setup()
t.Cleanup(func() { test.Teardown() })
r := recipe.Get(test.AbraTestRecipe)
if err := r.EnsureExists(); err != nil {
r := recipe.Get("abra-test-recipe")
err := r.EnsureExists()
if err != nil {
t.Fatal(err)
}
@ -260,10 +225,7 @@ func TestEnvVarModifiersIncluded(t *testing.T) {
}
func TestNoOverwriteNonVersionEnvVars(t *testing.T) {
test.Setup()
t.Cleanup(func() { test.Teardown() })
app, err := appPkg.GetApp(expectedAppFiles, test.AppName)
app, err := appPkg.GetApp(testPkg.ExpectedAppFiles, testPkg.AppName)
if err != nil {
t.Fatal(err)
}
@ -272,7 +234,7 @@ func TestNoOverwriteNonVersionEnvVars(t *testing.T) {
t.Fatal(err)
}
app, err = appPkg.GetApp(expectedAppFiles, test.AppName)
app, err = appPkg.GetApp(testPkg.ExpectedAppFiles, testPkg.AppName)
if err != nil {
t.Fatal(err)
}

View File

@ -8,44 +8,11 @@ import (
"testing"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/log"
)
func setup() {
teardown()
if err := os.Mkdir(os.ExpandEnv("$ABRA_DIR"), 0764); err != nil {
if !os.IsExist(err) {
log.Fatal(err)
}
}
if err := os.Mkdir(os.ExpandEnv("$ABRA_DIR/recipes"), 0764); err != nil {
if !os.IsExist(err) {
log.Fatal(err)
}
}
}
func teardown() {
abraDir := os.ExpandEnv("$ABRA_DIR")
if abraDir == fmt.Sprintf("%s/.abra", os.ExpandEnv("$HOME")) {
log.Fatal("set $ABRA_DIR before running the test suite")
}
if err := os.RemoveAll(abraDir); err != nil {
log.Fatal(err)
}
}
func TestClone(t *testing.T) {
setup()
t.Cleanup(func() { teardown() })
dir := path.Join(config.RECIPES_DIR, "gitea")
if err := os.RemoveAll(dir); err != nil {
t.Fatal(err)
}
os.RemoveAll(dir)
gitURL := fmt.Sprintf("%s/%s.git", config.REPOS_BASE_URL, "gitea")
if err := Clone(dir, gitURL); err != nil {
@ -58,11 +25,6 @@ func TestClone(t *testing.T) {
}
func TestCancelGitClone(t *testing.T) {
t.Skip("https://git.coopcloud.tech/toolshed/abra/issues/814")
setup()
t.Cleanup(func() { teardown() })
dir := path.Join(config.RECIPES_DIR, "gitea")
os.RemoveAll(dir)

View File

@ -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
}

View File

@ -23,14 +23,6 @@ var (
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 {
@ -46,7 +38,11 @@ func LoadLocale() (string, *gotext.Mo) {
}
}
locale := GetLocaleStr()
locale := os.Getenv("LANG")
if lastUnderscore := strings.LastIndex(locale, "_"); lastUnderscore != -1 {
locale = locale[0:lastUnderscore]
}
if locale != "" {
if slices.Contains(linguas, locale) {
Locale = locale

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -142,7 +142,7 @@ var LintRules = map[string][]LintRule{
Function: LintAppService,
},
{
Ref: "R016",
Ref: "R015",
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(".env.sample for %s couldn't be read: %s", r.Name, err))
return false, errors.New(i18n.G("unable to discover .env.sample for %s", r.Name))
}
if _, ok := sampleEnv["DOMAIN"]; !ok {

View File

@ -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(".env.sample for %s couldn't be read: %s", r.Name, err))
return sampleEnv, errors.New(i18n.G("unable to discover .env.sample for %s", r.Name))
}
return sampleEnv, nil
}

View File

@ -32,12 +32,6 @@ 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
}
@ -409,18 +403,15 @@ 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: %s", tag, r.Dir, err))
warnMsg = append(warnMsg, i18n.G("skipping tag %s: checkout failed: %s", tag, err))
return nil
log.Debug(i18n.G("failed to check out %s in %s", tag, r.Dir))
return err
}
log.Debug(i18n.G("git checkout: %s in %s", ref.Name(), r.Dir))
config, err := r.GetComposeConfig(nil)
if err != nil {
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
return err
}
versionMeta := make(map[string]ServiceMeta)
@ -428,9 +419,7 @@ func (r Recipe) GetRecipeVersions() (RecipeVersions, []string, error) {
img, err := reference.ParseNormalizedNamed(service.Image)
if err != nil {
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
return err
}
path := reference.Path(img)
@ -456,7 +445,6 @@ 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
}

View File

@ -5,15 +5,12 @@ import (
"path/filepath"
"testing"
"coopcloud.tech/abra/pkg/test"
"github.com/stretchr/testify/assert"
)
func TestIsDirty(t *testing.T) {
test.Setup()
t.Cleanup(func() { test.Teardown() })
r := Get("abra-test-recipe")
r := Get(test.RecipeName)
if err := r.EnsureExists(); err != nil {
t.Fatal(err)
}
@ -25,9 +22,10 @@ func TestIsDirty(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer f.Close()
t.Cleanup(func() { os.Remove(fpath) })
defer t.Cleanup(func() {
os.Remove(fpath)
})
dirty, err := r.IsDirty()
if err != nil {

View File

@ -5,7 +5,6 @@ import (
"testing"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/test"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
@ -102,24 +101,24 @@ func TestGet(t *testing.T) {
}
func TestGetVersionLabelLocalDoesNotUseTimeoutLabel(t *testing.T) {
test.Setup()
t.Cleanup(func() { test.Teardown() })
r := Get(test.RecipeName)
r := Get("traefik")
if err := r.EnsureExists(); err != nil {
t.Fatal(err)
}
timeout := "120"
if err := test.AddEnv("TIMEOUT", timeout); err != nil {
t.Fatal(err)
}
for i := 1; i < 3; i++ {
for i := 1; i < 50; i++ {
label, err := r.GetVersionLabelLocal()
if err != nil {
t.Fatal(err)
}
assert.NotEqual(t, label, timeout)
// NOTE(d1): this is potentially quite a brittle unit test as it needs to
// hardcode the default timeout label to ensure that the label parser never
// returns it. hopefully this won't fail too often! if you're here because
// of a failure, just update the `defaultTimeoutLabel` value & permalink
// below
// https://git.coopcloud.tech/coop-cloud/traefik/src/commit/ac3a47fe8ca3ef92db84f64cfedfbb348000faee/.env.sample#L2
defaultTimeoutLabel := "300"
assert.NotEqual(t, label, defaultTimeoutLabel)
}
}

View File

@ -5,8 +5,6 @@ package secret
import (
"context"
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"slices"
@ -41,14 +39,6 @@ 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
@ -97,17 +87,6 @@ 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
@ -198,8 +177,6 @@ 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
@ -208,45 +185,11 @@ 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":
@ -281,23 +224,12 @@ 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 {
var password string
var err error
if secret.Charset == "bytes" {
password, err = generateRandomBytes(secret.Length)
} else {
password, err = GeneratePassword(uint(secret.Length), secret.Charset)
}
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))
@ -318,9 +250,6 @@ 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))

View File

@ -18,80 +18,42 @@ 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) {
@ -102,48 +64,3 @@ 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)
}

View File

@ -5,6 +5,3 @@ 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:

View File

@ -12,9 +12,6 @@ services:
- test_pass_five
- test_pass_six
- test_pass_seven
- test_pass_eight
- test_pass_nine
- test_pass_ten
secrets:
test_pass_one:
@ -38,12 +35,3 @@ secrets:
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}

View File

@ -2,50 +2,57 @@ package test
import (
"fmt"
"log"
"os"
"path"
gitPkg "coopcloud.tech/abra/pkg/git"
"git.coopcloud.tech/toolshed/godotenv"
appPkg "coopcloud.tech/abra/pkg/app"
"coopcloud.tech/abra/pkg/envfile"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
)
var (
AppName = "test_app.example.com"
ServerName = "test_server"
RecipeName = "test_recipe"
TFiles = []string{"bar.env", "foo.env"}
TFolders = []string{"dir1", "dir2"}
TestServer = os.ExpandEnv("$PWD/../../tests/resources/test_server")
TestDir = os.ExpandEnv("$PWD/../../tests/resources/test_dir")
TFiles = []string{"bar.env", "foo.env"}
TFolders = []string{"dir1", "dir2"}
ExpectedAppEnv = envfile.AppEnv{
"DOMAIN": "test_app.example.com",
"RECIPE": "test_recipe",
}
ServerDir = os.ExpandEnv("$ABRA_DIR/servers/test_server")
RecipeDir = os.ExpandEnv("$ABRA_DIR/recipes/test_recipe")
TestDir = os.ExpandEnv("$PWD/../../tests/resources/test_dir")
ExpectedApp = appPkg.App{
Name: AppName,
Recipe: recipe.Get(ExpectedAppEnv["RECIPE"]),
Domain: ExpectedAppEnv["DOMAIN"],
Env: ExpectedAppEnv,
Path: ExpectedAppFile.Path,
Server: ExpectedAppFile.Server,
}
AppEnvPath = path.Join(ServerDir, fmt.Sprintf("%s.env", AppName))
ExpectedAppFile = appPkg.AppFile{
Path: path.Join(TestServer, fmt.Sprintf("%s.env", AppName)),
Server: ServerName,
}
AbraTestRecipe = "abra-test-recipe"
ExpectedAppFiles = map[string]appPkg.AppFile{
AppName: ExpectedAppFile,
}
)
func Teardown() {
abraDir := os.ExpandEnv("$ABRA_DIR")
if abraDir == fmt.Sprintf("%s/.abra", os.ExpandEnv("$HOME")) {
log.Fatal("set $ABRA_DIR before running the test suite")
}
func RmServerAppRecipe() {
testAppLink := os.ExpandEnv("$ABRA_DIR/servers/test_server")
os.Remove(testAppLink)
if err := os.RemoveAll(abraDir); err != nil {
log.Fatal(err)
}
testRecipeLink := os.ExpandEnv("$ABRA_DIR/recipes/test_recipe")
os.Remove(testRecipeLink)
}
func Setup() {
Teardown()
if err := os.Mkdir(os.ExpandEnv("$ABRA_DIR"), 0764); err != nil {
if !os.IsExist(err) {
log.Fatal(err)
}
}
func MkServerAppRecipe() {
RmServerAppRecipe()
if err := os.Mkdir(os.ExpandEnv("$ABRA_DIR/servers"), 0700); err != nil {
if !os.IsExist(err) {
@ -59,36 +66,15 @@ func Setup() {
}
}
serverSrcDir := os.ExpandEnv("$PWD/../../tests/resources/test_server")
serverDestDir := os.ExpandEnv("$ABRA_DIR/servers/test_server")
if err := os.CopyFS(serverDestDir, os.DirFS(serverSrcDir)); err != nil {
testAppDir := os.ExpandEnv("$PWD/../../tests/resources/test_server")
testAppLink := os.ExpandEnv("$ABRA_DIR/servers/test_server")
if err := os.Symlink(testAppDir, testAppLink); err != nil {
log.Fatal(err)
}
recipeSrcDir := os.ExpandEnv("$PWD/../../tests/resources/test_recipe")
recipeDestDir := os.ExpandEnv("$ABRA_DIR/recipes/test_recipe")
if err := os.CopyFS(recipeDestDir, os.DirFS(recipeSrcDir)); err != nil {
log.Fatal(err)
}
if err := gitPkg.Init(recipeDestDir, true, "tester", "helo@coopcloud.tech"); err != nil {
testRecipeDir := os.ExpandEnv("$PWD/../../tests/resources/test_recipe")
testRecipeLink := os.ExpandEnv("$ABRA_DIR/recipes/test_recipe")
if err := os.Symlink(testRecipeDir, testRecipeLink); err != nil {
log.Fatal(err)
}
}
func AddEnv(envKey, envValue string) error {
filePath := os.ExpandEnv(fmt.Sprintf("$ABRA_DIR/servers/%s/%s.env", ServerName, AppName))
envVars, _, err := godotenv.Read(filePath)
if err != nil {
return err
}
envVars[envKey] = envValue
if err := godotenv.Write(envVars, filePath); err != nil {
return err
}
return nil
}

View File

@ -11,7 +11,7 @@ import (
"github.com/docker/cli/cli/connhelper/ssh"
"github.com/docker/cli/cli/context/docker"
dCliContextStore "github.com/docker/cli/cli/context/store"
dClient "github.com/docker/docker/client"
dClient "github.com/moby/moby/client"
"github.com/pkg/errors"
)
@ -73,7 +73,7 @@ func getDockerEndpoint(host string) (docker.Endpoint, error) {
if err != nil {
return docker.Endpoint{}, err
}
if _, err := dClient.NewClientWithOpts(opts...); err != nil {
if _, err := dClient.New(opts...); err != nil {
return docker.Endpoint{}, err
}
return ep, nil

View File

@ -138,7 +138,7 @@ func loadConfigFile(filename string) (*composetypes.ConfigFile, error) {
config, err := loader.ParseYAML(bytes)
if err != nil {
return nil, fmt.Errorf("%s: %s", filename, err)
return nil, err
}
return &composetypes.ConfigFile{

View File

@ -19,7 +19,7 @@ import (
"coopcloud.tech/abra/pkg/ui"
"coopcloud.tech/abra/pkg/upstream/convert"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/command/stack/formatter"
"github.com/docker/cli/cli/command/formatter"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"

View File

@ -8,7 +8,7 @@
"gomodTidy"
],
"ignoreDeps": [
"github.com/docker/cli",
"github.com/spf13/cobra"
"github.com/urfave/cli",
"goreleaser/goreleaser"
]
}

View File

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

View File

@ -50,7 +50,7 @@ echo "========================================================================"
echo "========================================================================"
echo "BUILDING ABRA"
echo "========================================================================"
export PATH="$PATH:/usr/local/go/bin"
export PATH="/usr/lib/go-1.21/bin:$PATH"
make build
echo "========================================================================"

View File

@ -106,7 +106,7 @@ teardown(){
run $ABRA app check "$TEST_APP_DOMAIN" --chaos
assert_failure
assert_output --partial 'no such file or directory'
assert_output --partial 'unable to discover .env.sample'
}
@test "error if missing env var" {

View File

@ -23,24 +23,12 @@ 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" {
@ -87,10 +75,8 @@ 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
@ -102,8 +88,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
@ -616,89 +602,3 @@ teardown(){
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"
}

View File

@ -161,7 +161,7 @@ teardown(){
--no-input --no-converge-checks --chaos
assert_success
run $ABRA app ls --status --chaos
run $ABRA app ls --status
assert_success
assert_output --partial "+U"
@ -190,16 +190,13 @@ teardown(){
# bats test_tags=slow
@test "list ignores borked tags" {
run $ABRA app deploy "$TEST_APP_DOMAIN" \
--no-input --no-converge-checks
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag \
-a "2.4.8_1" -m "feat: completely borked tag"
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
_deploy_app
run $ABRA app ls --status --debug
assert_success
assert_output --partial "unable to parse"
assert_output --partial "unable to parse 2.4.8_1"
}

View File

@ -63,30 +63,6 @@ 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")

View File

@ -13,6 +13,7 @@ _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"
@ -20,20 +21,4 @@ _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
}

View File

@ -22,7 +22,7 @@ _remove_tags(){
# expected : 0
# actual : 0
# --
assert_output --regexp '[[:space:]]*0'
assert_output --regexp '[[:space:]]0'
}
_reset_tags() {

View File

@ -5,22 +5,11 @@ _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 "$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"
run git clone "https://git.coopcloud.tech/toolshed/$TEST_RECIPE" "$ABRA_DIR/recipes/$TEST_RECIPE"
assert_success
fi
}
@ -30,10 +19,6 @@ _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
}

View File

@ -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,14 +21,6 @@ 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" {
@ -40,10 +32,10 @@ teardown() {
}
@test "release patch bump" {
run $ABRA recipe upgrade "$TEST_RECIPE" --no-input --patch --commit
run $ABRA recipe upgrade "$TEST_RECIPE" --no-input --patch
assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" show
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" diff
assert_success
assert_output --partial 'image: nginx:1.21.6'
@ -53,9 +45,17 @@ 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 'INFO new release published:'
assert_output --partial 'no -p/--publish passed, not publishing'
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 --commit
run $ABRA recipe upgrade "$TEST_RECIPE" --no-input --minor
assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" show
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" diff
assert_success
assert_output --regexp 'image: nginx:1.2.*'
@ -76,57 +76,58 @@ 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 'INFO new release published:'
assert_output --partial 'no -p/--publish passed, not publishing'
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag --list
assert_success
assert_output --regexp '0\.4\.0\+1\.2.*'
}
@test "release with unstaged changes" {
run bash -c 'echo "# unstaged changes" >> "$ABRA_DIR/recipes/$TEST_RECIPE/compose.yml"'
@test "unknown files not committed" {
run $ABRA recipe upgrade "$TEST_RECIPE" --no-input --patch
assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" diff --quiet
assert_failure
run bash -c 'echo "unstaged changes" >> "$ABRA_DIR/recipes/$TEST_RECIPE/foo"'
assert_success
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
run $ABRA recipe sync "$TEST_RECIPE" --no-input --patch
assert_success
run $ABRA recipe release "$TEST_RECIPE" --no-input --patch
assert_failure
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
assert_output --partial 'no -p/--publish passed, not publishing'
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" add compose.yml
assert_success
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" diff --quiet --cached
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" rm foo
assert_failure
run $ABRA recipe release "$TEST_RECIPE" --no-input --patch
assert_failure
assert_output --partial "working directory not clean"
assert_output --partial "fatal: pathspec 'foo' did not match any files"
}
@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 sync "$TEST_RECIPE" --no-input --patch
assert_success
assert_output --partial 'new release published:'
run $ABRA recipe release "$TEST_RECIPE" --no-input --minor
assert_success
assert_output --partial 'no -p/--publish passed, not publishing'
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/release/next"
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/release/0.4.0+1.21.0"
@ -145,125 +146,14 @@ teardown() {
assert_success
assert_output --regexp 'nginx:1.29.1'
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" commit -am "updated nginx"
run sed -i "s/0.2.0+1.21.0/0.2.0+1.29.1/g" "$ABRA_DIR/recipes/$TEST_RECIPE/compose.yml"
assert_success
run $ABRA recipe release "$TEST_RECIPE" --no-input "0.2.0+1.29.1"
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" diff
assert_success
assert_output --regexp 'coop-cloud\.\$\{STACK_NAME\}\.version=0\.2\.0\+1\.29\.1'
run $ABRA recipe release "$TEST_RECIPE" --no-input --minor
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
}

View File

@ -0,0 +1,200 @@
#!/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
if [[ -d "$ABRA_DIR/recipes/foobar" ]]; then
run rm -rf "$ABRA_DIR/recipes/foobar"
assert_success
fi
}
@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'
}
@test "sync with no tags or previous release" {
_remove_tags
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 recipe without input fails with prompt" {
run $ABRA recipe new foobar
assert_success
assert_exists "$ABRA_DIR/recipes/foobar"
run $ABRA recipe sync foobar --no-input --patch
assert_failure
assert_output --partial "input required for initial version"
}
@test "sync new recipe: development release" {
run $ABRA recipe new foobar
assert_success
assert_exists "$ABRA_DIR/recipes/foobar"
run bash -c "echo 0.1.0 | $ABRA recipe sync foobar --patch"
assert_success
assert_output --regexp 'coop-cloud\.\$\{STACK_NAME\}\.version=0\.1\.0\+1\.2.*'
}
@test "sync new recipe: public release" {
run $ABRA recipe new foobar
assert_success
assert_exists "$ABRA_DIR/recipes/foobar"
run bash -c "echo 1.0.0 | $ABRA recipe sync foobar --patch"
assert_success
assert_output --regexp 'coop-cloud\.\$\{STACK_NAME\}\.version=1\.0\.0\+1\.2.*'
}
@test "sync 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 bash -c "echo 0.1.0 | $ABRA recipe sync foobar --patch"
assert_failure
assert_output --partial "automagic insertion not supported yet"
}

View File

@ -106,37 +106,3 @@ 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"
}

View File

@ -1,9 +1,6 @@
RECIPE=test_recipe
DOMAIN=test_app.example.com
#
# NOTE(d1): for new changes, you *MUST* also update ../test_server/test_app.example.com.env
#
# NOTE(d1): TestReadEnv
# FOO=BAR
# NOTE(d1): ensure commented out TIMEOUT doesn't get included
# see TestReadEnv in ./pkg/envfile
# TIMEOUT=120

View File

@ -8,7 +8,6 @@ services:
- proxy
deploy:
labels:
- "coop-cloud.${STACK_NAME}.version=0.1.0+0.1.0"
- "coop-cloud.${STACK_NAME}.timeout=${TIMEOUT}"
networks:

View File

@ -1,9 +1,6 @@
RECIPE=test_recipe
DOMAIN=test_app.example.com
#
# NOTE(d1): for new changes, you *MUST* also update ../test_recipe/.env.sample
#
# NOTE(d1): TestReadEnv
# FOO=BAR
# NOTE(d1): ensure commented out TIMEOUT doesn't get included
# see TestReadEnv in ./pkg/envfile
# TIMEOUT=120

View File

@ -69,8 +69,6 @@ func (l *lineReader) Read(p []byte) (n int, err error) {
if isPrefix {
return 0, ArmorCorrupt
}
// Trim the line to remove any whitespace
line = bytes.TrimSpace(line)
if bytes.HasPrefix(line, armorEnd) {
l.eof = true

View File

@ -125,10 +125,7 @@ func (c *curve25519) Encaps(rand io.Reader, point []byte) (ephemeral, sharedSecr
// "VB = convert point V to the octet string"
// sharedPoint corresponds to `VB`.
var sharedPoint x25519lib.Key
ok := x25519lib.Shared(&sharedPoint, &ephemeralPrivate, &pubKey)
if !ok {
return nil, nil, errors.KeyInvalidError("ecc: the public key is a low order point")
}
x25519lib.Shared(&sharedPoint, &ephemeralPrivate, &pubKey)
return ephemeralPublic[:], sharedPoint[:], nil
}
@ -149,10 +146,7 @@ func (c *curve25519) Decaps(vsG, secret []byte) (sharedSecret []byte, err error)
// RFC6637 §8: "Note that the recipient obtains the shared secret by calculating
// S = rV = rvG, where (r,R) is the recipient's key pair."
// sharedPoint corresponds to `S`.
ok := x25519lib.Shared(&sharedPoint, &decodedPrivate, &ephemeralPublic)
if !ok {
return nil, errors.KeyInvalidError("ecc: the public key is a low order point")
}
x25519lib.Shared(&sharedPoint, &decodedPrivate, &ephemeralPublic)
return sharedPoint[:], nil
}

View File

@ -78,7 +78,7 @@ func (c *genericCurve) GenerateECDSA(rand io.Reader) (x, y, secret *big.Int, err
func (c *genericCurve) Encaps(rand io.Reader, point []byte) (ephemeral, sharedSecret []byte, err error) {
xP, yP := elliptic.Unmarshal(c.Curve, point)
if xP == nil {
return nil, nil, errors.KeyInvalidError(fmt.Sprintf("ecc (%s): invalid point", c.Curve.Params().Name))
panic("invalid point")
}
d, x, y, err := elliptic.GenerateKey(c.Curve, rand)
@ -99,9 +99,6 @@ func (c *genericCurve) Encaps(rand io.Reader, point []byte) (ephemeral, sharedSe
func (c *genericCurve) Decaps(ephemeral, secret []byte) (sharedSecret []byte, err error) {
x, y := elliptic.Unmarshal(c.Curve, ephemeral)
if x == nil {
return nil, errors.KeyInvalidError(fmt.Sprintf("ecc (%s): invalid point", c.Curve.Params().Name))
}
zbBig, _ := c.Curve.ScalarMult(x, y, secret)
byteLen := (c.Curve.Params().BitSize + 7) >> 3
zb := make([]byte, byteLen)

View File

@ -178,18 +178,6 @@ type Config struct {
// When set to true, a key without flags is treated as if all flags are enabled.
// This behavior is consistent with GPG.
InsecureAllowAllKeyFlagsWhenMissing bool
// InsecureGenerateNonCriticalKeyFlags causes the "Key Flags" signature subpacket
// to be non-critical in newly generated signatures.
// This may be needed for keys to be accepted by older clients who do not recognize
// the subpacket.
// For example, rpm 4.14.3-150400.59.3.1 in OpenSUSE Leap 15.4 does not recognize it.
InsecureGenerateNonCriticalKeyFlags bool
// InsecureGenerateNonCriticalSignatureCreationTime causes the "Signature Creation Time" signature subpacket
// to be non-critical in newly generated signatures.
// This may be needed for keys to be accepted by older clients who do not recognize
// the subpacket.
// For example, yum 3.4.3-168 in CentOS 7 and yum 3.4.3-158 in Amazon Linux 2 do not recognize it.
InsecureGenerateNonCriticalSignatureCreationTime bool
// MaxDecompressedMessageSize specifies the maximum number of bytes that can be
// read from a compressed packet. This serves as an upper limit to prevent
@ -432,20 +420,6 @@ func (c *Config) AllowAllKeyFlagsWhenMissing() bool {
return c.InsecureAllowAllKeyFlagsWhenMissing
}
func (c *Config) GenerateNonCriticalKeyFlags() bool {
if c == nil {
return false
}
return c.InsecureGenerateNonCriticalKeyFlags
}
func (c *Config) GenerateNonCriticalSignatureCreationTime() bool {
if c == nil {
return false
}
return c.InsecureGenerateNonCriticalSignatureCreationTime
}
func (c *Config) DecompressedMessageSizeLimit() *int64 {
if c == nil {
return nil

View File

@ -933,7 +933,7 @@ func (sig *Signature) Sign(h hash.Hash, priv *PrivateKey, config *Config) (err e
}
sig.Notations = append(sig.Notations, &notation)
}
sig.outSubpackets, err = sig.buildSubpackets(priv.PublicKey, config)
sig.outSubpackets, err = sig.buildSubpackets(priv.PublicKey)
if err != nil {
return err
}
@ -1254,11 +1254,11 @@ type outputSubpacket struct {
contents []byte
}
func (sig *Signature) buildSubpackets(issuer PublicKey, config *Config) (subpackets []outputSubpacket, err error) {
func (sig *Signature) buildSubpackets(issuer PublicKey) (subpackets []outputSubpacket, err error) {
creationTime := make([]byte, 4)
binary.BigEndian.PutUint32(creationTime, uint32(sig.CreationTime.Unix()))
// Signature Creation Time
subpackets = append(subpackets, outputSubpacket{true, creationTimeSubpacket, !config.GenerateNonCriticalSignatureCreationTime(), creationTime})
subpackets = append(subpackets, outputSubpacket{true, creationTimeSubpacket, true, creationTime})
// Signature Expiration Time
if sig.SigLifetimeSecs != nil && *sig.SigLifetimeSecs != 0 {
sigLifetime := make([]byte, 4)
@ -1357,7 +1357,7 @@ func (sig *Signature) buildSubpackets(issuer PublicKey, config *Config) (subpack
if sig.FlagGroupKey {
flags |= KeyFlagGroupKey
}
subpackets = append(subpackets, outputSubpacket{true, keyFlagsSubpacket, !config.GenerateNonCriticalKeyFlags(), []byte{flags}})
subpackets = append(subpackets, outputSubpacket{true, keyFlagsSubpacket, true, []byte{flags}})
}
// Signer's User ID
if sig.SignerUserId != nil {

View File

@ -2,7 +2,6 @@ package colorprofile
import (
"bytes"
"context"
"io"
"os/exec"
"runtime"
@ -174,11 +173,6 @@ func envColorProfile(env environ) (p Profile) {
}
}
if len(env["WT_SESSION"]) > 0 {
// Windows Terminal supports TrueColor
return TrueColor
}
if isCloudShell, _ := strconv.ParseBool(env.get("GOOGLE_CLOUD_SHELL")); isCloudShell {
return TrueColor
}
@ -245,13 +239,13 @@ func tmux(env environ) (p Profile) {
// Check if tmux has either Tc or RGB capabilities. Otherwise, return
// ANSI256.
p = ANSI256
cmd := exec.CommandContext(context.Background(), "tmux", "info")
cmd := exec.Command("tmux", "info")
out, err := cmd.Output()
if err != nil {
return
}
for line := range bytes.SplitSeq(out, []byte("\n")) {
for _, line := range bytes.Split(out, []byte("\n")) {
if (bytes.Contains(line, []byte("Tc")) || bytes.Contains(line, []byte("RGB"))) &&
bytes.Contains(line, []byte("true")) {
return TrueColor

View File

@ -14,6 +14,11 @@ func windowsColorProfile(env map[string]string) (Profile, bool) {
return TrueColor, true
}
if len(env["WT_SESSION"]) > 0 {
// Windows Terminal supports TrueColor
return TrueColor, true
}
major, _, build := windows.RtlGetNtVersionNumbers()
if build < 10586 || major < 10 {
// No ANSI support before WindowsNT 10 build 10586

View File

@ -40,11 +40,9 @@ func (w *Writer) Write(p []byte) (int, error) {
case w.Profile == TrueColor:
return w.Forward.Write(p) //nolint:wrapcheck
case w.Profile <= NoTTY:
_, err := io.WriteString(w.Forward, ansi.Strip(string(p)))
return len(p), err
return io.WriteString(w.Forward, ansi.Strip(string(p))) //nolint:wrapcheck
case w.Profile == ASCII, w.Profile == ANSI, w.Profile == ANSI256:
_, err := w.downsample(p)
return len(p), err
return w.downsample(p)
default:
return 0, fmt.Errorf("invalid profile: %v", w.Profile)
}

View File

@ -7,6 +7,7 @@ linters:
- exhaustive
- goconst
- godot
- godox
- gomoddirectives
- goprintffuncname
- gosec
@ -26,16 +27,9 @@ linters:
- whitespace
- wrapcheck
exclusions:
rules:
- text: '(slog|log)\.\w+'
linters:
- noctx
generated: lax
presets:
- common-false-positives
settings:
exhaustive:
default-signifies-exhaustive: true
issues:
max-issues-per-linter: 0
max-same-issues: 0

View File

@ -7,7 +7,7 @@ import (
"time"
)
func (l *Logger) jsonFormatter(keyvals ...any) {
func (l *Logger) jsonFormatter(keyvals ...interface{}) {
jw := &jsonWriter{w: &l.b}
jw.start()

View File

@ -8,7 +8,7 @@ import (
"github.com/go-logfmt/logfmt"
)
func (l *Logger) logfmtFormatter(keyvals ...any) {
func (l *Logger) logfmtFormatter(keyvals ...interface{}) {
e := logfmt.NewEncoder(&l.b)
for i := 0; i < len(keyvals); i += 2 {

View File

@ -41,19 +41,19 @@ type Logger struct {
reportCaller bool
reportTimestamp bool
fields []any
fields []interface{}
helpers *sync.Map
styles *Styles
}
// Logf logs a message with formatting.
func (l *Logger) Logf(level Level, format string, args ...any) {
func (l *Logger) Logf(level Level, format string, args ...interface{}) {
l.Log(level, fmt.Sprintf(format, args...))
}
// Log logs the given message with the given keyvals for the given level.
func (l *Logger) Log(level Level, msg any, keyvals ...any) {
func (l *Logger) Log(level Level, msg interface{}, keyvals ...interface{}) {
if atomic.LoadUint32(&l.isDiscard) != 0 {
return
}
@ -81,8 +81,8 @@ func (l *Logger) Log(level Level, msg any, keyvals ...any) {
l.handle(level, l.timeFunc(time.Now()), []runtime.Frame{frame}, msg, keyvals...)
}
func (l *Logger) handle(level Level, ts time.Time, frames []runtime.Frame, msg any, keyvals ...any) {
var kvs []any
func (l *Logger) handle(level Level, ts time.Time, frames []runtime.Frame, msg interface{}, keyvals ...interface{}) {
var kvs []interface{}
if l.reportTimestamp && !ts.IsZero() {
kvs = append(kvs, TimestampKey, ts)
}
@ -327,7 +327,7 @@ func (l *Logger) SetStyles(s *Styles) {
}
// With returns a new logger with the given keyvals added.
func (l *Logger) With(keyvals ...any) *Logger {
func (l *Logger) With(keyvals ...interface{}) *Logger {
var st Styles
l.mu.Lock()
sl := *l
@ -336,7 +336,7 @@ func (l *Logger) With(keyvals ...any) *Logger {
sl.b = bytes.Buffer{}
sl.mu = &sync.RWMutex{}
sl.helpers = &sync.Map{}
sl.fields = append(make([]any, 0, len(l.fields)+len(keyvals)), l.fields...)
sl.fields = append(make([]interface{}, 0, len(l.fields)+len(keyvals)), l.fields...)
sl.fields = append(sl.fields, keyvals...)
sl.styles = &st
return &sl
@ -350,63 +350,63 @@ func (l *Logger) WithPrefix(prefix string) *Logger {
}
// Debug prints a debug message.
func (l *Logger) Debug(msg any, keyvals ...any) {
func (l *Logger) Debug(msg interface{}, keyvals ...interface{}) {
l.Log(DebugLevel, msg, keyvals...)
}
// Info prints an info message.
func (l *Logger) Info(msg any, keyvals ...any) {
func (l *Logger) Info(msg interface{}, keyvals ...interface{}) {
l.Log(InfoLevel, msg, keyvals...)
}
// Warn prints a warning message.
func (l *Logger) Warn(msg any, keyvals ...any) {
func (l *Logger) Warn(msg interface{}, keyvals ...interface{}) {
l.Log(WarnLevel, msg, keyvals...)
}
// Error prints an error message.
func (l *Logger) Error(msg any, keyvals ...any) {
func (l *Logger) Error(msg interface{}, keyvals ...interface{}) {
l.Log(ErrorLevel, msg, keyvals...)
}
// Fatal prints a fatal message and exits.
func (l *Logger) Fatal(msg any, keyvals ...any) {
func (l *Logger) Fatal(msg interface{}, keyvals ...interface{}) {
l.Log(FatalLevel, msg, keyvals...)
os.Exit(1)
}
// Print prints a message with no level.
func (l *Logger) Print(msg any, keyvals ...any) {
func (l *Logger) Print(msg interface{}, keyvals ...interface{}) {
l.Log(noLevel, msg, keyvals...)
}
// Debugf prints a debug message with formatting.
func (l *Logger) Debugf(format string, args ...any) {
func (l *Logger) Debugf(format string, args ...interface{}) {
l.Log(DebugLevel, fmt.Sprintf(format, args...))
}
// Infof prints an info message with formatting.
func (l *Logger) Infof(format string, args ...any) {
func (l *Logger) Infof(format string, args ...interface{}) {
l.Log(InfoLevel, fmt.Sprintf(format, args...))
}
// Warnf prints a warning message with formatting.
func (l *Logger) Warnf(format string, args ...any) {
func (l *Logger) Warnf(format string, args ...interface{}) {
l.Log(WarnLevel, fmt.Sprintf(format, args...))
}
// Errorf prints an error message with formatting.
func (l *Logger) Errorf(format string, args ...any) {
func (l *Logger) Errorf(format string, args ...interface{}) {
l.Log(ErrorLevel, fmt.Sprintf(format, args...))
}
// Fatalf prints a fatal message with formatting and exits.
func (l *Logger) Fatalf(format string, args ...any) {
func (l *Logger) Fatalf(format string, args ...interface{}) {
l.Log(FatalLevel, fmt.Sprintf(format, args...))
os.Exit(1)
}
// Printf prints a message with no level and formatting.
func (l *Logger) Printf(format string, args ...any) {
func (l *Logger) Printf(format string, args ...interface{}) {
l.Log(noLevel, fmt.Sprintf(format, args...))
}

View File

@ -36,7 +36,7 @@ func (l *Logger) Handle(ctx context.Context, record slog.Record) error {
return nil
}
fields := make([]any, 0, record.NumAttrs()*2)
fields := make([]interface{}, 0, record.NumAttrs()*2)
record.Attrs(func(a slog.Attr) bool {
fields = append(fields, a.Key, a.Value)
return true
@ -52,7 +52,7 @@ func (l *Logger) Handle(ctx context.Context, record slog.Record) error {
//
// Implements slog.Handler.
func (l *Logger) WithAttrs(attrs []slog.Attr) slog.Handler {
fields := make([]any, 0, len(attrs)*2)
fields := make([]interface{}, 0, len(attrs)*2)
for _, attr := range attrs {
fields = append(fields, attr.Key, attr.Value)
}

View File

@ -33,7 +33,7 @@ func (l *Logger) Enabled(_ context.Context, level slog.Level) bool {
//
// Implements slog.Handler.
func (l *Logger) Handle(_ context.Context, record slog.Record) error {
fields := make([]any, 0, record.NumAttrs()*2)
fields := make([]interface{}, 0, record.NumAttrs()*2)
record.Attrs(func(a slog.Attr) bool {
fields = append(fields, a.Key, a.Value)
return true
@ -49,7 +49,7 @@ func (l *Logger) Handle(_ context.Context, record slog.Record) error {
//
// Implements slog.Handler.
func (l *Logger) WithAttrs(attrs []slog.Attr) slog.Handler {
fields := make([]any, 0, len(attrs)*2)
fields := make([]interface{}, 0, len(attrs)*2)
for _, attr := range attrs {
fields = append(fields, attr.Key, attr.Value)
}

View File

@ -55,7 +55,7 @@ type Options struct {
// CallerOffset is the caller format for the logger. The default is 0.
CallerOffset int
// Fields is the fields for the logger. The default is no fields.
Fields []any
Fields []interface{}
// Formatter is the formatter for the logger. The default is TextFormatter.
Formatter Formatter
}

View File

@ -155,7 +155,7 @@ func GetPrefix() string {
}
// With returns a new logger with the given keyvals.
func With(keyvals ...any) *Logger {
func With(keyvals ...interface{}) *Logger {
return Default().With(keyvals...)
}
@ -172,74 +172,74 @@ func Helper() {
}
// Log logs a message with the given level.
func Log(level Level, msg any, keyvals ...any) {
func Log(level Level, msg interface{}, keyvals ...interface{}) {
Default().Log(level, msg, keyvals...)
}
// Debug logs a debug message.
func Debug(msg any, keyvals ...any) {
func Debug(msg interface{}, keyvals ...interface{}) {
Default().Log(DebugLevel, msg, keyvals...)
}
// Info logs an info message.
func Info(msg any, keyvals ...any) {
func Info(msg interface{}, keyvals ...interface{}) {
Default().Log(InfoLevel, msg, keyvals...)
}
// Warn logs a warning message.
func Warn(msg any, keyvals ...any) {
func Warn(msg interface{}, keyvals ...interface{}) {
Default().Log(WarnLevel, msg, keyvals...)
}
// Error logs an error message.
func Error(msg any, keyvals ...any) {
func Error(msg interface{}, keyvals ...interface{}) {
Default().Log(ErrorLevel, msg, keyvals...)
}
// Fatal logs a fatal message and exit.
func Fatal(msg any, keyvals ...any) {
func Fatal(msg interface{}, keyvals ...interface{}) {
Default().Log(FatalLevel, msg, keyvals...)
os.Exit(1)
}
// Print logs a message with no level.
func Print(msg any, keyvals ...any) {
func Print(msg interface{}, keyvals ...interface{}) {
Default().Log(noLevel, msg, keyvals...)
}
// Logf logs a message with formatting and level.
func Logf(level Level, format string, args ...any) {
func Logf(level Level, format string, args ...interface{}) {
Default().Logf(level, format, args...)
}
// Debugf logs a debug message with formatting.
func Debugf(format string, args ...any) {
func Debugf(format string, args ...interface{}) {
Default().Log(DebugLevel, fmt.Sprintf(format, args...))
}
// Infof logs an info message with formatting.
func Infof(format string, args ...any) {
func Infof(format string, args ...interface{}) {
Default().Log(InfoLevel, fmt.Sprintf(format, args...))
}
// Warnf logs a warning message with formatting.
func Warnf(format string, args ...any) {
func Warnf(format string, args ...interface{}) {
Default().Log(WarnLevel, fmt.Sprintf(format, args...))
}
// Errorf logs an error message with formatting.
func Errorf(format string, args ...any) {
func Errorf(format string, args ...interface{}) {
Default().Log(ErrorLevel, fmt.Sprintf(format, args...))
}
// Fatalf logs a fatal message with formatting and exit.
func Fatalf(format string, args ...any) {
func Fatalf(format string, args ...interface{}) {
Default().Log(FatalLevel, fmt.Sprintf(format, args...))
os.Exit(1)
}
// Printf logs a message with formatting and no level.
func Printf(format string, args ...any) {
func Printf(format string, args ...interface{}) {
Default().Log(noLevel, fmt.Sprintf(format, args...))
}

View File

@ -62,7 +62,7 @@ const (
)
var bufPool = sync.Pool{
New: func() any {
New: func() interface{} {
return new(strings.Builder)
},
}
@ -164,7 +164,7 @@ func writeSpace(w io.Writer, first bool) {
}
}
func (l *Logger) textFormatter(keyvals ...any) {
func (l *Logger) textFormatter(keyvals ...interface{}) {
st := l.styles
lenKeyvals := len(keyvals)

View File

@ -1,29 +1,5 @@
package ansi
import (
"os"
"strconv"
"github.com/clipperhouse/displaywidth"
"github.com/mattn/go-runewidth"
)
var wcOptions = &runewidth.Condition{
EastAsianWidth: false,
StrictEmojiNeutral: true,
}
var dwOptions = &displaywidth.Options{
EastAsianWidth: false,
}
func init() {
if ea, err := strconv.ParseBool(os.Getenv("RUNEWIDTH_EASTASIAN")); err == nil && ea {
wcOptions.EastAsianWidth = true
dwOptions.EastAsianWidth = true
}
}
// Method is a type that represents the how the renderer should calculate the
// display width of cells.
type Method uint8

View File

@ -4,7 +4,9 @@ import (
"unicode/utf8"
"github.com/charmbracelet/x/ansi/parser"
"github.com/clipperhouse/displaywidth"
"github.com/clipperhouse/uax29/v2/graphemes"
"github.com/mattn/go-runewidth"
)
// State represents the state of the ANSI escape sequence parser used by
@ -437,15 +439,15 @@ func FirstGraphemeCluster[T string | []byte](b T, m Method) (T, int) {
case string:
cluster := graphemes.FromString(b).First()
if m == WcWidth {
return T(cluster), wcOptions.StringWidth(cluster)
return T(cluster), runewidth.StringWidth(cluster)
}
return T(cluster), dwOptions.String(cluster)
return T(cluster), displaywidth.String(cluster)
case []byte:
cluster := graphemes.FromBytes(b).First()
if m == WcWidth {
return T(cluster), wcOptions.StringWidth(string(cluster))
return T(cluster), runewidth.StringWidth(string(cluster))
}
return T(cluster), dwOptions.Bytes(cluster)
return T(cluster), displaywidth.Bytes(cluster)
}
panic("unreachable")
}

View File

@ -10,7 +10,7 @@ var parserPool = sync.Pool{
New: func() any {
p := NewParser()
p.SetParamsSize(parser.MaxParamsSize)
p.SetDataSize(1024 * 4) // 4KB of data buffer
p.SetDataSize(1024 * 1024 * 4) // 4MB of data buffer
return p
},
}

View File

@ -1,3 +1,2 @@
.DS_Store
*.out
*.test

View File

@ -18,27 +18,13 @@ by running `go generate` from the top package directory.
## Pull Requests and branches
For PRs (pull requests), you can use the gh CLI tool. Compare the current branch with main. Reviewing a PR and reviewing a branch are about the same, but the PR may add context.
For PRs (pull requests), you can use the gh CLI tool to retrieve details,
or post comments. Then, compare the current branch with main. Reviewing a PR
and reviewing a branch are about the same, but the PR may add context.
Understand the goals of the PR. Note any API changes, especially breaking changes.
Look for bugs. Think like GitHub Copilot or Cursor BugBot.
Look for thoroughness of tests, as well as GoDoc comments.
Retrieve and consider the comments on the PR, which may have come from GitHub Copilot or Cursor BugBot. Think like GitHub Copilot or Cursor BugBot.
Offer to optionally post a brief summary of the review to the PR, via the gh CLI tool.
## Tagged Go releases
If I ask you whether we are ready to release, this means a tagged Go release on the main branch. Go releases are git tagged with a version number.
Review the changes since the last release, i.e. the previous git tag. Ensure that the changes are complete and correct. Identify new features, bug fixes, and performance improvements.
Identify breaking changes, especially API changes.
Ensure good test coverage. Look for performance changes, especially performance regressions, by running benchmarks against the previous release.
Ensure that the documentation in READMEs and GoDocs are complete, correct and consistent.
Offer to post a brief summary of the review to the PR, via the gh CLI tool.
## Comparisons to go-runewidth

View File

@ -1,64 +1,5 @@
# Changelog
## [0.11.0]
[Compare](https://github.com/clipperhouse/displaywidth/compare/v0.10.0...v0.11.0)
### Added
- New `ControlSequences8Bit` option to treat 8-bit ECMA-48 (C1) escape sequences as zero-width. (#22)
### Changed
- Upgraded uax29 dependency to v2.7.0 for 8-bit escape sequence support in the grapheme iterator.
- Truncation now validates that preserved trailing escape sequences are zero-width, preventing edge cases where non-zero-width sequences could leak into output.
### Note
- `ControlSequences8Bit` is deliberately ignored by `TruncateString` and `TruncateBytes`, because C1 byte values (0x800x9F) overlap with UTF-8 multi-byte encoding.
## [0.10.0]
[Compare](https://github.com/clipperhouse/displaywidth/compare/v0.9.0...v0.10.0)
### Added
- New `ControlSequences` option to treat ECMA-48/ANSI escape sequences as zero-width. (#20)
- `TruncateString` and `TruncateBytes` now preserve trailing ANSI escape sequences (such as SGR resets) when `ControlSequences` is true, preventing color bleed in terminal output.
### Changed
- Removed `stringish` dependency; generic type constraints are now inline `~string | []byte`.
- Upgraded uax29 dependency to v2.6.0 for ANSI escape sequence support in the grapheme iterator.
## [0.9.0]
[Compare](https://github.com/clipperhouse/displaywidth/compare/v0.8.0...v0.9.0)
### Changed
- Unicode 17 support: East Asian Width and emoji data updated to Unicode 17.0.0. (#18)
- Upgraded uax29 dependency to v2.5.0 (Unicode 17 grapheme segmentation).
## [0.8.0]
[Compare](https://github.com/clipperhouse/displaywidth/compare/v0.7.0...v0.8.0)
### Changed
- Performance: ASCII fast path that applies to any run of printable
ASCII. 2x-10x faster for ASCII text vs v0.7.0. (#16)
- Upgraded uax29 dependency to v2.4.0 for Unicode 16 support. Text that includes
Indic_Conjunct_Break may segment differently (and more correctly). (#15)
## [0.7.0]
[Compare](https://github.com/clipperhouse/displaywidth/compare/v0.6.2...v0.7.0)
### Added
- New `TruncateString` and `TruncateBytes` methods to truncate strings to a
maximum display width, with optional tail (like an ellipsis). (#13)
## [0.6.2]
[Compare](https://github.com/clipperhouse/displaywidth/compare/v0.6.1...v0.6.2)
### Changed
- Internal: reduced property categories for simpler trie.
## [0.6.1]
[Compare](https://github.com/clipperhouse/displaywidth/compare/v0.6.0...v0.6.1)
@ -78,7 +19,7 @@
widths of grapheme clusters.
### Changed
- Fast ASCII lookups
- Added ASCII fast paths
## [0.5.0]

View File

@ -61,38 +61,8 @@ func main() {
### Options
Create the options you need, and then use methods on the options struct.
```go
var myOptions = displaywidth.Options{
EastAsianWidth: true,
ControlSequences: true,
}
width := myOptions.String("Hello, 世界!")
```
#### ControlSequences
`ControlSequences` specifies whether to ignore ECMA-48 escape sequences
when calculating the display width. When `false` (default), ANSI escape
sequences are treated as just a series of characters. When `true`, they are
treated as a single zero-width unit.
#### ControlSequences8Bit
`ControlSequences8Bit` specifies whether to ignore 8-bit ECMA-48 escape sequences
when calculating the display width. When `false` (default), these are treated
as just a series of characters. When `true`, they are treated as a single
zero-width unit.
Note: this option is ignored by the `Truncate` methods, as the concatenation
can lead to unintended UTF-8 semantics.
#### EastAsianWidth
`EastAsianWidth` defines how
[East Asian Ambiguous characters](https://www.unicode.org/reports/tr11/#Ambiguous)
There is one option, `displaywidth.Options.EastAsianWidth`, which defines
how [East Asian Ambiguous characters](https://www.unicode.org/reports/tr11/#Ambiguous)
are treated.
When `false` (default), East Asian Ambiguous characters are treated as width 1.
@ -100,8 +70,26 @@ When `true`, they are treated as width 2.
You may wish to configure this based on environment variables or locale.
`go-runewidth`, for example, does so
[during package initialization](https://github.com/mattn/go-runewidth/blob/master/runewidth.go#L26C1-L45C2). `displaywidth` does not do this automatically, we prefer to leave it to you.
[during package initialization](https://github.com/mattn/go-runewidth/blob/master/runewidth.go#L26C1-L45C2).
`displaywidth` does not do this automatically, we prefer to leave it to you.
You might do something like:
```go
var width displaywidth.Options // zero value is default
func init() {
if os.Getenv("EAST_ASIAN_WIDTH") == "true" {
width = displaywidth.Options{EastAsianWidth: true}
}
// or check locale, or any other logic you want
}
// use it in your logic
func myApp() {
fmt.Println(width.String("Hello, 世界!"))
}
```
## Technical standards and compatibility
@ -109,25 +97,18 @@ This package implements the Unicode East Asian Width standard
([UAX #11](https://www.unicode.org/reports/tr11/tr11-43.html)), and handles
[version selectors](https://en.wikipedia.org/wiki/Variation_Selectors_(Unicode_block)),
and [regional indicator pairs](https://en.wikipedia.org/wiki/Regional_indicator_symbol)
(flags). We implement [Unicode TR51](https://www.unicode.org/reports/tr51/tr51-27.html)
for emojis. We are keeping an eye on
[emerging standards](https://www.jeffquast.com/post/state-of-terminal-emulation-2025/).
(flags). We implement [Unicode TR51](https://www.unicode.org/reports/tr51/tr51-27.html). We are keeping
an eye on [emerging standards](https://www.jeffquast.com/post/state-of-terminal-emulation-2025/).
For control sequences, we implement the [ECMA-48](https://ecma-international.org/publications-and-standards/standards/ecma-48/) standard for 7-bit and 8-bit control sequences.
`clipperhouse/displaywidth`, `mattn/go-runewidth`, and `rivo/uniseg` will
give the same outputs for most real-world text. Extensive details are in the
[compatibility analysis](comparison/COMPATIBILITY_ANALYSIS.md).
## Invalid UTF-8
If you wish to investigate the core logic, see the `lookupProperties` and `width`
functions in [width.go](width.go#L139). The essential trie generation logic is in
`buildPropertyBitmap` in [unicode.go](internal/gen/unicode.go#L316).
This package does not validate UTF-8. If you pass invalid UTF-8, the results
are undefined. We fuzz against invalid UTF-8 to ensure we don't panic or
loop indefinitely.
The `ControlSequences8Bit` option means that we will segment valid 8-bit
control sequences, which are typically _not_ valid UTF-8. 8-bit control bytes
happen to also be UTF-8 continuation bytes. Use with caution.
## Prior Art
@ -152,39 +133,39 @@ goarch: arm64
pkg: github.com/clipperhouse/displaywidth/comparison
cpu: Apple M2
BenchmarkString_Mixed/clipperhouse/displaywidth-8 5784 ns/op 291.69 MB/s 0 B/op 0 allocs/op
BenchmarkString_Mixed/mattn/go-runewidth-8 14751 ns/op 114.36 MB/s 0 B/op 0 allocs/op
BenchmarkString_Mixed/rivo/uniseg-8 19360 ns/op 87.14 MB/s 0 B/op 0 allocs/op
BenchmarkString_Mixed/clipperhouse/displaywidth-8 10400 ns/op 162.21 MB/s 0 B/op 0 allocs/op
BenchmarkString_Mixed/mattn/go-runewidth-8 14296 ns/op 118.00 MB/s 0 B/op 0 allocs/op
BenchmarkString_Mixed/rivo/uniseg-8 19770 ns/op 85.33 MB/s 0 B/op 0 allocs/op
BenchmarkString_ASCII/clipperhouse/displaywidth-8 54.60 ns/op 2344.32 MB/s 0 B/op 0 allocs/op
BenchmarkString_ASCII/mattn/go-runewidth-8 1195 ns/op 107.08 MB/s 0 B/op 0 allocs/op
BenchmarkString_ASCII/rivo/uniseg-8 1578 ns/op 81.13 MB/s 0 B/op 0 allocs/op
BenchmarkString_EastAsian/clipperhouse/displaywidth-8 10593 ns/op 159.26 MB/s 0 B/op 0 allocs/op
BenchmarkString_EastAsian/mattn/go-runewidth-8 23980 ns/op 70.35 MB/s 0 B/op 0 allocs/op
BenchmarkString_EastAsian/rivo/uniseg-8 19777 ns/op 85.30 MB/s 0 B/op 0 allocs/op
BenchmarkString_EastAsian/clipperhouse/displaywidth-8 5837 ns/op 289.01 MB/s 0 B/op 0 allocs/op
BenchmarkString_EastAsian/mattn/go-runewidth-8 24418 ns/op 69.09 MB/s 0 B/op 0 allocs/op
BenchmarkString_EastAsian/rivo/uniseg-8 19339 ns/op 87.23 MB/s 0 B/op 0 allocs/op
BenchmarkString_ASCII/clipperhouse/displaywidth-8 1032 ns/op 124.09 MB/s 0 B/op 0 allocs/op
BenchmarkString_ASCII/mattn/go-runewidth-8 1162 ns/op 110.16 MB/s 0 B/op 0 allocs/op
BenchmarkString_ASCII/rivo/uniseg-8 1586 ns/op 80.69 MB/s 0 B/op 0 allocs/op
BenchmarkString_Emoji/clipperhouse/displaywidth-8 3225 ns/op 224.51 MB/s 0 B/op 0 allocs/op
BenchmarkString_Emoji/mattn/go-runewidth-8 4851 ns/op 149.25 MB/s 0 B/op 0 allocs/op
BenchmarkString_Emoji/rivo/uniseg-8 6591 ns/op 109.85 MB/s 0 B/op 0 allocs/op
BenchmarkString_Emoji/clipperhouse/displaywidth-8 3017 ns/op 240.01 MB/s 0 B/op 0 allocs/op
BenchmarkString_Emoji/mattn/go-runewidth-8 4745 ns/op 152.58 MB/s 0 B/op 0 allocs/op
BenchmarkString_Emoji/rivo/uniseg-8 6745 ns/op 107.34 MB/s 0 B/op 0 allocs/op
BenchmarkRune_Mixed/clipperhouse/displaywidth-8 3385 ns/op 498.34 MB/s 0 B/op 0 allocs/op
BenchmarkRune_Mixed/mattn/go-runewidth-8 5354 ns/op 315.07 MB/s 0 B/op 0 allocs/op
BenchmarkRune_Mixed/clipperhouse/displaywidth-8 3381 ns/op 498.90 MB/s 0 B/op 0 allocs/op
BenchmarkRune_Mixed/mattn/go-runewidth-8 5383 ns/op 313.41 MB/s 0 B/op 0 allocs/op
BenchmarkRune_EastAsian/clipperhouse/displaywidth-8 3397 ns/op 496.56 MB/s 0 B/op 0 allocs/op
BenchmarkRune_EastAsian/mattn/go-runewidth-8 15673 ns/op 107.64 MB/s 0 B/op 0 allocs/op
BenchmarkRune_EastAsian/clipperhouse/displaywidth-8 3395 ns/op 496.96 MB/s 0 B/op 0 allocs/op
BenchmarkRune_EastAsian/mattn/go-runewidth-8 15645 ns/op 107.83 MB/s 0 B/op 0 allocs/op
BenchmarkRune_ASCII/clipperhouse/displaywidth-8 255.7 ns/op 500.53 MB/s 0 B/op 0 allocs/op
BenchmarkRune_ASCII/mattn/go-runewidth-8 261.5 ns/op 489.55 MB/s 0 B/op 0 allocs/op
BenchmarkRune_ASCII/clipperhouse/displaywidth-8 257.8 ns/op 496.57 MB/s 0 B/op 0 allocs/op
BenchmarkRune_ASCII/mattn/go-runewidth-8 267.3 ns/op 478.89 MB/s 0 B/op 0 allocs/op
BenchmarkRune_Emoji/clipperhouse/displaywidth-8 1371 ns/op 528.22 MB/s 0 B/op 0 allocs/op
BenchmarkRune_Emoji/mattn/go-runewidth-8 2267 ns/op 319.43 MB/s 0 B/op 0 allocs/op
BenchmarkRune_Emoji/clipperhouse/displaywidth-8 1338 ns/op 541.24 MB/s 0 B/op 0 allocs/op
BenchmarkRune_Emoji/mattn/go-runewidth-8 2287 ns/op 316.58 MB/s 0 B/op 0 allocs/op
BenchmarkTruncateWithTail/clipperhouse/displaywidth-8 3229 ns/op 54.82 MB/s 192 B/op 14 allocs/op
BenchmarkTruncateWithTail/mattn/go-runewidth-8 8408 ns/op 21.05 MB/s 192 B/op 14 allocs/op
BenchmarkTruncateWithTail/clipperhouse/displaywidth-8 3689 ns/op 47.98 MB/s 192 B/op 14 allocs/op
BenchmarkTruncateWithTail/mattn/go-runewidth-8 8069 ns/op 21.93 MB/s 192 B/op 14 allocs/op
BenchmarkTruncateWithoutTail/clipperhouse/displaywidth-8 3554 ns/op 64.43 MB/s 0 B/op 0 allocs/op
BenchmarkTruncateWithoutTail/mattn/go-runewidth-8 11189 ns/op 20.47 MB/s 0 B/op 0 allocs/op
BenchmarkTruncateWithoutTail/clipperhouse/displaywidth-8 3457 ns/op 66.24 MB/s 0 B/op 0 allocs/op
BenchmarkTruncateWithoutTail/mattn/go-runewidth-8 10441 ns/op 21.93 MB/s 0 B/op 0 allocs/op
```
Here are some notes on [how to make Unicode things fast](https://clipperhouse.com/go-unicode/).

View File

@ -1,6 +1,7 @@
package displaywidth
import (
"github.com/clipperhouse/stringish"
"github.com/clipperhouse/uax29/v2/graphemes"
)
@ -8,8 +9,8 @@ import (
//
// Iterate using the Next method, and get the width of the current grapheme
// using the Width method.
type Graphemes[T ~string | []byte] struct {
iter *graphemes.Iterator[T]
type Graphemes[T stringish.Interface] struct {
iter graphemes.Iterator[T]
options Options
}
@ -43,11 +44,10 @@ func StringGraphemes(s string) Graphemes[string] {
// Iterate using the Next method, and get the width of the current grapheme
// using the Width method.
func (options Options) StringGraphemes(s string) Graphemes[string] {
g := graphemes.FromString(s)
g.AnsiEscapeSequences = options.ControlSequences
g.AnsiEscapeSequences8Bit = options.ControlSequences8Bit
return Graphemes[string]{iter: g, options: options}
return Graphemes[string]{
iter: graphemes.FromString(s),
options: options,
}
}
// BytesGraphemes returns an iterator over grapheme clusters for the given
@ -65,9 +65,8 @@ func BytesGraphemes(s []byte) Graphemes[[]byte] {
// Iterate using the Next method, and get the width of the current grapheme
// using the Width method.
func (options Options) BytesGraphemes(s []byte) Graphemes[[]byte] {
g := graphemes.FromBytes(s)
g.AnsiEscapeSequences = options.ControlSequences
g.AnsiEscapeSequences8Bit = options.ControlSequences8Bit
return Graphemes[[]byte]{iter: g, options: options}
return Graphemes[[]byte]{
iter: graphemes.FromBytes(s),
options: options,
}
}

View File

@ -1,30 +0,0 @@
package displaywidth
// Options allows you to specify the treatment of ambiguous East Asian
// characters and ANSI escape sequences.
type Options struct {
// EastAsianWidth specifies whether to treat ambiguous East Asian characters
// as width 1 or 2. When false (default), ambiguous East Asian characters
// are treated as width 1. When true, they are width 2.
EastAsianWidth bool
// ControlSequences specifies whether to ignore 7-bit ECMA-48 escape sequences
// when calculating the display width. When false (default), ANSI escape
// sequences are treated as just a series of characters. When true, they are
// treated as a single zero-width unit.
ControlSequences bool
// ControlSequences8Bit specifies whether to ignore 8-bit ECMA-48 escape sequences
// when calculating the display width. When false (default), these are treated
// as just a series of characters. When true, they are treated as a single
// zero-width unit.
ControlSequences8Bit bool
}
// DefaultOptions is the default options for the display width
// calculation, which is EastAsianWidth false, ControlSequences false, and
// ControlSequences8Bit false.
var DefaultOptions = Options{
EastAsianWidth: false,
ControlSequences: false,
ControlSequences8Bit: false,
}

View File

@ -2,6 +2,8 @@
package displaywidth
import "github.com/clipperhouse/stringish"
// property is an enum representing the properties of a character
type property uint8
@ -17,7 +19,7 @@ const (
// lookup returns the trie value for the first UTF-8 encoding in s and
// the width in bytes of this encoding. The size will be 0 if s does not
// hold enough bytes to complete the encoding. len(s) must be greater than 0.
func lookup[T ~string | []byte](s T) (v uint8, sz int) {
func lookup[T stringish.Interface](s T) (v uint8, sz int) {
c0 := s[0]
switch {
case c0 < 0x80: // is ASCII
@ -77,7 +79,7 @@ func lookup[T ~string | []byte](s T) (v uint8, sz int) {
return 0, 1
}
// stringWidthTrie. Total size: 17664 bytes (17.25 KiB). Checksum: 220983462f26d765.
// stringWidthTrie. Total size: 17664 bytes (17.25 KiB). Checksum: c77d82ff2d69f0d2.
// type stringWidthTrie struct { }
// func newStringWidthTrie(i int) *stringWidthTrie {
@ -1131,31 +1133,27 @@ var stringWidthValues = [15744]uint8{
// Block 0xc2, offset 0x3080
0x30a0: 0x0002, 0x30a1: 0x0002, 0x30a2: 0x0002, 0x30a3: 0x0002,
0x30a4: 0x0001,
0x30b0: 0x0002, 0x30b1: 0x0002, 0x30b2: 0x0002, 0x30b3: 0x0002, 0x30b4: 0x0002, 0x30b5: 0x0002,
0x30b6: 0x0002,
0x30b0: 0x0002, 0x30b1: 0x0002,
// Block 0xc3, offset 0x30c0
0x30c0: 0x0002, 0x30c1: 0x0002, 0x30c2: 0x0002, 0x30c3: 0x0002, 0x30c4: 0x0002, 0x30c5: 0x0002,
0x30c6: 0x0002, 0x30c7: 0x0002, 0x30c8: 0x0002, 0x30c9: 0x0002, 0x30ca: 0x0002, 0x30cb: 0x0002,
0x30cc: 0x0002, 0x30cd: 0x0002, 0x30ce: 0x0002, 0x30cf: 0x0002, 0x30d0: 0x0002, 0x30d1: 0x0002,
0x30d2: 0x0002, 0x30d3: 0x0002, 0x30d4: 0x0002, 0x30d5: 0x0002,
0x30ff: 0x0002,
0x30d2: 0x0002, 0x30d3: 0x0002, 0x30d4: 0x0002, 0x30d5: 0x0002, 0x30d6: 0x0002, 0x30d7: 0x0002,
0x30d8: 0x0002, 0x30d9: 0x0002, 0x30da: 0x0002, 0x30db: 0x0002, 0x30dc: 0x0002, 0x30dd: 0x0002,
0x30de: 0x0002, 0x30df: 0x0002, 0x30e0: 0x0002, 0x30e1: 0x0002, 0x30e2: 0x0002, 0x30e3: 0x0002,
0x30e4: 0x0002, 0x30e5: 0x0002, 0x30e6: 0x0002, 0x30e7: 0x0002, 0x30e8: 0x0002, 0x30e9: 0x0002,
0x30ea: 0x0002, 0x30eb: 0x0002, 0x30ec: 0x0002, 0x30ed: 0x0002, 0x30ee: 0x0002, 0x30ef: 0x0002,
0x30f0: 0x0002, 0x30f1: 0x0002, 0x30f2: 0x0002, 0x30f3: 0x0002, 0x30f4: 0x0002, 0x30f5: 0x0002,
0x30f6: 0x0002, 0x30f7: 0x0002,
// Block 0xc4, offset 0x3100
0x3100: 0x0002, 0x3101: 0x0002, 0x3102: 0x0002, 0x3103: 0x0002, 0x3104: 0x0002, 0x3105: 0x0002,
0x3106: 0x0002, 0x3107: 0x0002, 0x3108: 0x0002, 0x3109: 0x0002, 0x310a: 0x0002, 0x310b: 0x0002,
0x310c: 0x0002, 0x310d: 0x0002, 0x310e: 0x0002, 0x310f: 0x0002, 0x3110: 0x0002, 0x3111: 0x0002,
0x3112: 0x0002, 0x3113: 0x0002, 0x3114: 0x0002, 0x3115: 0x0002, 0x3116: 0x0002, 0x3117: 0x0002,
0x3118: 0x0002, 0x3119: 0x0002, 0x311a: 0x0002, 0x311b: 0x0002, 0x311c: 0x0002, 0x311d: 0x0002,
0x311e: 0x0002,
0x3112: 0x0002, 0x3113: 0x0002, 0x3114: 0x0002, 0x3115: 0x0002,
0x313f: 0x0002,
// Block 0xc5, offset 0x3140
0x3140: 0x0002, 0x3141: 0x0002, 0x3142: 0x0002, 0x3143: 0x0002, 0x3144: 0x0002, 0x3145: 0x0002,
0x3146: 0x0002, 0x3147: 0x0002, 0x3148: 0x0002, 0x3149: 0x0002, 0x314a: 0x0002, 0x314b: 0x0002,
0x314c: 0x0002, 0x314d: 0x0002, 0x314e: 0x0002, 0x314f: 0x0002, 0x3150: 0x0002, 0x3151: 0x0002,
0x3152: 0x0002, 0x3153: 0x0002, 0x3154: 0x0002, 0x3155: 0x0002, 0x3156: 0x0002, 0x3157: 0x0002,
0x3158: 0x0002, 0x3159: 0x0002, 0x315a: 0x0002, 0x315b: 0x0002, 0x315c: 0x0002, 0x315d: 0x0002,
0x315e: 0x0002, 0x315f: 0x0002, 0x3160: 0x0002, 0x3161: 0x0002, 0x3162: 0x0002, 0x3163: 0x0002,
0x3164: 0x0002, 0x3165: 0x0002, 0x3166: 0x0002, 0x3167: 0x0002, 0x3168: 0x0002, 0x3169: 0x0002,
0x316a: 0x0002, 0x316b: 0x0002, 0x316c: 0x0002, 0x316d: 0x0002, 0x316e: 0x0002, 0x316f: 0x0002,
0x3170: 0x0002, 0x3171: 0x0002, 0x3172: 0x0002,
0x3146: 0x0002, 0x3147: 0x0002, 0x3148: 0x0002,
// Block 0xc6, offset 0x3180
0x31b0: 0x0002, 0x31b1: 0x0002, 0x31b2: 0x0002, 0x31b3: 0x0002, 0x31b5: 0x0002,
0x31b6: 0x0002, 0x31b7: 0x0002, 0x31b8: 0x0002, 0x31b9: 0x0002, 0x31ba: 0x0002, 0x31bb: 0x0002,
@ -1445,7 +1443,7 @@ var stringWidthValues = [15744]uint8{
0x3b40: 0x0002, 0x3b41: 0x0002, 0x3b42: 0x0002, 0x3b43: 0x0002, 0x3b44: 0x0002, 0x3b45: 0x0002,
0x3b4c: 0x0002, 0x3b50: 0x0002, 0x3b51: 0x0002,
0x3b52: 0x0002, 0x3b55: 0x0002, 0x3b56: 0x0002, 0x3b57: 0x0002,
0x3b58: 0x0002, 0x3b5c: 0x0002, 0x3b5d: 0x0002,
0x3b5c: 0x0002, 0x3b5d: 0x0002,
0x3b5e: 0x0002, 0x3b5f: 0x0002,
0x3b6b: 0x0002, 0x3b6c: 0x0002,
0x3b74: 0x0002, 0x3b75: 0x0002,
@ -1484,8 +1482,8 @@ var stringWidthValues = [15744]uint8{
0x3c7c: 0x0002,
// Block 0xf2, offset 0x3c80
0x3c80: 0x0002, 0x3c81: 0x0002, 0x3c82: 0x0002, 0x3c83: 0x0002, 0x3c84: 0x0002, 0x3c85: 0x0002,
0x3c86: 0x0002, 0x3c87: 0x0002, 0x3c88: 0x0002, 0x3c89: 0x0002, 0x3c8a: 0x0002,
0x3c8e: 0x0002, 0x3c8f: 0x0002, 0x3c90: 0x0002, 0x3c91: 0x0002,
0x3c86: 0x0002, 0x3c87: 0x0002, 0x3c88: 0x0002, 0x3c89: 0x0002,
0x3c8f: 0x0002, 0x3c90: 0x0002, 0x3c91: 0x0002,
0x3c92: 0x0002, 0x3c93: 0x0002, 0x3c94: 0x0002, 0x3c95: 0x0002, 0x3c96: 0x0002, 0x3c97: 0x0002,
0x3c98: 0x0002, 0x3c99: 0x0002, 0x3c9a: 0x0002, 0x3c9b: 0x0002, 0x3c9c: 0x0002, 0x3c9d: 0x0002,
0x3c9e: 0x0002, 0x3c9f: 0x0002, 0x3ca0: 0x0002, 0x3ca1: 0x0002, 0x3ca2: 0x0002, 0x3ca3: 0x0002,
@ -1496,13 +1494,12 @@ var stringWidthValues = [15744]uint8{
0x3cbc: 0x0002, 0x3cbd: 0x0002, 0x3cbe: 0x0002, 0x3cbf: 0x0002,
// Block 0xf3, offset 0x3cc0
0x3cc0: 0x0002, 0x3cc1: 0x0002, 0x3cc2: 0x0002, 0x3cc3: 0x0002, 0x3cc4: 0x0002, 0x3cc5: 0x0002,
0x3cc6: 0x0002, 0x3cc8: 0x0002,
0x3ccd: 0x0002, 0x3cce: 0x0002, 0x3ccf: 0x0002, 0x3cd0: 0x0002, 0x3cd1: 0x0002,
0x3cc6: 0x0002,
0x3cce: 0x0002, 0x3ccf: 0x0002, 0x3cd0: 0x0002, 0x3cd1: 0x0002,
0x3cd2: 0x0002, 0x3cd3: 0x0002, 0x3cd4: 0x0002, 0x3cd5: 0x0002, 0x3cd6: 0x0002, 0x3cd7: 0x0002,
0x3cd8: 0x0002, 0x3cd9: 0x0002, 0x3cda: 0x0002, 0x3cdb: 0x0002, 0x3cdc: 0x0002,
0x3cdf: 0x0002, 0x3ce0: 0x0002, 0x3ce1: 0x0002, 0x3ce2: 0x0002, 0x3ce3: 0x0002,
0x3ce4: 0x0002, 0x3ce5: 0x0002, 0x3ce6: 0x0002, 0x3ce7: 0x0002, 0x3ce8: 0x0002, 0x3ce9: 0x0002,
0x3cea: 0x0002, 0x3cef: 0x0002,
0x3cf0: 0x0002, 0x3cf1: 0x0002, 0x3cf2: 0x0002, 0x3cf3: 0x0002, 0x3cf4: 0x0002, 0x3cf5: 0x0002,
0x3cf6: 0x0002, 0x3cf7: 0x0002, 0x3cf8: 0x0002,
// Block 0xf4, offset 0x3d00
@ -1634,10 +1631,10 @@ var stringWidthIndex = [1920]uint8{
0x440: 0x39, 0x441: 0x39, 0x442: 0x39, 0x443: 0x39, 0x444: 0x39, 0x445: 0x39, 0x446: 0x39, 0x447: 0x39,
0x448: 0x39, 0x449: 0x39, 0x44a: 0x39, 0x44b: 0x39, 0x44c: 0x39, 0x44d: 0x39, 0x44e: 0x39, 0x44f: 0x39,
0x450: 0x39, 0x451: 0x39, 0x452: 0x39, 0x453: 0x39, 0x454: 0x39, 0x455: 0x39, 0x456: 0x39, 0x457: 0x39,
0x458: 0x39, 0x459: 0x39, 0x45a: 0x39, 0x45b: 0x39, 0x45c: 0x39, 0x45d: 0x39, 0x45e: 0x39, 0x45f: 0x39,
0x458: 0x39, 0x459: 0x39, 0x45a: 0x39, 0x45b: 0x39, 0x45c: 0x39, 0x45d: 0x39, 0x45e: 0x39, 0x45f: 0xc1,
0x460: 0x39, 0x461: 0x39, 0x462: 0x39, 0x463: 0x39, 0x464: 0x39, 0x465: 0x39, 0x466: 0x39, 0x467: 0x39,
0x468: 0x39, 0x469: 0x39, 0x46a: 0x39, 0x46b: 0x39, 0x46c: 0x39, 0x46d: 0x39, 0x46e: 0x39, 0x46f: 0x39,
0x470: 0x39, 0x471: 0x39, 0x472: 0x39, 0x473: 0xc1, 0x474: 0xc2, 0x476: 0x39, 0x477: 0xc3,
0x470: 0x39, 0x471: 0x39, 0x472: 0x39, 0x473: 0xc2, 0x474: 0xc3,
// Block 0x12, offset 0x480
0x4bf: 0xc4,
// Block 0x13, offset 0x4c0

View File

@ -1,149 +0,0 @@
package displaywidth
import (
"strings"
"github.com/clipperhouse/uax29/v2/graphemes"
)
// TruncateString truncates a string to the given maxWidth, and appends the
// given tail if the string is truncated.
//
// It ensures the visible width, including the width of the tail, is less than or
// equal to maxWidth.
//
// When [Options.ControlSequences] is true, 7-bit ANSI escape sequences that
// appear after the truncation point are preserved in the output. This ensures
// that escape sequences such as SGR resets are not lost, preventing color
// bleed in terminal output.
//
// [Options.ControlSequences8Bit] is ignored by truncation. 8-bit C1 byte values
// (0x80-0x9F) overlap with UTF-8 multi-byte encoding, so manipulating them
// during truncation can shift byte boundaries and form unintended visible
// characters. Use [Options.String] or [Options.Bytes] for 8-bit-aware width
// measurement.
func (options Options) TruncateString(s string, maxWidth int, tail string) string {
// We deliberately ignore ControlSequences8Bit for truncation, see above.
options.ControlSequences8Bit = false
maxWidthWithoutTail := maxWidth - options.String(tail)
var pos, total int
g := graphemes.FromString(s)
g.AnsiEscapeSequences = options.ControlSequences
for g.Next() {
gw := graphemeWidth(g.Value(), options)
if total+gw <= maxWidthWithoutTail {
pos = g.End()
}
total += gw
if total > maxWidth {
if options.ControlSequences {
// Build result with trailing 7-bit ANSI escape sequences preserved
var b strings.Builder
b.Grow(len(s) + len(tail)) // at most original + tail
b.WriteString(s[:pos])
b.WriteString(tail)
rem := graphemes.FromString(s[pos:])
rem.AnsiEscapeSequences = options.ControlSequences
for rem.Next() {
v := rem.Value()
// Only preserve 7-bit escapes (ESC = 0x1B) that measure
// as zero-width on their own; some sequences (e.g. SOS)
// are only valid in their original context.
if len(v) > 0 && v[0] == 0x1B && options.String(v) == 0 {
b.WriteString(v)
}
}
return b.String()
}
return s[:pos] + tail
}
}
// No truncation
return s
}
// TruncateString truncates a string to the given maxWidth, and appends the
// given tail if the string is truncated.
//
// It ensures the total width, including the width of the tail, is less than or
// equal to maxWidth.
func TruncateString(s string, maxWidth int, tail string) string {
return DefaultOptions.TruncateString(s, maxWidth, tail)
}
// TruncateBytes truncates a []byte to the given maxWidth, and appends the
// given tail if the []byte is truncated.
//
// It ensures the visible width, including the width of the tail, is less than or
// equal to maxWidth.
//
// When [Options.ControlSequences] is true, 7-bit ANSI escape sequences that
// appear after the truncation point are preserved in the output. This ensures
// that escape sequences such as SGR resets are not lost, preventing color
// bleed in terminal output.
//
// [Options.ControlSequences8Bit] is ignored by truncation. 8-bit C1 byte values
// (0x80-0x9F) overlap with UTF-8 multi-byte encoding, so manipulating them
// during truncation can shift byte boundaries and form unintended visible
// characters. Use [Options.String] or [Options.Bytes] for 8-bit-aware width
// measurement.
func (options Options) TruncateBytes(s []byte, maxWidth int, tail []byte) []byte {
// We deliberately ignore ControlSequences8Bit for truncation, see above.
options.ControlSequences8Bit = false
maxWidthWithoutTail := maxWidth - options.Bytes(tail)
var pos, total int
g := graphemes.FromBytes(s)
g.AnsiEscapeSequences = options.ControlSequences
for g.Next() {
gw := graphemeWidth(g.Value(), options)
if total+gw <= maxWidthWithoutTail {
pos = g.End()
}
total += gw
if total > maxWidth {
if options.ControlSequences {
// Build result with trailing 7-bit ANSI escape sequences preserved
result := make([]byte, 0, len(s)+len(tail)) // at most original + tail
result = append(result, s[:pos]...)
result = append(result, tail...)
rem := graphemes.FromBytes(s[pos:])
rem.AnsiEscapeSequences = options.ControlSequences
for rem.Next() {
v := rem.Value()
// Only preserve 7-bit escapes (ESC = 0x1B) that measure
// as zero-width on their own; some sequences (e.g. SOS)
// are only valid in their original context.
if len(v) > 0 && v[0] == 0x1B && options.Bytes(v) == 0 {
result = append(result, v...)
}
}
return result
}
result := make([]byte, 0, pos+len(tail))
result = append(result, s[:pos]...)
result = append(result, tail...)
return result
}
}
// No truncation
return s
}
// TruncateBytes truncates a []byte to the given maxWidth, and appends the
// given tail if the []byte is truncated.
//
// It ensures the total width, including the width of the tail, is less than or
// equal to maxWidth.
func TruncateBytes(s []byte, maxWidth int, tail []byte) []byte {
return DefaultOptions.TruncateBytes(s, maxWidth, tail)
}

View File

@ -3,9 +3,22 @@ package displaywidth
import (
"unicode/utf8"
"github.com/clipperhouse/stringish"
"github.com/clipperhouse/uax29/v2/graphemes"
)
// Options allows you to specify the treatment of ambiguous East Asian
// characters. When EastAsianWidth is false (default), ambiguous East Asian
// characters are treated as width 1. When EastAsianWidth is true, ambiguous
// East Asian characters are treated as width 2.
type Options struct {
EastAsianWidth bool
}
// DefaultOptions is the default options for the display width
// calculation, which is EastAsianWidth: false.
var DefaultOptions = Options{EastAsianWidth: false}
// String calculates the display width of a string,
// by iterating over grapheme clusters in the string
// and summing their widths.
@ -16,44 +29,19 @@ func String(s string) int {
// String calculates the display width of a string, for the given options, by
// iterating over grapheme clusters in the string and summing their widths.
func (options Options) String(s string) int {
width := 0
pos := 0
for pos < len(s) {
// Try ASCII optimization
asciiLen := printableASCIILength(s[pos:])
if asciiLen > 0 {
width += asciiLen
pos += asciiLen
continue
}
// Not ASCII, use grapheme parsing
g := graphemes.FromString(s[pos:])
g.AnsiEscapeSequences = options.ControlSequences
g.AnsiEscapeSequences8Bit = options.ControlSequences8Bit
start := pos
for g.Next() {
v := g.Value()
width += graphemeWidth(v, options)
pos += len(v)
// Quick check: if remaining might have printable ASCII, break to outer loop
if pos < len(s) && s[pos] >= 0x20 && s[pos] <= 0x7E {
break
}
}
// Defensive, should not happen: if no progress was made,
// skip a byte to prevent infinite loop. Only applies if
// the grapheme parser misbehaves.
if pos == start {
pos++
}
// Optimization: no need to parse grapheme
switch len(s) {
case 0:
return 0
case 1:
return asciiWidth(s[0])
}
width := 0
g := graphemes.FromString(s)
for g.Next() {
width += graphemeWidth(g.Value(), options)
}
return width
}
@ -67,44 +55,19 @@ func Bytes(s []byte) int {
// Bytes calculates the display width of a []byte, for the given options, by
// iterating over grapheme clusters in the slice and summing their widths.
func (options Options) Bytes(s []byte) int {
width := 0
pos := 0
for pos < len(s) {
// Try ASCII optimization
asciiLen := printableASCIILength(s[pos:])
if asciiLen > 0 {
width += asciiLen
pos += asciiLen
continue
}
// Not ASCII, use grapheme parsing
g := graphemes.FromBytes(s[pos:])
g.AnsiEscapeSequences = options.ControlSequences
g.AnsiEscapeSequences8Bit = options.ControlSequences8Bit
start := pos
for g.Next() {
v := g.Value()
width += graphemeWidth(v, options)
pos += len(v)
// Quick check: if remaining might have printable ASCII, break to outer loop
if pos < len(s) && s[pos] >= 0x20 && s[pos] <= 0x7E {
break
}
}
// Defensive, should not happen: if no progress was made,
// skip a byte to prevent infinite loop. Only applies if
// the grapheme parser misbehaves.
if pos == start {
pos++
}
// Optimization: no need to parse grapheme
switch len(s) {
case 0:
return 0
case 1:
return asciiWidth(s[0])
}
width := 0
g := graphemes.FromBytes(s)
for g.Next() {
width += graphemeWidth(g.Value(), options)
}
return width
}
@ -144,30 +107,86 @@ func (options Options) Rune(r rune) int {
const _Default property = 0
// TruncateString truncates a string to the given maxWidth, and appends the
// given tail if the string is truncated.
//
// It ensures the total width, including the width of the tail, is less than or
// equal to maxWidth.
func (options Options) TruncateString(s string, maxWidth int, tail string) string {
maxWidthWithoutTail := maxWidth - options.String(tail)
var pos, total int
g := graphemes.FromString(s)
for g.Next() {
gw := graphemeWidth(g.Value(), options)
if total+gw <= maxWidthWithoutTail {
pos = g.End()
}
total += gw
if total > maxWidth {
return s[:pos] + tail
}
}
// No truncation
return s
}
// TruncateString truncates a string to the given maxWidth, and appends the
// given tail if the string is truncated.
//
// It ensures the total width, including the width of the tail, is less than or
// equal to maxWidth.
func TruncateString(s string, maxWidth int, tail string) string {
return DefaultOptions.TruncateString(s, maxWidth, tail)
}
// TruncateBytes truncates a []byte to the given maxWidth, and appends the
// given tail if the []byte is truncated.
//
// It ensures the total width, including the width of the tail, is less than or
// equal to maxWidth.
func (options Options) TruncateBytes(s []byte, maxWidth int, tail []byte) []byte {
maxWidthWithoutTail := maxWidth - options.Bytes(tail)
var pos, total int
g := graphemes.FromBytes(s)
for g.Next() {
gw := graphemeWidth(g.Value(), options)
if total+gw <= maxWidthWithoutTail {
pos = g.End()
}
total += gw
if total > maxWidth {
result := make([]byte, 0, pos+len(tail))
result = append(result, s[:pos]...)
result = append(result, tail...)
return result
}
}
// No truncation
return s
}
// TruncateBytes truncates a []byte to the given maxWidth, and appends the
// given tail if the []byte is truncated.
//
// It ensures the total width, including the width of the tail, is less than or
// equal to maxWidth.
func TruncateBytes(s []byte, maxWidth int, tail []byte) []byte {
return DefaultOptions.TruncateBytes(s, maxWidth, tail)
}
// graphemeWidth returns the display width of a grapheme cluster.
// The passed string must be a single grapheme cluster.
func graphemeWidth[T ~string | []byte](s T, options Options) int {
if len(s) == 0 {
func graphemeWidth[T stringish.Interface](s T, options Options) int {
// Optimization: no need to look up properties
switch len(s) {
case 0:
return 0
}
// C1 controls (0x80-0x9F) are zero-width when 8-bit control sequences
// are enabled. This must be checked before the single-byte optimization
// below, which would otherwise return width 1 for these bytes.
if options.ControlSequences8Bit && s[0] >= 0x80 && s[0] <= 0x9F {
return 0
}
// Optimization: single-byte graphemes need no property lookup
if len(s) == 1 {
case 1:
return asciiWidth(s[0])
}
// Multi-byte grapheme clusters led by a C0 control (0x00-0x1F)
if s[0] <= 0x1F {
return 0
}
p, sz := lookup(s)
prop := property(p)
@ -200,31 +219,9 @@ func asciiWidth(b byte) int {
return 1
}
// printableASCIILength returns the length of consecutive printable ASCII bytes
// starting at the beginning of s.
func printableASCIILength[T string | []byte](s T) int {
i := 0
for ; i < len(s); i++ {
b := s[i]
// Printable ASCII is 0x20-0x7E (space through tilde)
if b < 0x20 || b > 0x7E {
break
}
}
// If the next byte is non-ASCII (>= 0x80), back off by 1. The grapheme
// parser may group the last ASCII byte with subsequent non-ASCII bytes,
// such as combining marks.
if i > 0 && i < len(s) && s[i] >= 0x80 {
i--
}
return i
}
// isVS16 checks if the slice matches VS16 (U+FE0F) UTF-8 encoding
// (EF B8 8F). It assumes len(s) >= 3.
func isVS16[T ~string | []byte](s T) bool {
func isVS16[T stringish.Interface](s T) bool {
return s[0] == 0xEF && s[1] == 0xB8 && s[2] == 0x8F
}

2
vendor/github.com/clipperhouse/stringish/.gitignore generated vendored Normal file
View File

@ -0,0 +1,2 @@
.DS_Store
*.test

21
vendor/github.com/clipperhouse/stringish/LICENSE generated vendored Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 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.

64
vendor/github.com/clipperhouse/stringish/README.md generated vendored Normal file
View File

@ -0,0 +1,64 @@
# stringish
A small Go module that provides a generic type constraint for “string-like”
data, and a utf8 package that works with both strings and byte slices
without conversions.
```go
type Interface interface {
~[]byte | ~string
}
```
[![Go Reference](https://pkg.go.dev/badge/github.com/clipperhouse/stringish/utf8.svg)](https://pkg.go.dev/github.com/clipperhouse/stringish/utf8)
[![Test Status](https://github.com/clipperhouse/stringish/actions/workflows/gotest.yml/badge.svg)](https://github.com/clipperhouse/stringish/actions/workflows/gotest.yml)
## Install
```
go get github.com/clipperhouse/stringish
```
## Examples
```go
import (
"github.com/clipperhouse/stringish"
"github.com/clipperhouse/stringish/utf8"
)
s := "Hello, 世界"
r, size := utf8.DecodeRune(s) // not DecodeRuneInString 🎉
b := []byte("Hello, 世界")
r, size = utf8.DecodeRune(b) // same API!
func MyFoo[T stringish.Interface](s T) T {
// pass a string or a []byte
// iterate, slice, transform, whatever
}
```
## Motivation
Sometimes we want APIs to accept `string` or `[]byte` without having to convert
between those types. That conversion usually allocates!
By implementing with `stringish.Interface`, we can have a single API, and
single implementation for both types: one `Foo` instead of `Foo` and
`FooString`.
We have converted the
[`unicode/utf8` package](https://github.com/clipperhouse/stringish/blob/main/utf8/utf8.go)
as an example -- note the absence of`*InString` funcs. We might look at `x/text`
next.
## Used by
- clipperhouse/uax29: [stringish trie](https://github.com/clipperhouse/uax29/blob/master/graphemes/trie.go#L27), [stringish iterator](https://github.com/clipperhouse/uax29/blob/master/internal/iterators/iterator.go#L9), [stringish SplitFunc](https://github.com/clipperhouse/uax29/blob/master/graphemes/splitfunc.go#L21)
- [clipperhouse/displaywidth](https://github.com/clipperhouse/displaywidth)
## Prior discussion
- [Consideration of similar by the Go team](https://github.com/golang/go/issues/48643)

View File

@ -0,0 +1,5 @@
package stringish
type Interface interface {
~[]byte | ~string
}

View File

@ -1,4 +1,4 @@
An implementation of grapheme cluster boundaries from [Unicode text segmentation](https://unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries) (UAX 29), for Unicode 17.
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.
[![Documentation](https://pkg.go.dev/badge/github.com/clipperhouse/uax29/v2/graphemes.svg)](https://pkg.go.dev/github.com/clipperhouse/uax29/v2/graphemes)
![Tests](https://github.com/clipperhouse/uax29/actions/workflows/gotest.yml/badge.svg)
@ -7,17 +7,18 @@ An implementation of grapheme cluster boundaries from [Unicode text segmentation
## Quick start
```
go get github.com/clipperhouse/uax29/v2/graphemes
go get "github.com/clipperhouse/uax29/v2/graphemes"
```
```go
import "github.com/clipperhouse/uax29/v2/graphemes"
text := "Hello, 世界. Nice dog! 👍🐶"
g := graphemes.FromString(text)
for g.Next() { // Next() returns true until end of data
fmt.Println(g.Value()) // Do something with the current grapheme
tokens := graphemes.FromString(text)
for tokens.Next() { // Next() returns true until end of data
fmt.Println(tokens.Value()) // Do something with the current grapheme
}
```
@ -25,7 +26,7 @@ _A grapheme is a “single visible character”, which might be a simple as a si
## Conformance
We use the Unicode [test suite](https://unicode.org/reports/tr41/tr41-36.html#Tests29).
We use the Unicode [test suite](https://unicode.org/reports/tr41/tr41-26.html#Tests29).
![Tests](https://github.com/clipperhouse/uax29/actions/workflows/gotest.yml/badge.svg)
![Fuzz](https://github.com/clipperhouse/uax29/actions/workflows/gofuzz.yml/badge.svg)
@ -36,10 +37,11 @@ We use the Unicode [test suite](https://unicode.org/reports/tr41/tr41-36.html#Te
```go
text := "Hello, 世界. Nice dog! 👍🐶"
g := graphemes.FromString(text)
for g.Next() { // Next() returns true until end of data
fmt.Println(g.Value()) // Do something with the current grapheme
tokens := graphemes.FromString(text)
for tokens.Next() { // Next() returns true until end of data
fmt.Println(tokens.Value()) // Do something with the current grapheme
}
```
@ -48,15 +50,15 @@ for g.Next() { // Next() returns true until end of data
`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
g := graphemes.FromReader(r)
r := getYourReader() // from a file or network maybe
tokens := graphemes.FromReader(r)
for g.Scan() { // Scan() returns true until error or EOF
fmt.Println(g.Text()) // Do something with the current grapheme
for tokens.Scan() { // Scan() returns true until error or EOF
fmt.Println(tokens.Text()) // Do something with the current grapheme
}
if g.Err() != nil { // Check the error
log.Fatal(g.Err())
if tokens.Err() != nil { // Check the error
log.Fatal(tokens.Err())
}
```
@ -65,52 +67,24 @@ if g.Err() != nil { // Check the error
```go
b := []byte("Hello, 世界. Nice dog! 👍🐶")
g := graphemes.FromBytes(b)
tokens := graphemes.FromBytes(b)
for g.Next() { // Next() returns true until end of data
fmt.Println(g.Value()) // Do something with the current grapheme
for tokens.Next() { // Next() returns true until end of data
fmt.Println(tokens.Value()) // Do something with the current grapheme
}
```
### ANSI escape sequences
By the UAX 29 specification, ANSI escape sequences are not grapheme clusters. To treat 7-bit ANSI escape sequences as a single cluster, set `AnsiEscapeSequences` to true.
```go
text := "Hello, \x1b[31mworld\x1b[0m!"
g := graphemes.FromString(text)
g.AnsiEscapeSequences = true
for g.Next() {
fmt.Println(g.Value())
}
```
To also parse 8-bit C1 controls (non-UTF-8 bytes), set `AnsiEscapeSequences8Bit` to true.
```go
g.AnsiEscapeSequences = true // 7-bit forms (ESC ...)
g.AnsiEscapeSequences8Bit = true // 8-bit C1 forms (0x80-0x9F), not valid UTF-8
```
For ESC-initiated (7-bit) control strings, only 7-bit terminators are recognized.
For C1-initiated (8-bit) control strings, only C1 ST (`0x9C`) is recognized as ST.
We implement [ECMA-48](https://ecma-international.org/publications-and-standards/standards/ecma-48/) control codes in both 7-bit and 8-bit representations. 8-bit control codes are not UTF-8 encoded and are not valid UTF-8, caveat emptor.
### Benchmarks
On a Mac M2 laptop, we see around 200MB/s, or around 100 million graphemes per second, and no allocations.
```
goos: darwin
goarch: arm64
pkg: github.com/clipperhouse/uax29/graphemes/comparative
cpu: Apple M2
BenchmarkGraphemesMixed/clipperhouse/uax29-8 142635 ns/op 245.12 MB/s 0 B/op 0 allocs/op
BenchmarkGraphemesMixed/rivo/uniseg-8 2018284 ns/op 17.32 MB/s 0 B/op 0 allocs/op
BenchmarkGraphemesASCII/clipperhouse/uax29-8 8846 ns/op 508.73 MB/s 0 B/op 0 allocs/op
BenchmarkGraphemesASCII/rivo/uniseg-8 366760 ns/op 12.27 MB/s 0 B/op 0 allocs/op
BenchmarkGraphemes/clipperhouse/uax29-8 173805 ns/op 201.16 MB/s 0 B/op 0 allocs/op
BenchmarkGraphemes/rivo/uniseg-8 2045128 ns/op 17.10 MB/s 0 B/op 0 allocs/op
```
### Invalid inputs

View File

@ -1,138 +0,0 @@
package graphemes
// ansiEscapeLength returns the byte length of a valid 7-bit ANSI escape
// sequence at the start of data, or 0 if none.
//
// Recognized forms (ECMA-48 / ISO 6429):
// - CSI: ESC [ then parameter bytes (0x30-0x3F), intermediate (0x20-0x2F), final (0x40-0x7E)
// - OSC: ESC ] then payload until BEL (0x07), 7-bit ST (ESC \), CAN (0x18), or SUB (0x1A)
// - DCS, SOS, PM, APC: ESC P/X/^/_ then payload until 7-bit ST (ESC \), CAN, or SUB
// - Two-byte: ESC + Fe/Fs (0x40-0x7E excluding above), or Fp (0x30-0x3F), or nF (0x20-0x2F then final)
func ansiEscapeLength[T ~string | ~[]byte](data T) int {
n := len(data)
if n < 2 || data[0] != esc {
return 0
}
b1 := data[1]
switch b1 {
case '[': // CSI
body := csiBodyLength(data[2:])
if body == 0 {
return 0
}
return 2 + body
case ']': // OSC - allows BEL or 7-bit ST terminator
body := oscLength(data[2:])
if body < 0 {
return 0
}
return 2 + body
case 'P', 'X', '^', '_': // DCS, SOS, PM, APC
body := stSequenceLength(data[2:])
if body < 0 {
return 0
}
return 2 + body
}
if b1 >= 0x40 && b1 <= 0x7E {
// Fe/Fs two-byte; [ ] P X ^ _ handled above
return 2
}
if b1 >= 0x30 && b1 <= 0x3F {
// Fp (private) two-byte
return 2
}
if b1 >= 0x20 && b1 <= 0x2F {
// nF: intermediates then one final (0x30-0x7E)
i := 2
for i < n && data[i] >= 0x20 && data[i] <= 0x2F {
i++
}
if i < n && data[i] >= 0x30 && data[i] <= 0x7E {
return i + 1
}
return 0
}
return 0
}
// csiBodyLength returns the length of the CSI body (param/intermediate/final bytes).
// data is the slice after "ESC [".
// Per ECMA-48, the CSI body has the form:
//
// parameters (0x300x3F)*, intermediates (0x200x2F)*, final (0x400x7E)
//
// Once an intermediate byte is seen, subsequent parameter bytes are invalid.
func csiBodyLength[T ~string | ~[]byte](data T) int {
seenIntermediate := false
for i := 0; i < len(data); i++ {
b := data[i]
if b >= 0x30 && b <= 0x3F {
if seenIntermediate {
return 0
}
continue
}
if b >= 0x20 && b <= 0x2F {
seenIntermediate = true
continue
}
if b >= 0x40 && b <= 0x7E {
return i + 1
}
return 0
}
return 0
}
// oscLength returns the length of the OSC body.
// data is the slice after "ESC ]".
//
// Returns:
// - n >= 0: consumed body length (includes BEL/ST terminator when present)
// - -1: not terminated in the provided data
//
// OSC accepts BEL (0x07) or 7-bit ST (ESC \) as terminators by widespread convention.
// Per ECMA-48, CAN (0x18) and SUB (0x1A) cancel the control string; in that
// case they are not part of the OSC sequence length.
func oscLength[T ~string | ~[]byte](data T) int {
for i := 0; i < len(data); i++ {
b := data[i]
if b == bel {
return i + 1
}
if b == can || b == sub {
return i
}
if b == esc && i+1 < len(data) && data[i+1] == '\\' {
return i + 2
}
}
return -1
}
// stSequenceLength returns the length of a control-string body.
// data is the slice after "ESC x".
//
// Returns:
// - n >= 0: consumed body length (includes ST terminator when present)
// - -1: not terminated in the provided data
//
// Used for DCS, SOS, PM, and APC, which per ECMA-48 terminate with ST.
// ST here is the 7-bit form (ESC \).
// CAN (0x18) and SUB (0x1A) cancel the control string; in that case they are
// not part of the sequence length.
func stSequenceLength[T ~string | ~[]byte](data T) int {
for i := 0; i < len(data); i++ {
if data[i] == can || data[i] == sub {
return i
}
if data[i] == esc && i+1 < len(data) && data[i+1] == '\\' {
return i + 2
}
}
return -1
}

View File

@ -1,79 +0,0 @@
package graphemes
// ansiEscapeLength8Bit returns the byte length of a valid 8-bit C1 ANSI
// sequence at the start of data, or 0 if none.
//
// Recognized forms (ECMA-48 / ISO 6429):
// - C1 CSI (0x9B) body as parameter/intermediate/final bytes
// - C1 OSC (0x9D) body terminated by BEL, C1 ST, CAN, or SUB
// - C1 DCS/SOS/PM/APC (0x90/0x98/0x9E/0x9F) body terminated by C1 ST, CAN, or SUB
// - Standalone C1 controls (0x80..0x9F not listed above): single byte
func ansiEscapeLength8Bit[T ~string | ~[]byte](data T) int {
if len(data) == 0 {
return 0
}
switch data[0] {
case 0x9B: // C1 CSI
body := csiBodyLength(data[1:])
if body == 0 {
return 0
}
return 1 + body
case 0x9D: // C1 OSC
body := oscLengthC1(data[1:])
if body < 0 {
return 0
}
return 1 + body
case 0x90, 0x98, 0x9E, 0x9F: // C1 DCS, SOS, PM, APC
body := stSequenceLengthC1(data[1:])
if body < 0 {
return 0
}
return 1 + body
default:
if data[0] >= 0x80 && data[0] <= 0x9F {
return 1
}
}
return 0
}
// oscLengthC1 returns the length of a C1 OSC body.
// data is the slice after the C1 OSC initiator (0x9D).
//
// Returns:
// - n >= 0: consumed body length (includes BEL/ST terminator when present)
// - -1: not terminated in the provided data
//
// Terminators: BEL (0x07) or C1 ST (0x9C).
// CAN (0x18) and SUB (0x1A) cancel the control string.
func oscLengthC1[T ~string | ~[]byte](data T) int {
for i := 0; i < len(data); i++ {
b := data[i]
if b == bel || b == st {
return i + 1
}
if b == can || b == sub {
return i
}
}
return -1
}
// stSequenceLengthC1 parses DCS/SOS/PM/APC bodies that terminate with C1 ST
// (0x9C), or are canceled by CAN/SUB.
func stSequenceLengthC1[T ~string | ~[]byte](data T) int {
for i := 0; i < len(data); i++ {
b := data[i]
if b == can || b == sub {
return i
}
if b == st {
return i + 1
}
}
return -1
}

View File

@ -1,44 +1,12 @@
package graphemes
import "unicode/utf8"
import (
"github.com/clipperhouse/stringish"
"github.com/clipperhouse/uax29/v2/internal/iterators"
)
// 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]{
split: splitFuncString,
data: 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]{
split: splitFuncBytes,
data: b,
}
}
// Iterator is a generic iterator for grapheme clusters in strings or byte slices,
// with an ASCII hot path optimization.
type Iterator[T ~string | ~[]byte] struct {
split func(T, bool) (int, T, error)
data T
pos int
start int
// AnsiEscapeSequences treats 7-bit ANSI escape sequences (ECMA-48) as
// single grapheme clusters when true. The default is false.
//
// 8-bit controls are not enabled by this option. See [AnsiEscapeSequences8Bit].
AnsiEscapeSequences bool
// AnsiEscapeSequences8Bit treats 8-bit C1 ANSI escape sequences (ECMA-48) as single
// grapheme clusters when true. The default is false.
//
// 8-bit control bytes are not UTF-8 encoded, i.e. not valid UTF-8. If you
// choose this option, you are choosing to interpret non-UTF-8 data, caveat
// emptor.
AnsiEscapeSequences8Bit bool
type Iterator[T stringish.Interface] struct {
*iterators.Iterator[T]
}
var (
@ -46,99 +14,18 @@ var (
splitFuncBytes = splitFunc[[]byte]
)
const (
esc = 0x1B
cr = 0x0D
bel = 0x07
can = 0x18
sub = 0x1A
st = 0x9C
)
// Next advances the iterator to the next grapheme cluster.
// Returns false when there are no more grapheme clusters.
func (iter *Iterator[T]) Next() bool {
if iter.pos >= len(iter.data) {
return false
// 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),
}
iter.start = iter.pos
b := iter.data[iter.pos]
if iter.AnsiEscapeSequences && b == esc {
if a := ansiEscapeLength(iter.data[iter.pos:]); a > 0 {
iter.pos += a
return true
}
}
if iter.AnsiEscapeSequences8Bit && b >= 0x80 && b <= 0x9F {
if a := ansiEscapeLength8Bit(iter.data[iter.pos:]); a > 0 {
iter.pos += a
return true
}
}
// ASCII hot path: any ASCII is one grapheme when next byte is ASCII or end.
if b < utf8.RuneSelf && b != cr {
if iter.pos+1 >= len(iter.data) || iter.data[iter.pos+1] < utf8.RuneSelf {
iter.pos++
return true
}
}
// Fall back to UAX29 grapheme parsing
remaining := iter.data[iter.pos:]
advance, _, err := iter.split(remaining, true)
if err != nil {
panic(err)
}
if advance <= 0 {
panic("splitFunc returned a zero or negative advance")
}
iter.pos += advance
if iter.pos > len(iter.data) {
panic("splitFunc advanced beyond end of data")
}
return true
}
// Value returns the current grapheme cluster.
func (iter *Iterator[T]) Value() T {
return iter.data[iter.start:iter.pos]
}
// Start returns the byte position of the current grapheme in the original data.
func (iter *Iterator[T]) Start() int {
return iter.start
}
// End returns the byte position after the current grapheme in the original data.
func (iter *Iterator[T]) End() int {
return iter.pos
}
// Reset resets the iterator to the beginning of the data.
func (iter *Iterator[T]) Reset() {
iter.start = 0
iter.pos = 0
}
// SetText sets the data for the iterator to operate on, and resets all state.
func (iter *Iterator[T]) SetText(data T) {
iter.data = data
iter.start = 0
iter.pos = 0
}
// First returns the first grapheme cluster without advancing the iterator.
func (iter *Iterator[T]) First() T {
if len(iter.data) == 0 {
return iter.data
// 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),
}
// Use a copy to leverage Next()'s ASCII optimization
cp := *iter
cp.pos = 0
cp.start = 0
cp.Next()
return cp.Value()
}

View File

@ -2,6 +2,8 @@ package graphemes
import (
"bufio"
"github.com/clipperhouse/stringish"
)
// is determines if lookup intersects propert(ies)
@ -11,22 +13,12 @@ func (lookup property) is(properties property) bool {
const _Ignore = _Extend
// incbState tracks state for GB9c rule (Indic conjunct clusters)
// Pattern: Consonant (Extend|Linker)* Linker (Extend|Linker)* × Consonant
type incbState int
const (
incbNone incbState = iota // initial/reset
incbConsonant // seen Consonant, awaiting Linker
incbLinker // seen Consonant and Linker (conjunct ready)
)
// 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 ~string | ~[]byte](data T, atEOF bool) (advance int, token T, err error) {
func splitFunc[T stringish.Interface](data T, atEOF bool) (advance int, token T, err error) {
var empty T
if len(data) == 0 {
return 0, empty, nil
@ -38,9 +30,6 @@ func splitFunc[T ~string | ~[]byte](data T, atEOF bool) (advance int, token T, e
var lastLastExIgnore property = 0 // "last one before that"
var regionalIndicatorCount int
// GB9c state: tracking Indic conjunct clusters
var incb incbState
// 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
@ -87,23 +76,6 @@ func splitFunc[T ~string | ~[]byte](data T, atEOF bool) (advance int, token T, e
lastExIgnore = last
}
// Update GB9c state based on what we just advanced past
if last.is(_InCBConsonant | _InCBLinker | _InCBExtend) {
switch {
case last.is(_InCBConsonant):
if incb != incbLinker {
incb = incbConsonant
}
case last.is(_InCBLinker):
if incb >= incbConsonant {
incb = incbLinker
}
// case last.is(_InCBExtend): stay in current state
}
} else {
incb = incbNone
}
current, w = lookup(data[pos:])
if w == 0 {
if atEOF {
@ -169,14 +141,11 @@ func splitFunc[T ~string | ~[]byte](data T, atEOF bool) (advance int, token T, e
}
// https://unicode.org/reports/tr29/#GB9c
// Do not break within certain combinations with Indic_Conjunct_Break (InCB)=Linker.
if incb == incbLinker && current.is(_InCBConsonant) {
// After matching the pattern, reset state to start tracking a new pattern
// The current Consonant becomes the start of the new pattern
incb = incbConsonant
pos += w
continue
}
// 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) {

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,100 @@
package iterators
import "github.com/clipperhouse/stringish"
type SplitFunc[T stringish.Interface] func(T, bool) (int, T, error)
// Iterator is a generic iterator for words that are either []byte or string.
// Iterate while Next() is true, and access the word via Value().
type Iterator[T stringish.Interface] struct {
split SplitFunc[T]
data T
start int
pos int
}
// New creates a new Iterator for the given data and SplitFunc.
func New[T stringish.Interface](split SplitFunc[T], data T) *Iterator[T] {
return &Iterator[T]{
split: split,
data: data,
}
}
// SetText sets the text for the iterator to operate on, and resets all state.
func (iter *Iterator[T]) SetText(data T) {
iter.data = data
iter.start = 0
iter.pos = 0
}
// Split sets the SplitFunc for the Iterator.
func (iter *Iterator[T]) Split(split SplitFunc[T]) {
iter.split = split
}
// Next advances the iterator to the next token. It returns false when there
// are no remaining tokens or an error occurred.
func (iter *Iterator[T]) Next() bool {
if iter.pos == len(iter.data) {
return false
}
if iter.pos > len(iter.data) {
panic("SplitFunc advanced beyond the end of the data")
}
iter.start = iter.pos
advance, _, err := iter.split(iter.data[iter.pos:], true)
if err != nil {
panic(err)
}
if advance <= 0 {
panic("SplitFunc returned a zero or negative advance")
}
iter.pos += advance
if iter.pos > len(iter.data) {
panic("SplitFunc advanced beyond the end of the data")
}
return true
}
// Value returns the current token.
func (iter *Iterator[T]) Value() T {
return iter.data[iter.start:iter.pos]
}
// Start returns the byte position of the current token in the original data.
func (iter *Iterator[T]) Start() int {
return iter.start
}
// End returns the byte position after the current token in the original data.
func (iter *Iterator[T]) End() int {
return iter.pos
}
// Reset resets the iterator to the beginning of the data.
func (iter *Iterator[T]) Reset() {
iter.start = 0
iter.pos = 0
}
func (iter *Iterator[T]) First() T {
if len(iter.data) == 0 {
return iter.data
}
advance, _, err := iter.split(iter.data, true)
if err != nil {
panic(err)
}
if advance <= 0 {
panic("SplitFunc returned a zero or negative advance")
}
if advance > len(iter.data) {
panic("SplitFunc advanced beyond the end of the data")
}
return iter.data[:advance]
}

12
vendor/github.com/docker/cli/AUTHORS generated vendored
View File

@ -63,6 +63,7 @@ Andreas Köhler <andi5.py@gmx.net>
Andres G. Aragoneses <knocte@gmail.com>
Andres Leon Rangel <aleon1220@gmail.com>
Andrew France <andrew@avito.co.uk>
Andrew He <he.andrew.mail@gmail.com>
Andrew Hsu <andrewhsu@docker.com>
Andrew Macpherson <hopscotch23@gmail.com>
Andrew McDonnell <bugs@andrewmcdonnell.net>
@ -86,11 +87,12 @@ Archimedes Trajano <developer@trajano.net>
Arko Dasgupta <arko@tetrate.io>
Arnaud Porterie <icecrime@gmail.com>
Arnaud Rebillout <elboulangero@gmail.com>
Arthur Flageul <arthur.flageul@gmail.com>
Arthur Peka <arthur.peka@outlook.com>
Ashly Mathew <ashly.mathew@sap.com>
Ashwini Oruganti <ashwini.oruganti@gmail.com>
Aslam Ahemad <aslamahemad@gmail.com>
Austin Vazquez <austin.vazquez.dev@gmail.com>
Austin Vazquez <austin.vazquez@docker.com>
Azat Khuyiyakhmetov <shadow_uz@mail.ru>
Bardia Keyoumarsi <bkeyouma@ucsc.edu>
Barnaby Gray <barnaby@pickle.me.uk>
@ -135,10 +137,12 @@ Cao Weiwei <cao.weiwei30@zte.com.cn>
Carlo Mion <mion00@gmail.com>
Carlos Alexandro Becker <caarlos0@gmail.com>
Carlos de Paula <me@carlosedp.com>
carsontham <carsontham@outlook.com>
Carston Schilds <Carston.Schilds@visier.com>
Casey Korver <casey@korver.dev>
Ce Gao <ce.gao@outlook.com>
Cedric Davies <cedricda@microsoft.com>
Cesar Talledo <cesar.talledo@docker.com>
Cezar Sa Espinola <cezarsa@gmail.com>
Chad Faragher <wyckster@hotmail.com>
Chao Wang <wangchao.fnst@cn.fujitsu.com>
@ -220,7 +224,7 @@ David Alvarez <david.alvarez@flyeralarm.com>
David Beitey <david@davidjb.com>
David Calavera <david.calavera@gmail.com>
David Cramer <davcrame@cisco.com>
David Dooling <dooling@gmail.com>
David Dooling <david.dooling@docker.com>
David Gageot <david@gageot.net>
David Karlsson <david.karlsson@docker.com>
David le Blanc <systemmonkey42@users.noreply.github.com>
@ -265,6 +269,7 @@ Eli Uriegas <eli.uriegas@docker.com>
Eli Uriegas <seemethere101@gmail.com>
Elias Faxö <elias.faxo@tre.se>
Elliot Luo <956941328@qq.com>
Eng Zer Jun <engzerjun@gmail.com>
Eric Bode <eric.bode@foundries.io>
Eric Curtin <ericcurtin17@gmail.com>
Eric Engestrom <eric@engestrom.ch>
@ -345,6 +350,7 @@ Henning Sprang <henning.sprang@gmail.com>
Henry N <henrynmail-github@yahoo.de>
Hernan Garcia <hernandanielg@gmail.com>
Hongbin Lu <hongbin034@gmail.com>
Hossein Abbasi <16090309+hsnabszhdn@users.noreply.github.com>
Hu Keping <hukeping@huawei.com>
Huayi Zhang <irachex@gmail.com>
Hugo Chastel <Hugo-C@users.noreply.github.com>
@ -595,6 +601,7 @@ Michael Prokop <github@michael-prokop.at>
Michael Scharf <github@scharf.gr>
Michael Spetsiotis <michael_spets@hotmail.com>
Michael Steinert <mike.steinert@gmail.com>
Michael Tews <michael@tews.dev>
Michael West <mwest@mdsol.com>
Michal Minář <miminar@redhat.com>
Michał Czeraszkiewicz <czerasz@gmail.com>
@ -896,6 +903,7 @@ Wenlong Zhang <zhangwenlong@loongson.cn>
Wenzhi Liang <wenzhi.liang@gmail.com>
Wes Morgan <cap10morgan@gmail.com>
Wewang Xiaorenfine <wang.xiaoren@zte.com.cn>
Will Wang <willww64@gmail.com>
William Henry <whenry@redhat.com>
Xianglin Gao <xlgao@zju.edu.cn>
Xiaodong Liu <liuxiaodong@loongson.cn>

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