Compare commits
78 Commits
fix/613
...
0.13.0-rc1
| Author | SHA1 | Date | |
|---|---|---|---|
|
8c64a8049d
|
|||
|
b073072489
|
|||
|
9daa4fee48
|
|||
| ea48917e6c | |||
| 728f873a3e | |||
|
ddb90dd44d
|
|||
|
7a8485492e
|
|||
| 32bb05abba | |||
|
3d2006a696
|
|||
| 521f5c1647 | |||
| 5eb41bc803 | |||
| fc39721501 | |||
| 44bacc582b | |||
|
53e8b52717
|
|||
|
0aba922dda
|
|||
| 4e0eb739b4 | |||
| 6b661dd7a7 | |||
|
39102752c0
|
|||
| 5cfc1c076c | |||
| 10f7ed74b0 | |||
|
227d37dc26
|
|||
|
0ccf3d2b12
|
|||
|
f87ce74027
|
|||
| 4349ee82bc | |||
|
f9ea7506d0
|
|||
|
1fe2d0421b
|
|||
| 59c0d1f4c5 | |||
|
7c3364f87a
|
|||
| 2c5a273fa7 | |||
|
c54fe3ef85
|
|||
| bda0d23d39 | |||
| b9dc7b8437 | |||
| 8f7dbfedbc | |||
| 4b863f1e15 | |||
| 064c9f5d65 | |||
| 98e48c95c7 | |||
| c85d8ee6d1 | |||
| 23268a0e92 | |||
| d60d426752 | |||
| 0e273de8f6 | |||
| ab32118bfe | |||
| 683396d75a | |||
| 4db6755f0d | |||
| 4c132e30f6 | |||
| f5aeae30c7 | |||
| 1d24107956 | |||
| f835b87255 | |||
|
dba21d6a29
|
|||
| 182fc41c58 | |||
| 304ac87cec | |||
| 5b3929d885 | |||
| c41df874d1 | |||
|
b721adbf9c
|
|||
| 42f9e6d458 | |||
| 9e7bc31d4d | |||
| b79c4f33b6 | |||
| cc87d5b3da | |||
| 8b5e3f3c78 | |||
|
db7c4042d0
|
|||
|
ed1a66dc5f
|
|||
|
bb93e4266a
|
|||
|
a2cc70b2f5
|
|||
|
ce1aa3d870
|
|||
|
d75700c8a9
|
|||
|
0ccc4aae72
|
|||
|
ec22d5d51d
|
|||
|
ab42584d05
|
|||
| 40eb6e9a18 | |||
| 35eb9d4a89 | |||
|
08cc63d523
|
|||
| 797b8d899b | |||
|
fb786306b5
|
|||
| c3a2048eba | |||
| 1bdc11ba62 | |||
| cc8703310c | |||
|
fcd5bd863d
|
|||
|
e6af2da9dd
|
|||
| 4b688825e0 |
@ -1,4 +1,6 @@
|
||||
---
|
||||
version: 2
|
||||
|
||||
gitea_urls:
|
||||
api: https://git.coopcloud.tech/api/v1
|
||||
download: https://git.coopcloud.tech/
|
||||
|
||||
@ -10,14 +10,17 @@
|
||||
- cassowary
|
||||
- chasqui
|
||||
- codegod100
|
||||
- cyrnel
|
||||
- decentral1se
|
||||
- fauno
|
||||
- frando
|
||||
- iexos
|
||||
- jade
|
||||
- kawaiipunk
|
||||
- knoflook
|
||||
- mayel
|
||||
- moritz
|
||||
- namnatulco
|
||||
- p4u1
|
||||
- rix
|
||||
- roxxers
|
||||
|
||||
@ -10,6 +10,7 @@ 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"
|
||||
@ -107,6 +108,15 @@ 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 {
|
||||
@ -260,6 +270,7 @@ checkout as-is. Recipe commit hashes are also supported as values for
|
||||
app.Name,
|
||||
app.Server,
|
||||
internal.DontWaitConverge,
|
||||
internal.NoInput,
|
||||
f,
|
||||
); err != nil {
|
||||
log.Fatal(err)
|
||||
@ -280,13 +291,21 @@ checkout as-is. Recipe commit hashes are also supported as values for
|
||||
}
|
||||
|
||||
func getLatestVersionOrCommit(app appPkg.App) (string, error) {
|
||||
versions, err := app.Recipe.Tags()
|
||||
recipeVersions, warnings, err := app.Recipe.GetRecipeVersions()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(versions) > 0 && !internal.Chaos {
|
||||
return versions[len(versions)-1], nil
|
||||
for _, warning := range warnings {
|
||||
log.Warn(warning)
|
||||
}
|
||||
|
||||
if len(recipeVersions) > 0 && !internal.Chaos {
|
||||
latest := recipeVersions[len(recipeVersions)-1]
|
||||
for tag := range latest {
|
||||
log.Debug(i18n.G("selected latest recipe version: %s (from %d available versions)", tag, len(recipeVersions)))
|
||||
return tag, nil
|
||||
}
|
||||
}
|
||||
|
||||
head, err := app.Recipe.Head()
|
||||
|
||||
@ -113,6 +113,10 @@ Use "--status/-S" flag to query all servers for the live deployment status.`),
|
||||
totalAppsCount++
|
||||
|
||||
if status {
|
||||
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
status := i18n.G("unknown")
|
||||
version := i18n.G("unknown")
|
||||
chaos := i18n.G("unknown")
|
||||
@ -325,6 +329,14 @@ func init() {
|
||||
i18n.G("show apps of a specific server"),
|
||||
)
|
||||
|
||||
AppListCommand.Flags().BoolVarP(
|
||||
&internal.Chaos,
|
||||
i18n.G("chaos"),
|
||||
i18n.G("C"),
|
||||
false,
|
||||
i18n.G("ignore uncommitted recipes changes"),
|
||||
)
|
||||
|
||||
AppListCommand.RegisterFlagCompletionFunc(
|
||||
i18n.G("server"),
|
||||
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
|
||||
@ -72,6 +72,10 @@ var AppNewCommand = &cobra.Command{
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
recipe := internal.ValidateRecipe(args, cmd.Name())
|
||||
|
||||
if err := recipe.Ensure(internal.GetEnsureContext()); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if len(args) == 2 && internal.Chaos {
|
||||
log.Fatal(i18n.G("cannot use [version] and --chaos together"))
|
||||
}
|
||||
@ -98,10 +102,14 @@ var AppNewCommand = &cobra.Command{
|
||||
var recipeVersions recipePkg.RecipeVersions
|
||||
if recipeVersion == "" {
|
||||
var err error
|
||||
recipeVersions, _, err = recipe.GetRecipeVersions()
|
||||
var warnings []string
|
||||
recipeVersions, warnings, err = recipe.GetRecipeVersions()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for _, warning := range warnings {
|
||||
log.Warn(warning)
|
||||
}
|
||||
}
|
||||
|
||||
if len(recipeVersions) > 0 {
|
||||
@ -110,6 +118,8 @@ var AppNewCommand = &cobra.Command{
|
||||
recipeVersion = tag
|
||||
}
|
||||
|
||||
log.Debug(i18n.G("selected recipe version: %s (from %d available versions)", recipeVersion, len(recipeVersions)))
|
||||
|
||||
if _, err := recipe.EnsureVersion(recipeVersion); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@ -128,6 +128,7 @@ Pass "--all-services/-a" to restart all services.`),
|
||||
AppName: app.Name,
|
||||
ServerName: app.Server,
|
||||
Filters: f,
|
||||
NoInput: internal.NoInput,
|
||||
NoLog: true,
|
||||
Quiet: true,
|
||||
}
|
||||
|
||||
@ -246,6 +246,7 @@ beforehand. See "abra app backup" for more.`),
|
||||
stackName,
|
||||
app.Server,
|
||||
internal.DontWaitConverge,
|
||||
internal.NoInput,
|
||||
f,
|
||||
); err != nil {
|
||||
log.Fatal(err)
|
||||
|
||||
@ -282,6 +282,7 @@ beforehand. See "abra app backup" for more.`),
|
||||
stackName,
|
||||
app.Server,
|
||||
internal.DontWaitConverge,
|
||||
internal.NoInput,
|
||||
f,
|
||||
); err != nil {
|
||||
log.Fatal(err)
|
||||
|
||||
@ -64,7 +64,7 @@ func DeployOverview(
|
||||
server = "local"
|
||||
}
|
||||
|
||||
domain := app.Domain
|
||||
domain := fmt.Sprintf("https://%s", app.Domain)
|
||||
if domain == "" {
|
||||
domain = config.MISSING_DEFAULT
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@ import (
|
||||
gitPkg "coopcloud.tech/abra/pkg/git"
|
||||
"coopcloud.tech/abra/pkg/i18n"
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
"coopcloud.tech/abra/pkg/recipe"
|
||||
recipePkg "coopcloud.tech/abra/pkg/recipe"
|
||||
"coopcloud.tech/tagcmp"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/distribution/reference"
|
||||
@ -23,6 +23,9 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Errors
|
||||
var errEmptyVersionsInCatalogue = errors.New(i18n.G("catalogue versions list is unexpectedly empty"))
|
||||
|
||||
// translators: `abra recipe release` aliases. use a comma separated list of
|
||||
// aliases with no spaces in between
|
||||
var recipeReleaseAliases = i18n.G("rl")
|
||||
@ -50,7 +53,7 @@ recipe updates are properly communicated. I.e. developers of an app might
|
||||
publish a minor version but that might lead to changes in the recipe which are
|
||||
major and therefore require intervention while doing the upgrade work.
|
||||
|
||||
Publish your new release to git.coopcloud.tech with "--publish/-p". This
|
||||
This command will publish your new release to git.coopcloud.tech. This
|
||||
requires that you have permission to git push to these repositories and have
|
||||
your SSH keys configured on your account. Enable ssh-agent and make sure to add
|
||||
your private key and enter your passphrase beforehand.
|
||||
@ -60,12 +63,13 @@ your private key and enter your passphrase beforehand.
|
||||
Example: ` # publish release
|
||||
eval ` + "`ssh-agent`" + `
|
||||
ssh-add ~/.ssh/id_ed25519
|
||||
abra recipe release gitea -p`,
|
||||
abra recipe release gitea`,
|
||||
Args: cobra.RangeArgs(1, 2),
|
||||
ValidArgsFunction: func(
|
||||
cmd *cobra.Command,
|
||||
args []string,
|
||||
toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
toComplete string,
|
||||
) ([]string, cobra.ShellCompDirective) {
|
||||
switch l := len(args); l {
|
||||
case 0:
|
||||
return autocomplete.RecipeNameComplete()
|
||||
@ -93,19 +97,207 @@ your private key and enter your passphrase beforehand.
|
||||
log.Fatal(i18n.G("main app service version for %s is empty?", recipe.Name))
|
||||
}
|
||||
|
||||
isClean, err := gitPkg.IsClean(recipe.Dir)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if !isClean {
|
||||
log.Fatal(i18n.G("working directory not clean in %s, aborting", recipe.Dir))
|
||||
}
|
||||
|
||||
tags, err := recipe.Tags()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var tagString string
|
||||
if len(args) == 2 {
|
||||
tagString = args[1]
|
||||
}
|
||||
|
||||
if tagString != "" {
|
||||
if _, err := tagcmp.Parse(tagString); err != nil {
|
||||
log.Fatal(i18n.G("cannot parse %s, invalid tag specified?", tagString))
|
||||
if (internal.Major || internal.Minor || internal.Patch) && tagString != "" {
|
||||
log.Fatal(i18n.G("cannot specify tag and bump type at the same time"))
|
||||
}
|
||||
|
||||
if len(tags) == 0 && tagString == "" {
|
||||
log.Warn(i18n.G("no git tags found for %s", recipe.Name))
|
||||
if internal.NoInput {
|
||||
log.Fatal(i18n.G("unable to continue, input required for initial version"))
|
||||
}
|
||||
fmt.Println(i18n.G(`
|
||||
The following options are two types of initial semantic version that you can
|
||||
pick for %s that will be published in the recipe catalogue. This follows the
|
||||
semver convention (more on https://semver.org), here is a short cheatsheet
|
||||
|
||||
0.1.0: development release, still hacking. when you make a major upgrade
|
||||
you increment the "y" part (i.e. 0.1.0 -> 0.2.0) and only move to
|
||||
using the "x" part when things are stable.
|
||||
|
||||
1.0.0: public release, assumed to be working. you already have a stable
|
||||
and reliable deployment of this app and feel relatively confident
|
||||
about it.
|
||||
|
||||
If you want people to be able alpha test your current config for %s but don't
|
||||
think it is quite reliable, go with 0.1.0 and people will know that things are
|
||||
likely to change.
|
||||
|
||||
`, recipe.Name, recipe.Name))
|
||||
var chosenVersion string
|
||||
edPrompt := &survey.Select{
|
||||
Message: i18n.G("which version do you want to begin with?"),
|
||||
Options: []string{"0.1.0", "1.0.0"},
|
||||
}
|
||||
|
||||
if err := survey.AskOne(edPrompt, &chosenVersion); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
tagString = fmt.Sprintf("%s+%s", chosenVersion, mainAppVersion)
|
||||
}
|
||||
|
||||
if tagString == "" && (!internal.Major && !internal.Minor && !internal.Patch) {
|
||||
|
||||
catl, err := recipePkg.ReadRecipeCatalogue(false)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
changesTable, err := formatter.CreateTable()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
latestRelease := tags[len(tags)-1]
|
||||
latestRecipeVersion, err := getLatestVersion(recipe, catl)
|
||||
if err != nil && err != errEmptyVersionsInCatalogue {
|
||||
log.Fatal(err)
|
||||
}
|
||||
changesTable.Headers(i18n.G("SERVICE"), latestRelease, i18n.G("PROPOSED CHANGES"))
|
||||
|
||||
allRecipeVersions := catl[recipe.Name].Versions
|
||||
for _, recipeVersion := range allRecipeVersions {
|
||||
if serviceVersions, ok := recipeVersion[latestRecipeVersion]; ok {
|
||||
for serviceName := range serviceVersions {
|
||||
serviceMeta := serviceVersions[serviceName]
|
||||
|
||||
existingImageTag := fmt.Sprintf("%s:%s", serviceMeta.Image, serviceMeta.Tag)
|
||||
newImageTag := fmt.Sprintf("%s:%s", serviceMeta.Image, imagesTmp[serviceMeta.Image])
|
||||
|
||||
if existingImageTag == newImageTag {
|
||||
continue
|
||||
}
|
||||
|
||||
changesTable.Row([]string{serviceName, existingImageTag, newImageTag}...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
changeOverview := changesTable.Render()
|
||||
|
||||
if err := internal.PromptBumpType("", latestRelease, changeOverview); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
if (internal.Major || internal.Minor || internal.Patch) && tagString != "" {
|
||||
log.Fatal(i18n.G("cannot specify tag and bump type at the same time"))
|
||||
if tagString == "" {
|
||||
repo, err := git.PlainOpen(recipe.Dir)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var lastGitTag tagcmp.Tag
|
||||
iter, err := repo.Tags()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if err := iter.ForEach(func(ref *plumbing.Reference) error {
|
||||
obj, err := repo.TagObject(ref.Hash())
|
||||
if err != nil {
|
||||
log.Fatal(i18n.G("tag at commit %s is unannotated or otherwise broken", ref.Hash()))
|
||||
return err
|
||||
}
|
||||
|
||||
tagcmpTag, err := tagcmp.Parse(obj.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if (lastGitTag == tagcmp.Tag{}) {
|
||||
lastGitTag = tagcmpTag
|
||||
} else if tagcmpTag.IsGreaterThan(lastGitTag) {
|
||||
lastGitTag = tagcmpTag
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// bumpType is used to decide what part of the tag should be incremented
|
||||
bumpType := btoi(internal.Major)*4 + btoi(internal.Minor)*2 + btoi(internal.Patch)
|
||||
if bumpType != 0 {
|
||||
// a bitwise check if the number is a power of 2
|
||||
if (bumpType & (bumpType - 1)) != 0 {
|
||||
log.Fatal(i18n.G("you can only use one version flag: --major, --minor or --patch"))
|
||||
}
|
||||
}
|
||||
|
||||
newTag := lastGitTag
|
||||
if bumpType > 0 {
|
||||
if internal.Patch {
|
||||
now, err := strconv.Atoi(newTag.Patch)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
newTag.Patch = strconv.Itoa(now + 1)
|
||||
} else if internal.Minor {
|
||||
now, err := strconv.Atoi(newTag.Minor)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
newTag.Patch = "0"
|
||||
newTag.Minor = strconv.Itoa(now + 1)
|
||||
} else if internal.Major {
|
||||
now, err := strconv.Atoi(newTag.Major)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
newTag.Patch = "0"
|
||||
newTag.Minor = "0"
|
||||
newTag.Major = strconv.Itoa(now + 1)
|
||||
}
|
||||
}
|
||||
|
||||
newTag.Metadata = mainAppVersion
|
||||
log.Debug(i18n.G("choosing %s as new version for %s", newTag.String(), recipe.Name))
|
||||
tagString = newTag.String()
|
||||
}
|
||||
|
||||
if _, err := tagcmp.Parse(tagString); err != nil {
|
||||
log.Fatal(i18n.G("invalid version %s specified", tagString))
|
||||
}
|
||||
|
||||
mainService := "app"
|
||||
label := i18n.G("coop-cloud.${STACK_NAME}.version=%s", tagString)
|
||||
if !internal.Dry {
|
||||
if err := recipe.UpdateLabel("compose.y*ml", mainService, label); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
} else {
|
||||
log.Info(i18n.G("dry run: not syncing label %s for recipe %s", tagString, recipe.Name))
|
||||
}
|
||||
|
||||
for _, tag := range tags {
|
||||
previousTagLeftHand := strings.Split(tag, "+")[0]
|
||||
newTagStringLeftHand := strings.Split(tagString, "+")[0]
|
||||
if previousTagLeftHand == newTagStringLeftHand {
|
||||
log.Fatal(i18n.G("%s+... conflicts with a previous release: %s", newTagStringLeftHand, tag))
|
||||
}
|
||||
}
|
||||
|
||||
repo, err := git.PlainOpen(recipe.Dir)
|
||||
@ -118,84 +310,20 @@ your private key and enter your passphrase beforehand.
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if tagString != "" {
|
||||
if err := createReleaseFromTag(recipe, tagString, mainAppVersion); err != nil {
|
||||
if cleanErr := cleanTag(recipe, tagString); cleanErr != nil {
|
||||
log.Fatal(cleanErr)
|
||||
}
|
||||
if cleanErr := cleanCommit(recipe, preCommitHead); cleanErr != nil {
|
||||
log.Fatal(cleanErr)
|
||||
}
|
||||
log.Fatal(err)
|
||||
if err := createReleaseFromTag(recipe, tagString, mainAppVersion); err != nil {
|
||||
if cleanErr := cleanTag(recipe, tagString); cleanErr != nil {
|
||||
log.Fatal(cleanErr)
|
||||
}
|
||||
if cleanErr := cleanCommit(recipe, preCommitHead); cleanErr != nil {
|
||||
log.Fatal(cleanErr)
|
||||
}
|
||||
}
|
||||
|
||||
tags, err := recipe.Tags()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
labelVersion, err := getLabelVersion(recipe, false)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
for _, tag := range tags {
|
||||
previousTagLeftHand := strings.Split(tag, "+")[0]
|
||||
newTagStringLeftHand := strings.Split(labelVersion, "+")[0]
|
||||
if previousTagLeftHand == newTagStringLeftHand {
|
||||
log.Fatal(i18n.G("%s+... conflicts with a previous release: %s", newTagStringLeftHand, tag))
|
||||
}
|
||||
}
|
||||
|
||||
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 recipe.Recipe) (map[string]string, error) {
|
||||
func GetImageVersions(recipe recipePkg.Recipe) (map[string]string, error) {
|
||||
services := make(map[string]string)
|
||||
|
||||
config, err := recipe.GetComposeConfig(nil)
|
||||
@ -239,7 +367,7 @@ func GetImageVersions(recipe recipe.Recipe) (map[string]string, error) {
|
||||
}
|
||||
|
||||
// createReleaseFromTag creates a new release based on a supplied recipe version string
|
||||
func createReleaseFromTag(recipe recipe.Recipe, tagString, mainAppVersion string) error {
|
||||
func createReleaseFromTag(recipe recipePkg.Recipe, tagString, mainAppVersion string) error {
|
||||
var err error
|
||||
|
||||
repo, err := git.PlainOpen(recipe.Dir)
|
||||
@ -302,7 +430,7 @@ func getTagCreateOptions(tag string) (git.CreateTagOptions, error) {
|
||||
|
||||
// addReleaseNotes checks if the release/next release note exists and moves the
|
||||
// file to release/<tag>.
|
||||
func addReleaseNotes(recipe recipe.Recipe, tag string) error {
|
||||
func addReleaseNotes(recipe recipePkg.Recipe, tag string) error {
|
||||
releaseDir := path.Join(recipe.Dir, "release")
|
||||
if _, err := os.Stat(releaseDir); errors.Is(err, os.ErrNotExist) {
|
||||
if err := os.Mkdir(releaseDir, 0755); err != nil {
|
||||
@ -387,7 +515,7 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func commitRelease(recipe recipe.Recipe, tag string) error {
|
||||
func commitRelease(recipe recipePkg.Recipe, tag string) error {
|
||||
if internal.Dry {
|
||||
log.Debug(i18n.G("dry run: no changes committed"))
|
||||
return nil
|
||||
@ -439,140 +567,29 @@ func tagRelease(tagString string, repo *git.Repository) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func pushRelease(recipe recipe.Recipe, tagString string) error {
|
||||
func pushRelease(recipe recipePkg.Recipe, tagString string) error {
|
||||
if internal.Dry {
|
||||
log.Info(i18n.G("dry run: no changes published"))
|
||||
return nil
|
||||
}
|
||||
|
||||
if !publish && !internal.NoInput {
|
||||
prompt := &survey.Confirm{
|
||||
Message: i18n.G("publish new release?"),
|
||||
}
|
||||
|
||||
if err := survey.AskOne(prompt, &publish); err != nil {
|
||||
return err
|
||||
}
|
||||
if os.Getenv("SSH_AUTH_SOCK") == "" {
|
||||
return errors.New(i18n.G("ssh-agent not found. see \"abra recipe release --help\" and try again"))
|
||||
}
|
||||
|
||||
if publish {
|
||||
if os.Getenv("SSH_AUTH_SOCK") == "" {
|
||||
return errors.New(i18n.G("ssh-agent not found. see \"abra recipe release --help\" and try again"))
|
||||
}
|
||||
|
||||
if err := recipe.Push(internal.Dry); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/src/tag/%s", recipe.GitURL, tagString)
|
||||
log.Info(i18n.G("new release published: %s", url))
|
||||
} else {
|
||||
log.Info(i18n.G("no -p/--publish passed, not publishing"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createReleaseFromPreviousTag(tagString, mainAppVersion string, recipe recipe.Recipe, tags []string) error {
|
||||
repo, err := git.PlainOpen(recipe.Dir)
|
||||
if err != nil {
|
||||
if err := recipe.Push(internal.Dry); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bumpType := btoi(internal.Major)*4 + btoi(internal.Minor)*2 + btoi(internal.Patch)
|
||||
if bumpType != 0 {
|
||||
if (bumpType & (bumpType - 1)) != 0 {
|
||||
return errors.New(i18n.G("you can only use one of: --major, --minor, --patch"))
|
||||
}
|
||||
}
|
||||
|
||||
var lastGitTag tagcmp.Tag
|
||||
for _, tag := range tags {
|
||||
parsed, err := tagcmp.Parse(tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if (lastGitTag == tagcmp.Tag{}) {
|
||||
lastGitTag = parsed
|
||||
} else if parsed.IsGreaterThan(lastGitTag) {
|
||||
lastGitTag = parsed
|
||||
}
|
||||
}
|
||||
|
||||
newTag := lastGitTag
|
||||
if internal.Patch {
|
||||
now, err := strconv.Atoi(newTag.Patch)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newTag.Patch = strconv.Itoa(now + 1)
|
||||
} else if internal.Minor {
|
||||
now, err := strconv.Atoi(newTag.Minor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newTag.Patch = "0"
|
||||
newTag.Minor = strconv.Itoa(now + 1)
|
||||
} else if internal.Major {
|
||||
now, err := strconv.Atoi(newTag.Major)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newTag.Patch = "0"
|
||||
newTag.Minor = "0"
|
||||
newTag.Major = strconv.Itoa(now + 1)
|
||||
}
|
||||
|
||||
if internal.Major || internal.Minor || internal.Patch {
|
||||
newTag.Metadata = mainAppVersion
|
||||
tagString = newTag.String()
|
||||
}
|
||||
|
||||
if lastGitTag.String() == tagString {
|
||||
return errors.New(i18n.G("latest git tag (%s) and synced label (%s) are the same?", lastGitTag, tagString))
|
||||
}
|
||||
|
||||
if !internal.NoInput {
|
||||
prompt := &survey.Confirm{
|
||||
Message: i18n.G("current: %s, new: %s, correct?", lastGitTag, tagString),
|
||||
}
|
||||
|
||||
var ok bool
|
||||
if err := survey.AskOne(prompt, &ok); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !ok {
|
||||
return errors.New(i18n.G("exiting as requested"))
|
||||
}
|
||||
}
|
||||
|
||||
if err := addReleaseNotes(recipe, tagString); err != nil {
|
||||
return errors.New(i18n.G("failed to add release notes: %s", err.Error()))
|
||||
}
|
||||
|
||||
if err := commitRelease(recipe, tagString); err != nil {
|
||||
return errors.New(i18n.G("failed to commit changes: %s", err.Error()))
|
||||
}
|
||||
|
||||
if err := tagRelease(tagString, repo); err != nil {
|
||||
return errors.New(i18n.G("failed to tag release: %s", err.Error()))
|
||||
}
|
||||
|
||||
if err := pushRelease(recipe, tagString); err != nil {
|
||||
return errors.New(i18n.G("failed to publish new release: %s", err.Error()))
|
||||
}
|
||||
url := fmt.Sprintf("%s/src/tag/%s", recipe.GitURL, tagString)
|
||||
log.Info(i18n.G("new release published: %s", url))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// cleanCommit soft removes the latest release commit. No change are lost the
|
||||
// the commit itself is removed. This is the equivalent of `git reset HEAD~1`.
|
||||
func cleanCommit(recipe recipe.Recipe, head *plumbing.Reference) error {
|
||||
func cleanCommit(recipe recipePkg.Recipe, head *plumbing.Reference) error {
|
||||
repo, err := git.PlainOpen(recipe.Dir)
|
||||
if err != nil {
|
||||
return errors.New(i18n.G("unable to open repo in %s: %s", recipe.Dir, err))
|
||||
@ -594,7 +611,7 @@ func cleanCommit(recipe recipe.Recipe, head *plumbing.Reference) error {
|
||||
}
|
||||
|
||||
// cleanTag removes a freshly created tag
|
||||
func cleanTag(recipe recipe.Recipe, tag string) error {
|
||||
func cleanTag(recipe recipePkg.Recipe, tag string) error {
|
||||
repo, err := git.PlainOpen(recipe.Dir)
|
||||
if err != nil {
|
||||
return errors.New(i18n.G("unable to open repo in %s: %s", recipe.Dir, err))
|
||||
@ -611,37 +628,17 @@ func cleanTag(recipe recipe.Recipe, tag string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func getLabelVersion(recipe recipe.Recipe, prompt bool) (string, error) {
|
||||
initTag, err := recipe.GetVersionLabelLocal()
|
||||
func getLatestVersion(recipe recipePkg.Recipe, catl recipePkg.RecipeCatalogue) (string, error) {
|
||||
versions, err := recipePkg.GetRecipeCatalogueVersions(recipe.Name, catl)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if initTag == "" {
|
||||
return "", errors.New(i18n.G("unable to read version for %s from synced label. Did you try running \"abra recipe sync %s\" already?", recipe.Name, recipe.Name))
|
||||
if len(versions) > 0 {
|
||||
return versions[len(versions)-1], nil
|
||||
}
|
||||
|
||||
log.Warn(i18n.G("discovered %s as currently synced recipe label", initTag))
|
||||
|
||||
if prompt && !internal.NoInput {
|
||||
var response bool
|
||||
prompt := &survey.Confirm{Message: i18n.G("use %s as the new version?", initTag)}
|
||||
if err := survey.AskOne(prompt, &response); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if !response {
|
||||
return "", errors.New(i18n.G("please fix your synced label for %s and re-run this command", recipe.Name))
|
||||
}
|
||||
}
|
||||
|
||||
return initTag, nil
|
||||
return "", errEmptyVersionsInCatalogue
|
||||
}
|
||||
|
||||
var (
|
||||
publish bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
RecipeReleaseCommand.Flags().BoolVarP(
|
||||
&internal.Dry,
|
||||
@ -674,12 +671,4 @@ func init() {
|
||||
false,
|
||||
i18n.G("increase the patch part of the version"),
|
||||
)
|
||||
|
||||
RecipeReleaseCommand.Flags().BoolVarP(
|
||||
&publish,
|
||||
i18n.G("publish"),
|
||||
i18n.G("p"),
|
||||
false,
|
||||
i18n.G("publish changes to git.coopcloud.tech"),
|
||||
)
|
||||
}
|
||||
|
||||
33
cli/recipe/release_test.go
Normal file
33
cli/recipe/release_test.go
Normal file
@ -0,0 +1,33 @@
|
||||
package recipe
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
recipePkg "coopcloud.tech/abra/pkg/recipe"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetLatestVersionReturnsErrorWhenVersionsIsEmpty(t *testing.T) {
|
||||
recipe := recipePkg.Recipe{}
|
||||
catalogue := recipePkg.RecipeCatalogue{}
|
||||
_, err := getLatestVersion(recipe, catalogue)
|
||||
assert.Equal(t, err, errEmptyVersionsInCatalogue)
|
||||
}
|
||||
|
||||
func TestGetLatestVersionReturnsLastVersion(t *testing.T) {
|
||||
recipe := recipePkg.Recipe{
|
||||
Name: "test",
|
||||
}
|
||||
versions := []map[string]map[string]recipePkg.ServiceMeta{
|
||||
make(map[string]map[string]recipePkg.ServiceMeta),
|
||||
make(map[string]map[string]recipePkg.ServiceMeta),
|
||||
}
|
||||
versions[0]["0.0.3"] = make(map[string]recipePkg.ServiceMeta)
|
||||
versions[1]["0.0.2"] = make(map[string]recipePkg.ServiceMeta)
|
||||
catalogue := make(recipePkg.RecipeCatalogue)
|
||||
catalogue["test"] = recipePkg.RecipeMeta{
|
||||
Versions: versions,
|
||||
}
|
||||
version, _ := getLatestVersion(recipe, catalogue)
|
||||
assert.Equal(t, version, "0.0.3")
|
||||
}
|
||||
@ -1,300 +0,0 @@
|
||||
package recipe
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"coopcloud.tech/abra/cli/internal"
|
||||
"coopcloud.tech/abra/pkg/autocomplete"
|
||||
"coopcloud.tech/abra/pkg/formatter"
|
||||
gitPkg "coopcloud.tech/abra/pkg/git"
|
||||
"coopcloud.tech/abra/pkg/i18n"
|
||||
"coopcloud.tech/abra/pkg/log"
|
||||
recipePkg "coopcloud.tech/abra/pkg/recipe"
|
||||
"coopcloud.tech/tagcmp"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// translators: `abra recipe reset` aliases. use a comma separated list of
|
||||
// aliases with no spaces in between
|
||||
var recipeSyncAliases = i18n.G("s")
|
||||
|
||||
var RecipeSyncCommand = &cobra.Command{
|
||||
// translators: `recipe sync` command
|
||||
Use: i18n.G("sync <recipe> [version] [flags]"),
|
||||
Aliases: strings.Split(recipeSyncAliases, ","),
|
||||
// translators: Short description for `recipe sync` command
|
||||
Short: i18n.G("Sync recipe version label"),
|
||||
Long: i18n.G(`Generate labels for the main recipe service.
|
||||
|
||||
By convention, the service named "app" using the following format:
|
||||
|
||||
coop-cloud.${STACK_NAME}.version=<version>
|
||||
|
||||
Where [version] can be specifed on the command-line or Abra can attempt to
|
||||
auto-generate it for you. The <recipe> configuration will be updated on the
|
||||
local file system.`),
|
||||
Args: cobra.RangeArgs(1, 2),
|
||||
ValidArgsFunction: func(
|
||||
cmd *cobra.Command,
|
||||
args []string,
|
||||
toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
switch l := len(args); l {
|
||||
case 0:
|
||||
return autocomplete.RecipeNameComplete()
|
||||
case 1:
|
||||
return autocomplete.RecipeVersionComplete(args[0])
|
||||
default:
|
||||
return nil, cobra.ShellCompDirectiveError
|
||||
}
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
recipe := internal.ValidateRecipe(args, cmd.Name())
|
||||
|
||||
mainApp, err := internal.GetMainAppImage(recipe)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
imagesTmp, err := GetImageVersions(recipe)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
mainAppVersion := imagesTmp[mainApp]
|
||||
|
||||
tags, err := recipe.Tags()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var nextTag string
|
||||
if len(args) == 2 {
|
||||
nextTag = args[1]
|
||||
}
|
||||
|
||||
if len(tags) == 0 && nextTag == "" {
|
||||
log.Warn(i18n.G("no git tags found for %s", recipe.Name))
|
||||
if internal.NoInput {
|
||||
log.Fatal(i18n.G("unable to continue, input required for initial version"))
|
||||
}
|
||||
fmt.Println(i18n.G(`
|
||||
The following options are two types of initial semantic version that you can
|
||||
pick for %s that will be published in the recipe catalogue. This follows the
|
||||
semver convention (more on https://semver.org), here is a short cheatsheet
|
||||
|
||||
0.1.0: development release, still hacking. when you make a major upgrade
|
||||
you increment the "y" part (i.e. 0.1.0 -> 0.2.0) and only move to
|
||||
using the "x" part when things are stable.
|
||||
|
||||
1.0.0: public release, assumed to be working. you already have a stable
|
||||
and reliable deployment of this app and feel relatively confident
|
||||
about it.
|
||||
|
||||
If you want people to be able alpha test your current config for %s but don't
|
||||
think it is quite reliable, go with 0.1.0 and people will know that things are
|
||||
likely to change.
|
||||
|
||||
`, recipe.Name, recipe.Name))
|
||||
var chosenVersion string
|
||||
edPrompt := &survey.Select{
|
||||
Message: i18n.G("which version do you want to begin with?"),
|
||||
Options: []string{"0.1.0", "1.0.0"},
|
||||
}
|
||||
|
||||
if err := survey.AskOne(edPrompt, &chosenVersion); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
nextTag = fmt.Sprintf("%s+%s", chosenVersion, mainAppVersion)
|
||||
}
|
||||
|
||||
if nextTag == "" && (!internal.Major && !internal.Minor && !internal.Patch) {
|
||||
var changeOverview string
|
||||
|
||||
catl, err := recipePkg.ReadRecipeCatalogue(false)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
versions, err := recipePkg.GetRecipeCatalogueVersions(recipe.Name, catl)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
changesTable, err := formatter.CreateTable()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
latestRelease := tags[len(tags)-1]
|
||||
changesTable.Headers(i18n.G("SERVICE"), latestRelease, i18n.G("PROPOSED CHANGES"))
|
||||
|
||||
latestRecipeVersion := versions[len(versions)-1]
|
||||
allRecipeVersions := catl[recipe.Name].Versions
|
||||
for _, recipeVersion := range allRecipeVersions {
|
||||
if serviceVersions, ok := recipeVersion[latestRecipeVersion]; ok {
|
||||
for serviceName := range serviceVersions {
|
||||
serviceMeta := serviceVersions[serviceName]
|
||||
|
||||
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"),
|
||||
)
|
||||
}
|
||||
@ -57,14 +57,13 @@ is up to the end-user to decide.
|
||||
|
||||
The command is interactive and will show a select input which allows you to
|
||||
make a seclection. Use the "?" key to see more help on navigating this
|
||||
interface.
|
||||
|
||||
You may invoke this command in "wizard" mode and be prompted for input.`),
|
||||
interface.`),
|
||||
Args: cobra.RangeArgs(0, 1),
|
||||
ValidArgsFunction: func(
|
||||
cmd *cobra.Command,
|
||||
args []string,
|
||||
toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
toComplete string,
|
||||
) ([]string, cobra.ShellCompDirective) {
|
||||
return autocomplete.RecipeNameComplete()
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
@ -337,12 +336,37 @@ You may invoke this command in "wizard" mode and be prompted for input.`),
|
||||
if err := gitPkg.DiffUnstaged(recipe.Dir); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if !internal.NoInput && !createCommit {
|
||||
prompt := &survey.Confirm{
|
||||
Message: i18n.G("commit changes?"),
|
||||
Default: true,
|
||||
}
|
||||
|
||||
if err := survey.AskOne(prompt, &createCommit); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
if createCommit {
|
||||
msg := i18n.G("chore: update image tags")
|
||||
if err := gitPkg.Commit(recipe.Dir, msg, internal.Dry); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Info(i18n.G("committed changes as '%s'", msg))
|
||||
}
|
||||
|
||||
} else {
|
||||
if createCommit {
|
||||
log.Warn(i18n.G("no changes, skip creating commit"))
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
var (
|
||||
allTags bool
|
||||
allTags bool
|
||||
createCommit bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -385,4 +409,12 @@ 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"),
|
||||
)
|
||||
}
|
||||
|
||||
@ -245,7 +245,6 @@ Config:
|
||||
recipe.RecipeNewCommand,
|
||||
recipe.RecipeReleaseCommand,
|
||||
recipe.RecipeResetCommand,
|
||||
recipe.RecipeSyncCommand,
|
||||
recipe.RecipeUpgradeCommand,
|
||||
recipe.RecipeVersionCommand,
|
||||
)
|
||||
|
||||
6
go.mod
6
go.mod
@ -8,13 +8,12 @@ require (
|
||||
coopcloud.tech/tagcmp v0.0.0-20250818180036-0ec1b205b5ca
|
||||
git.coopcloud.tech/toolshed/godotenv v1.5.2-0.20250103171850-4d0ca41daa5c
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7
|
||||
github.com/charmbracelet/bubbles v0.21.0
|
||||
github.com/charmbracelet/bubbletea v1.3.10
|
||||
github.com/charmbracelet/lipgloss v1.1.0
|
||||
github.com/charmbracelet/log v0.4.2
|
||||
github.com/distribution/reference v0.6.0
|
||||
github.com/docker/cli v28.4.0+incompatible
|
||||
github.com/docker/docker v28.4.0+incompatible
|
||||
github.com/docker/docker v28.5.2+incompatible
|
||||
github.com/docker/go-units v0.5.0
|
||||
github.com/go-git/go-git/v5 v5.16.2
|
||||
github.com/google/go-cmp v0.7.0
|
||||
@ -42,6 +41,7 @@ require (
|
||||
github.com/charmbracelet/colorprofile v0.3.2 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.10.2 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.2.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
@ -157,3 +157,5 @@ require (
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
golang.org/x/sys v0.36.0
|
||||
)
|
||||
|
||||
replace github.com/docker/cli v28.4.0+incompatible => git.coopcloud.tech/toolshed/docker-cli v28.5.3-0.20260202112816-30df2d0b3a00+incompatible
|
||||
|
||||
10
go.sum
10
go.sum
@ -27,6 +27,8 @@ coopcloud.tech/tagcmp v0.0.0-20250818180036-0ec1b205b5ca/go.mod h1:ESVm0wQKcbcFi
|
||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
git.coopcloud.tech/toolshed/docker-cli v28.5.3-0.20260202112816-30df2d0b3a00+incompatible h1:YdW2uK5sHj545lGz/FrozPueINkQ7fUjlsNd8aYcqik=
|
||||
git.coopcloud.tech/toolshed/docker-cli v28.5.3-0.20260202112816-30df2d0b3a00+incompatible/go.mod h1:PY19bHY5R4DLmRuCrv4TR7etURn/+tSTFuam4FUTiD8=
|
||||
git.coopcloud.tech/toolshed/godotenv v1.5.2-0.20250103171850-4d0ca41daa5c h1:oeKnUB79PKYD8D0/unYuu7MRcWryQQWOns8+JL+acrs=
|
||||
git.coopcloud.tech/toolshed/godotenv v1.5.2-0.20250103171850-4d0ca41daa5c/go.mod h1:fQuhwrpg6qb9NlFXKYi/LysWu1wxjraS8sxyW12CUF0=
|
||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
|
||||
@ -133,8 +135,6 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
|
||||
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
|
||||
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
||||
github.com/charmbracelet/colorprofile v0.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqITmrvb1uTIiI=
|
||||
@ -320,16 +320,14 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E=
|
||||
github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/cli v28.4.0+incompatible h1:RBcf3Kjw2pMtwui5V0DIMdyeab8glEw5QY0UUU4C9kY=
|
||||
github.com/docker/cli v28.4.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY=
|
||||
github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
|
||||
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v28.4.0+incompatible h1:KVC7bz5zJY/4AZe/78BIvCnPsLaC9T/zh72xnlrTTOk=
|
||||
github.com/docker/docker v28.4.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
|
||||
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y=
|
||||
github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
|
||||
github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=
|
||||
|
||||
@ -633,6 +633,11 @@ func (a App) WipeRecipeVersion() error {
|
||||
|
||||
// WriteRecipeVersion writes the recipe version to the app .env file.
|
||||
func (a App) WriteRecipeVersion(version string, dryRun bool) error {
|
||||
if version == config.UNKNOWN_DEFAULT {
|
||||
log.Debug(i18n.G("version is unknown, skipping env write"))
|
||||
return nil
|
||||
}
|
||||
|
||||
file, err := os.Open(a.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@ -224,3 +224,16 @@ func TestWriteRecipeVersionOverwrite(t *testing.T) {
|
||||
|
||||
assert.Equal(t, "foo", app.Recipe.EnvVersion)
|
||||
}
|
||||
|
||||
func TestWriteRecipeVersionUnknown(t *testing.T) {
|
||||
app, err := appPkg.GetApp(testPkg.ExpectedAppFiles, testPkg.AppName)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := app.WriteRecipeVersion(config.UNKNOWN_DEFAULT, false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.NotEqual(t, config.UNKNOWN_DEFAULT, app.Recipe.EnvVersion)
|
||||
}
|
||||
|
||||
@ -8,7 +8,6 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
contextPkg "coopcloud.tech/abra/pkg/context"
|
||||
@ -37,18 +36,27 @@ func WithTimeout(timeout int) Opt {
|
||||
// New initiates a new Docker client. New client connections are validated so
|
||||
// that we ensure connections via SSH to the daemon can succeed. It takes into
|
||||
// account that you may only want the local client and not communicate via SSH.
|
||||
// For this use-case, please pass "default" as the contextName.
|
||||
// For this use-case, please pass "default" as the serverName.
|
||||
func New(serverName string, opts ...Opt) (*client.Client, error) {
|
||||
var clientOpts []client.Opt
|
||||
|
||||
ctx, err := GetContext(serverName)
|
||||
if err != nil {
|
||||
serverDir := path.Join(config.SERVERS_DIR, serverName)
|
||||
if _, err := os.Stat(serverDir); err == nil {
|
||||
return nil, errors.New(i18n.G("server missing context, run \"abra server add %s\"?", serverName))
|
||||
if _, err := os.Stat(serverDir); err != nil {
|
||||
return nil, errors.New(i18n.G("server missing, run \"abra server add %s\"?", serverName))
|
||||
}
|
||||
|
||||
return nil, errors.New(i18n.G("unknown server, run \"abra server add %s\"?", serverName))
|
||||
// NOTE(p4u1): when the docker context does not exist but the server folder
|
||||
// is there, let's create a new docker context.
|
||||
if err = CreateContext(serverName); err != nil {
|
||||
return nil, errors.New(i18n.G("server missing context, context creation failed: %s", err))
|
||||
}
|
||||
|
||||
ctx, err = GetContext(serverName)
|
||||
if err != nil {
|
||||
return nil, errors.New(i18n.G("server missing context, run \"abra server add %s\"?", serverName))
|
||||
}
|
||||
}
|
||||
|
||||
ctxEndpoint, err := contextPkg.GetContextEndpoint(ctx)
|
||||
@ -75,8 +83,7 @@ func New(serverName string, opts ...Opt) (*client.Client, error) {
|
||||
|
||||
httpClient := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: helper.Dialer,
|
||||
IdleConnTimeout: 30 * time.Second,
|
||||
DialContext: helper.Dialer,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -165,7 +165,13 @@ func GetImagesForStack(cl *dockerClient.Client, app appPkg.App) (map[string]stri
|
||||
}
|
||||
|
||||
imageBaseName := reference.Path(imageParsed)
|
||||
imageTag := imageParsed.(reference.NamedTagged).Tag()
|
||||
namedTag, ok := imageParsed.(reference.NamedTagged)
|
||||
if !ok {
|
||||
// This is an image without a tag
|
||||
images[imageBaseName] = ""
|
||||
continue
|
||||
}
|
||||
imageTag := namedTag.Tag()
|
||||
|
||||
existingImageVersion, ok := images[imageBaseName]
|
||||
if !ok {
|
||||
@ -282,7 +288,13 @@ func GatherImagesForDeploy(cl *dockerClient.Client, app appPkg.App, compose *com
|
||||
}
|
||||
|
||||
imageBaseName := reference.Path(imageParsed)
|
||||
imageTag := imageParsed.(reference.NamedTagged).Tag()
|
||||
namedTag, ok := imageParsed.(reference.NamedTagged)
|
||||
if !ok {
|
||||
// This is an image without a tag
|
||||
newImages[imageBaseName] = ""
|
||||
continue
|
||||
}
|
||||
imageTag := namedTag.Tag()
|
||||
|
||||
existingImageVersion, ok := newImages[imageBaseName]
|
||||
if !ok {
|
||||
|
||||
@ -35,7 +35,7 @@ func Commit(repoPath, commitMessage string, dryRun bool) error {
|
||||
|
||||
if !dryRun {
|
||||
// NOTE(d1): `All: true` does not include untracked files
|
||||
_, err = commitWorktree.Commit(commitMessage, &git.CommitOptions{All: true})
|
||||
_, err := commitWorktree.Commit(commitMessage, &git.CommitOptions{All: true})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -142,7 +142,7 @@ var LintRules = map[string][]LintRule{
|
||||
Function: LintAppService,
|
||||
},
|
||||
{
|
||||
Ref: "R015",
|
||||
Ref: "R016",
|
||||
Level: i18n.G("error"),
|
||||
Description: i18n.G("deploy labels stanza present"),
|
||||
HowToResolve: i18n.G("include \"deploy: labels: ...\" stanza"),
|
||||
@ -258,7 +258,7 @@ func LintAppService(recipe recipe.Recipe) (bool, error) {
|
||||
func LintTraefikEnabledSkipCondition(r recipe.Recipe) (bool, error) {
|
||||
sampleEnv, err := r.SampleEnv()
|
||||
if err != nil {
|
||||
return false, errors.New(i18n.G("unable to discover .env.sample for %s", r.Name))
|
||||
return false, errors.New(i18n.G(".env.sample for %s couldn't be read: %s", r.Name, err))
|
||||
}
|
||||
|
||||
if _, ok := sampleEnv["DOMAIN"]; !ok {
|
||||
|
||||
@ -15,7 +15,7 @@ import (
|
||||
func (r Recipe) SampleEnv() (map[string]string, error) {
|
||||
sampleEnv, err := envfile.ReadEnv(r.SampleEnvPath)
|
||||
if err != nil {
|
||||
return sampleEnv, errors.New(i18n.G("unable to discover .env.sample for %s", r.Name))
|
||||
return sampleEnv, errors.New(i18n.G(".env.sample for %s couldn't be read: %s", r.Name, err))
|
||||
}
|
||||
return sampleEnv, nil
|
||||
}
|
||||
|
||||
@ -32,6 +32,12 @@ func (r Recipe) Ensure(ctx EnsureContext) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// NOTE(d1): if we cannot parse the .env.sample then there is a
|
||||
// fundamental problem which requires solving right now
|
||||
if _, err := r.SampleEnv(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if ctx.Chaos {
|
||||
return nil
|
||||
}
|
||||
@ -403,15 +409,18 @@ func (r Recipe) GetRecipeVersions() (RecipeVersions, []string, error) {
|
||||
Branch: plumbing.ReferenceName(ref.Name()),
|
||||
}
|
||||
if err := worktree.Checkout(checkOutOpts); err != nil {
|
||||
log.Debug(i18n.G("failed to check out %s in %s", tag, r.Dir))
|
||||
return err
|
||||
log.Debug(i18n.G("failed to check out %s in %s: %s", tag, r.Dir, err))
|
||||
warnMsg = append(warnMsg, i18n.G("skipping tag %s: checkout failed: %s", tag, err))
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Debug(i18n.G("git checkout: %s in %s", ref.Name(), r.Dir))
|
||||
|
||||
config, err := r.GetComposeConfig(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
log.Debug(i18n.G("failed to get compose config for %s: %s", tag, err))
|
||||
warnMsg = append(warnMsg, i18n.G("skipping tag %s: invalid compose config: %s", tag, err))
|
||||
return nil
|
||||
}
|
||||
|
||||
versionMeta := make(map[string]ServiceMeta)
|
||||
@ -419,7 +428,9 @@ func (r Recipe) GetRecipeVersions() (RecipeVersions, []string, error) {
|
||||
|
||||
img, err := reference.ParseNormalizedNamed(service.Image)
|
||||
if err != nil {
|
||||
return err
|
||||
log.Debug(i18n.G("failed to parse image for %s in %s: %s", service.Name, tag, err))
|
||||
warnMsg = append(warnMsg, i18n.G("skipping tag %s: invalid image reference in service %s: %s", tag, service.Name, err))
|
||||
return nil
|
||||
}
|
||||
|
||||
path := reference.Path(img)
|
||||
@ -445,6 +456,7 @@ func (r Recipe) GetRecipeVersions() (RecipeVersions, []string, error) {
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Warn(i18n.G("GetRecipeVersions encountered error for %s: %s (collected %d versions)", r.Name, err, len(versions)))
|
||||
return versions, warnMsg, nil
|
||||
}
|
||||
|
||||
|
||||
@ -11,7 +11,6 @@ import (
|
||||
"coopcloud.tech/abra/pkg/formatter"
|
||||
"coopcloud.tech/abra/pkg/i18n"
|
||||
"coopcloud.tech/abra/pkg/logs"
|
||||
"github.com/charmbracelet/bubbles/viewport"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/docker/cli/cli/command/service/progress"
|
||||
containerTypes "github.com/docker/docker/api/types/container"
|
||||
@ -42,12 +41,6 @@ type ServiceMeta struct {
|
||||
ID string
|
||||
}
|
||||
|
||||
const (
|
||||
statusMode = iota
|
||||
logsMode = iota
|
||||
errorsMode = iota
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
appName string
|
||||
cl *dockerClient.Client
|
||||
@ -56,10 +49,6 @@ type Model struct {
|
||||
timeout time.Duration
|
||||
width int
|
||||
filters filters.Args
|
||||
mode int
|
||||
|
||||
logsViewport viewport.Model
|
||||
logsViewportReady bool
|
||||
|
||||
Streams *[]stream
|
||||
Logs *[]string
|
||||
@ -247,10 +236,7 @@ func deployTimeout(m Model) tea.Msg {
|
||||
}
|
||||
|
||||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
var (
|
||||
cmd tea.Cmd
|
||||
cmds []tea.Cmd
|
||||
)
|
||||
var cmds []tea.Cmd
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
@ -258,25 +244,11 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
case "ctrl+c", "q":
|
||||
m.Quit = true
|
||||
return m, tea.Quit
|
||||
case "s":
|
||||
m.mode = statusMode
|
||||
case "l":
|
||||
m.mode = logsMode
|
||||
case "e":
|
||||
m.mode = errorsMode
|
||||
}
|
||||
|
||||
case tea.WindowSizeMsg:
|
||||
m.width = msg.Width
|
||||
|
||||
if !m.logsViewportReady {
|
||||
m.logsViewport = viewport.New(msg.Width, 20)
|
||||
m.logsViewportReady = true
|
||||
} else {
|
||||
m.logsViewport.Width = msg.Width
|
||||
m.logsViewport.Height = 20
|
||||
}
|
||||
|
||||
case progressCompleteMsg:
|
||||
if msg.failed {
|
||||
m.Failed = true
|
||||
@ -284,9 +256,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
|
||||
m.count += 1
|
||||
|
||||
// if m.complete() {
|
||||
// return m, tea.Quit
|
||||
// }
|
||||
if m.complete() {
|
||||
return m, tea.Quit
|
||||
}
|
||||
|
||||
case timeoutMsg:
|
||||
m.TimedOut = true
|
||||
@ -346,46 +318,12 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
)
|
||||
}
|
||||
|
||||
m.logsViewport, cmd = m.logsViewport.Update(msg)
|
||||
cmds = append(cmds, cmd)
|
||||
|
||||
return m, tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m Model) View() string {
|
||||
body := strings.Builder{}
|
||||
|
||||
body.WriteString("menu: [s]tatus [l]ogs [e]rrors\n")
|
||||
|
||||
var res string
|
||||
switch {
|
||||
case m.mode == statusMode:
|
||||
res = statusView(m)
|
||||
case m.mode == logsMode:
|
||||
res = logsView(m)
|
||||
}
|
||||
|
||||
return body.String() + res
|
||||
|
||||
}
|
||||
|
||||
func logsView(m Model) string {
|
||||
body := strings.Builder{}
|
||||
m.logsViewport.SetContent(strings.Join(*m.Logs, "\n"))
|
||||
m.logsViewport.GotoBottom()
|
||||
body.WriteString(m.logsViewport.View())
|
||||
return body.String()
|
||||
}
|
||||
|
||||
func errorsView(m Model) string {
|
||||
body := strings.Builder{}
|
||||
body.WriteString("ERRORS COMING SOON")
|
||||
return body.String()
|
||||
}
|
||||
|
||||
func statusView(m Model) string {
|
||||
body := strings.Builder{}
|
||||
|
||||
for _, stream := range *m.Streams {
|
||||
split := strings.Split(stream.Name, "_")
|
||||
short := split[len(split)-1]
|
||||
|
||||
@ -201,6 +201,7 @@ func RunDeploy(
|
||||
appName string,
|
||||
serverName string,
|
||||
dontWait bool,
|
||||
noInput bool,
|
||||
filters filters.Args,
|
||||
) error {
|
||||
log.Info(i18n.G("initialising deployment"))
|
||||
@ -226,6 +227,7 @@ func RunDeploy(
|
||||
appName,
|
||||
serverName,
|
||||
dontWait,
|
||||
noInput,
|
||||
filters,
|
||||
)
|
||||
}
|
||||
@ -248,6 +250,7 @@ func deployCompose(
|
||||
appName string,
|
||||
serverName string,
|
||||
dontWait bool,
|
||||
noInput bool,
|
||||
filters filters.Args,
|
||||
) error {
|
||||
namespace := convert.NewNamespace(opts.Namespace)
|
||||
@ -311,6 +314,7 @@ func deployCompose(
|
||||
Services: serviceIDs,
|
||||
AppName: appName,
|
||||
ServerName: serverName,
|
||||
NoInput: noInput,
|
||||
Filters: filters,
|
||||
}
|
||||
|
||||
@ -561,6 +565,7 @@ func timestamp() string {
|
||||
type WaitOpts struct {
|
||||
AppName string
|
||||
Filters filters.Args
|
||||
NoInput bool
|
||||
NoLog bool
|
||||
Quiet bool
|
||||
ServerName string
|
||||
@ -570,7 +575,13 @@ type WaitOpts struct {
|
||||
func WaitOnServices(ctx context.Context, cl *dockerClient.Client, opts WaitOpts) error {
|
||||
timeout := time.Duration(WaitTimeout) * time.Second
|
||||
model := ui.DeployInitialModel(ctx, cl, opts.Services, opts.AppName, timeout, opts.Filters)
|
||||
tui := tea.NewProgram(model)
|
||||
|
||||
var tui *tea.Program
|
||||
if opts.NoInput {
|
||||
tui = tea.NewProgram(model, tea.WithoutRenderer(), tea.WithInput(nil))
|
||||
} else {
|
||||
tui = tea.NewProgram(model)
|
||||
}
|
||||
|
||||
if !opts.Quiet {
|
||||
log.Info(i18n.G("polling deployment status"))
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
ABRA_VERSION="0.11.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.11.0-beta"
|
||||
RC_VERSION="0.13.0-rc1-beta"
|
||||
RC_VERSION_URL="https://git.coopcloud.tech/api/v1/repos/toolshed/abra/releases/tags/$RC_VERSION"
|
||||
|
||||
for arg in "$@"; do
|
||||
@ -14,15 +14,15 @@ done
|
||||
|
||||
function show_banner {
|
||||
echo ""
|
||||
echo " ____ ____ _ _ "
|
||||
echo " / ___|___ ___ _ __ / ___| | ___ _ _ __| |"
|
||||
echo " | | / _ \ _____ / _ \| '_ \ | | | |/ _ \| | | |/ _' |"
|
||||
echo " | |__| (_) |_____| (_) | |_) | | |___| | (_) | |_| | (_| |"
|
||||
echo " \____\___/ \___/| .__/ \____|_|\___/ \__,_|\__,_|"
|
||||
echo " |_|"
|
||||
echo " ____ ____ _ _ "
|
||||
echo " / ___|___ ___ _ __ / ___| | ___ _ _ __| |"
|
||||
echo " | | / _ \ ___ / _ \| '_ \ | | | |/ _ \| | | |/ _' |"
|
||||
echo " | |__| (_) |___| (_) | |_) | | |___| | (_) | |_| | (_| |"
|
||||
echo " \____\___/ \___/| .__/ \____|_|\___/ \__,_|\__,_|"
|
||||
echo " |_|"
|
||||
echo ""
|
||||
echo ""
|
||||
echo " === Public interest infrastructure === "
|
||||
echo " === Public interest infrastructure === "
|
||||
echo ""
|
||||
echo ""
|
||||
}
|
||||
@ -89,7 +89,7 @@ function install_abra_release {
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "$(tput setaf 3)WARNING: $HOME/.local/bin/ is not in \$PATH! If you want to run abra by just typing "abra" you should add it to your \$PATH! To do that run this once and restart your terminal:$(tput sgr0)"
|
||||
p=$HOME/.local/bin
|
||||
com="echo PATH=\$PATH:$p"
|
||||
com='echo PATH="$PATH:'"$p"'"'
|
||||
if [[ $SHELL =~ "bash" ]]; then
|
||||
echo "$com >> $HOME/.bashrc"
|
||||
elif [[ $SHELL =~ "fizsh" ]]; then
|
||||
|
||||
@ -106,7 +106,7 @@ teardown(){
|
||||
|
||||
run $ABRA app check "$TEST_APP_DOMAIN" --chaos
|
||||
assert_failure
|
||||
assert_output --partial 'unable to discover .env.sample'
|
||||
assert_output --partial 'no such file or directory'
|
||||
}
|
||||
|
||||
@test "error if missing env var" {
|
||||
|
||||
@ -23,12 +23,24 @@ teardown(){
|
||||
_reset_recipe
|
||||
_undeploy_app
|
||||
_undeploy_app2 "gitea.$TEST_SERVER"
|
||||
_undeploy_app2 "zammad.$TEST_SERVER"
|
||||
|
||||
_reset_app
|
||||
_reset_tags
|
||||
|
||||
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
|
||||
if [[ -d "$ABRA_DIR/recipes/foo" ]]; then
|
||||
run rm -rf "$ABRA_DIR/recipes/foo"
|
||||
assert_not_exists "$ABRA_DIR/recipes/foo"
|
||||
fi
|
||||
|
||||
# NOTE(d1): give some extra space for the pure chaos that we are unleashing
|
||||
# on the CI machine with these deploy tests. the hope is to prevent
|
||||
# lock-ups and network failures which are common in flaky swarm
|
||||
# mode
|
||||
sleep 1
|
||||
}
|
||||
|
||||
@test "validate app argument" {
|
||||
@ -543,7 +555,7 @@ teardown(){
|
||||
|
||||
# bats test_tags=slow
|
||||
@test "ignore timeout when not present in env" {
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks --debug
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks
|
||||
assert_success
|
||||
refute_output --partial "timeout: set to"
|
||||
}
|
||||
@ -554,6 +566,7 @@ teardown(){
|
||||
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||
assert_success
|
||||
|
||||
# NOTE(d1}: --debug required
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --no-converge-checks --debug
|
||||
assert_success
|
||||
assert_output --partial "timeout: set to 120"
|
||||
@ -579,16 +592,111 @@ teardown(){
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
@test "manually created server without context bails gracefully" {
|
||||
run mkdir -p "$ABRA_DIR/servers/default2"
|
||||
@test "re-deploy updates existing env vars" {
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/servers/default2"
|
||||
|
||||
run cp "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" "$ABRA_DIR/servers/default2/$TEST_APP_DOMAIN_2.env"
|
||||
run docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' \
|
||||
$(docker ps -f name="$TEST_APP_DOMAIN_$TEST_SERVER" -q)
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/servers/default2/$TEST_APP_DOMAIN_2.env"
|
||||
assert_output --partial "WITH_COMMENT=foo"
|
||||
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN_2" --no-input --no-converge-checks
|
||||
assert_failure
|
||||
assert_output --partial "server missing context"
|
||||
run sed -i 's/WITH_COMMENT=foo/WITH_COMMENT=bar/g' \
|
||||
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||
assert_success
|
||||
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --force
|
||||
assert_success
|
||||
|
||||
run docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' \
|
||||
$(docker ps -f name="$TEST_APP_DOMAIN_$TEST_SERVER" -q)
|
||||
assert_success
|
||||
refute_output --partial "WITH_COMMENT=foo"
|
||||
assert_output --partial "WITH_COMMENT=bar"
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
@test "deploy with udp and tcp on same port" {
|
||||
run sed -i 's/TYPE=abra-test-recipe:.*/TYPE=git.coopcloud.tech\/p4u1\/abra-test-recipe:030e8a1cb1a0f17281847b3e55d829220ad32c50/g' \
|
||||
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||
assert_success
|
||||
|
||||
run bash -c "printf '\nCOMPOSE_FILE=\"\$COMPOSE_FILE:compose.udp-and-tcp.yml\"' >> $ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||
assert_success
|
||||
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input
|
||||
assert_success
|
||||
|
||||
run docker service inspect --format '{{ range .Endpoint.Ports }}{{ .Protocol }}={{ .PublishedPort }}{{ end }}' \
|
||||
"${TEST_APP_DOMAIN//./_}_app"
|
||||
assert_success
|
||||
assert_output --partial "tcp=1312"
|
||||
assert_output --partial "udp=1312"
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
@test "does not crash when docker image has no tag" {
|
||||
run sed -i 's/TYPE=abra-test-recipe:.*/TYPE=git.coopcloud.tech\/p4u1\/abra-test-recipe:b29422d5a344ea45df271443182f775ea82b4da8/g' \
|
||||
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||
assert_success
|
||||
|
||||
run bash -c "printf '\nCOMPOSE_FILE=\"\$COMPOSE_FILE:compose.no-image-tag.yml\"' >> $ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||
assert_success
|
||||
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input
|
||||
assert_success
|
||||
}
|
||||
|
||||
|
||||
@test "does not use old recipe version when recipe is broken" {
|
||||
run $ABRA app new zammad \
|
||||
--no-input \
|
||||
--server "$TEST_SERVER" \
|
||||
--domain "zammad.$TEST_SERVER" \
|
||||
--secrets
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/servers/$TEST_SERVER/zammad.$TEST_SERVER.env"
|
||||
|
||||
# NOTE(d1): --no-converge-checks because the zammad recipe is a beast and we
|
||||
# mostly only care about the correct version being used
|
||||
run $ABRA app deploy "zammad.$TEST_SERVER" \
|
||||
--no-input --no-converge-checks
|
||||
assert_success
|
||||
refute_output --partial "1.0.0+6.3.1-95"
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
@test "unable to deploy borked tag" {
|
||||
_remove_tags
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag \
|
||||
-a "2.4.8_1" -m "feat: completely borked tag"
|
||||
assert_success
|
||||
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" "2.4.8_1" \
|
||||
--no-input --no-converge-checks --debug
|
||||
assert_failure
|
||||
assert_output --partial "unable to parse"
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
@test "app deploy with borked sample env gives useful error" {
|
||||
run $ABRA recipe new foo --no-input
|
||||
assert_success
|
||||
|
||||
run $ABRA app new foo \
|
||||
--no-input \
|
||||
--server "$TEST_SERVER" \
|
||||
--domain "foo.$TEST_SERVER" \
|
||||
--chaos
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/servers/$TEST_SERVER/foo.$TEST_SERVER.env"
|
||||
|
||||
run bash -c "printf '\nEVIL-VAR=EVIL' >> $ABRA_DIR/recipes/foo/.env.sample"
|
||||
assert_success
|
||||
|
||||
run $ABRA app deploy "foo.$TEST_SERVER" \
|
||||
--no-input --no-converge-checks --chaos
|
||||
assert_failure
|
||||
assert_output --partial "unexpected character"
|
||||
}
|
||||
|
||||
@ -68,6 +68,13 @@ teardown(){
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "domain shown with https" {
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" \
|
||||
--no-input --no-converge-checks
|
||||
assert_success
|
||||
assert_output --partial "https://$TEST_DOMAIN"
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
@test "show changed config version on re-deploy" {
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" \
|
||||
|
||||
@ -67,6 +67,16 @@ teardown(){
|
||||
assert_output --partial "$TEST_SERVER"
|
||||
assert_output --partial "$TEST_APP_DOMAIN"
|
||||
assert_output --partial "deployed"
|
||||
assert_output --partial "latest"
|
||||
|
||||
_remove_tags
|
||||
|
||||
run $ABRA app ls --status
|
||||
assert_success
|
||||
assert_output --partial "$TEST_SERVER"
|
||||
assert_output --partial "$TEST_APP_DOMAIN"
|
||||
assert_output --partial "deployed"
|
||||
assert_output --partial "latest"
|
||||
}
|
||||
|
||||
@test "filter by server" {
|
||||
@ -151,7 +161,7 @@ teardown(){
|
||||
--no-input --no-converge-checks --chaos
|
||||
assert_success
|
||||
|
||||
run $ABRA app ls --status
|
||||
run $ABRA app ls --status --chaos
|
||||
assert_success
|
||||
assert_output --partial "+U"
|
||||
|
||||
@ -160,23 +170,6 @@ teardown(){
|
||||
assert_not_exists "$ABRA_DIR/servers/foo.com"
|
||||
}
|
||||
|
||||
@test "list with status skips unknown servers" {
|
||||
if [[ ! -d "$ABRA_DIR/servers/foo" ]]; then
|
||||
run mkdir -p "$ABRA_DIR/servers/foo"
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/servers/foo"
|
||||
|
||||
run cp "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env" \
|
||||
"$ABRA_DIR/servers/foo/$TEST_APP_DOMAIN.env"
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/servers/foo/$TEST_APP_DOMAIN.env"
|
||||
fi
|
||||
|
||||
run $ABRA app ls --status
|
||||
assert_success
|
||||
assert_output --partial "server missing context"
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
@test "list does not fail if missing .env" {
|
||||
_deploy_app
|
||||
@ -197,13 +190,16 @@ teardown(){
|
||||
|
||||
# bats test_tags=slow
|
||||
@test "list ignores borked tags" {
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag \
|
||||
-a "2.4.8_1" -m "feat: completely borked tag"
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" \
|
||||
--no-input --no-converge-checks
|
||||
assert_success
|
||||
|
||||
_deploy_app
|
||||
# NOTE(d1): always upgradable tag which is also borked
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag \
|
||||
-a "100.100.100_1_2_3" -m "feat: completely borked tag"
|
||||
assert_success
|
||||
|
||||
run $ABRA app ls --status --debug
|
||||
assert_success
|
||||
assert_output --partial "unable to parse 2.4.8_1"
|
||||
assert_output --partial "unable to parse"
|
||||
}
|
||||
|
||||
@ -63,6 +63,30 @@ teardown(){
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "ensure recipe is up-to-date" {
|
||||
run bash -c 'git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -l'
|
||||
assert_success
|
||||
assert_output --partial '0.3.5+1.21.0'
|
||||
|
||||
run bash -c 'git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -d 0.3.5+1.21.0'
|
||||
assert_success
|
||||
|
||||
run bash -c 'git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -l'
|
||||
assert_success
|
||||
refute_output --partial '0.3.5+1.21.0'
|
||||
|
||||
run $ABRA app new "$TEST_RECIPE" \
|
||||
--no-input \
|
||||
--server "$TEST_SERVER" \
|
||||
--domain "$TEST_APP_DOMAIN"
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||
|
||||
run grep -q "TYPE=$TEST_RECIPE:0.3.5+1.21.0" \
|
||||
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||
assert_success
|
||||
}
|
||||
|
||||
@test "create new app with version commit" {
|
||||
tagHash=$(_get_tag_hash "0.3.0+1.21.0")
|
||||
|
||||
|
||||
@ -13,7 +13,6 @@ _common_setup() {
|
||||
load "$PWD/tests/integration/helpers/docker"
|
||||
|
||||
export ABRA="$PWD/abra"
|
||||
export KADABRA="$PWD/kadabra"
|
||||
|
||||
export TEST_APP_NAME="$(basename "${BATS_TEST_FILENAME//./_}")"
|
||||
export TEST_APP_DOMAIN="$TEST_APP_NAME.$TEST_SERVER"
|
||||
@ -21,4 +20,20 @@ _common_setup() {
|
||||
export TEST_RECIPE="abra-test-recipe"
|
||||
|
||||
_ensure_swarm
|
||||
_ensure_ssh_agent
|
||||
}
|
||||
|
||||
|
||||
_ensure_ssh_agent() {
|
||||
if ! command -v ssh-agent >/dev/null 2>&1
|
||||
then
|
||||
echo "ssh-agent is missing, please install it"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export SSH_AUTH_SOCK="$HOME/.ssh/ssh_auth_sock"
|
||||
if [ ! -S ~/.ssh/ssh_auth_sock ]; then
|
||||
eval `ssh-agent`
|
||||
ln -sf "$SSH_AUTH_SOCK" ~/.ssh/ssh_auth_sock
|
||||
fi
|
||||
}
|
||||
|
||||
@ -17,7 +17,12 @@ _remove_tags(){
|
||||
|
||||
run bash -c 'git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag -l | wc -l'
|
||||
assert_success
|
||||
assert_output '0'
|
||||
# If this was done without the --regexp I get this error:
|
||||
# -- output differs --
|
||||
# expected : 0
|
||||
# actual : 0
|
||||
# --
|
||||
assert_output --regexp '[[:space:]]*0'
|
||||
}
|
||||
|
||||
_reset_tags() {
|
||||
|
||||
@ -5,11 +5,22 @@ _latest_release(){
|
||||
}
|
||||
|
||||
_fetch_recipe() {
|
||||
# clone first to a bare repo which will serve as origin-ssh
|
||||
# this enables simulating git push in recipe release
|
||||
if [[ ! -d "$ABRA_DIR/recipes/$TEST_RECIPE" ]]; then
|
||||
run mkdir -p "$ABRA_DIR/origin-recipes"
|
||||
assert_success
|
||||
|
||||
run git clone "https://git.coopcloud.tech/toolshed/$TEST_RECIPE" "$ABRA_DIR/origin-recipes/$TEST_RECIPE.git" --bare
|
||||
assert_success
|
||||
|
||||
run mkdir -p "$ABRA_DIR/recipes"
|
||||
assert_success
|
||||
|
||||
run git clone "https://git.coopcloud.tech/toolshed/$TEST_RECIPE" "$ABRA_DIR/recipes/$TEST_RECIPE"
|
||||
run git clone "$ABRA_DIR/origin-recipes/$TEST_RECIPE.git" "$ABRA_DIR/recipes/$TEST_RECIPE"
|
||||
assert_success
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" remote add origin-ssh "$ABRA_DIR/origin-recipes/$TEST_RECIPE.git"
|
||||
assert_success
|
||||
fi
|
||||
}
|
||||
@ -19,6 +30,10 @@ _reset_recipe(){
|
||||
assert_success
|
||||
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE"
|
||||
|
||||
run rm -rf "$ABRA_DIR/origin-recipes/$TEST_RECIPE.git"
|
||||
assert_success
|
||||
assert_not_exists "$ABRA_DIR/origin-recipes/$TEST_RECIPE.git"
|
||||
|
||||
_fetch_recipe
|
||||
}
|
||||
|
||||
|
||||
@ -1,18 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
setup_file(){
|
||||
setup_file() {
|
||||
load "$PWD/tests/integration/helpers/common"
|
||||
_common_setup
|
||||
_add_server
|
||||
_new_app
|
||||
}
|
||||
|
||||
teardown_file(){
|
||||
teardown_file() {
|
||||
_rm_server
|
||||
_reset_recipe
|
||||
}
|
||||
|
||||
setup(){
|
||||
setup() {
|
||||
load "$PWD/tests/integration/helpers/common"
|
||||
_common_setup
|
||||
_set_git_author
|
||||
@ -21,6 +21,14 @@ setup(){
|
||||
teardown() {
|
||||
_reset_recipe
|
||||
_reset_tags
|
||||
if [[ -d "$ABRA_DIR/recipes/foobar" ]]; then
|
||||
run rm -rf "$ABRA_DIR/recipes/foobar"
|
||||
assert_success
|
||||
fi
|
||||
if [[ -d "$ABRA_DIR/origin-recipes/foobar.git" ]]; then
|
||||
run rm -rf "$ABRA_DIR/origin-recipes/foobar.git"
|
||||
assert_success
|
||||
fi
|
||||
}
|
||||
|
||||
@test "validate recipe argument" {
|
||||
@ -32,10 +40,10 @@ teardown() {
|
||||
}
|
||||
|
||||
@test "release patch bump" {
|
||||
run $ABRA recipe upgrade "$TEST_RECIPE" --no-input --patch
|
||||
run $ABRA recipe upgrade "$TEST_RECIPE" --no-input --patch --commit
|
||||
assert_success
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" diff
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" show
|
||||
assert_success
|
||||
assert_output --partial 'image: nginx:1.21.6'
|
||||
|
||||
@ -45,17 +53,9 @@ teardown() {
|
||||
-a "0.3.0+1.21.0" -m "fake: 0.3.0+1.21.0"
|
||||
assert_success
|
||||
|
||||
run $ABRA recipe sync "$TEST_RECIPE" --no-input --patch
|
||||
assert_success
|
||||
assert_output --partial 'synced label'
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" diff
|
||||
assert_success
|
||||
assert_output --partial 'coop-cloud.${STACK_NAME}.version=0.3.1+1.21.6'
|
||||
|
||||
run $ABRA recipe release "$TEST_RECIPE" --no-input --patch
|
||||
assert_success
|
||||
assert_output --partial 'no -p/--publish passed, not publishing'
|
||||
assert_output --partial 'INFO new release published:'
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag --list
|
||||
assert_success
|
||||
@ -63,10 +63,10 @@ teardown() {
|
||||
}
|
||||
|
||||
@test "release minor bump" {
|
||||
run $ABRA recipe upgrade "$TEST_RECIPE" --no-input --minor
|
||||
run $ABRA recipe upgrade "$TEST_RECIPE" --no-input --minor --commit
|
||||
assert_success
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" diff
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" show
|
||||
assert_success
|
||||
assert_output --regexp 'image: nginx:1.2.*'
|
||||
|
||||
@ -76,58 +76,57 @@ teardown() {
|
||||
-a "0.3.0+1.21.0" -m "fake: 0.3.0+1.21.0"
|
||||
assert_success
|
||||
|
||||
run $ABRA recipe sync "$TEST_RECIPE" --no-input --minor
|
||||
assert_success
|
||||
assert_output --partial 'synced label'
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" diff
|
||||
assert_success
|
||||
assert_output --regexp 'coop-cloud\.\$\{STACK_NAME\}\.version=0\.4\.0\+1\.2.*'
|
||||
|
||||
run $ABRA recipe release "$TEST_RECIPE" --no-input --minor
|
||||
assert_success
|
||||
assert_output --partial 'no -p/--publish passed, not publishing'
|
||||
assert_output --partial 'INFO new release published:'
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag --list
|
||||
assert_success
|
||||
assert_output --regexp '0\.4\.0\+1\.2.*'
|
||||
}
|
||||
|
||||
@test "unknown files not committed" {
|
||||
run $ABRA recipe upgrade "$TEST_RECIPE" --no-input --patch
|
||||
@test "release with unstaged changes" {
|
||||
run bash -c 'echo "# unstaged changes" >> "$ABRA_DIR/recipes/$TEST_RECIPE/compose.yml"'
|
||||
assert_success
|
||||
|
||||
run bash -c 'echo "unstaged changes" >> "$ABRA_DIR/recipes/$TEST_RECIPE/foo"'
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
|
||||
run $ABRA recipe sync "$TEST_RECIPE" --no-input --patch
|
||||
assert_success
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" diff --quiet
|
||||
assert_failure
|
||||
|
||||
run $ABRA recipe release "$TEST_RECIPE" --no-input --patch
|
||||
assert_success
|
||||
assert_output --partial 'no -p/--publish passed, not publishing'
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" rm foo
|
||||
assert_failure
|
||||
assert_output --partial "fatal: pathspec 'foo' did not match any files"
|
||||
assert_output --partial "working directory not clean"
|
||||
}
|
||||
|
||||
@test "release with staged changes" {
|
||||
run bash -c 'echo "# staged changes" >> "$ABRA_DIR/recipes/$TEST_RECIPE/compose.yml"'
|
||||
assert_success
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" add compose.yml
|
||||
assert_success
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" diff --quiet --cached
|
||||
assert_failure
|
||||
|
||||
run $ABRA recipe release "$TEST_RECIPE" --no-input --patch
|
||||
assert_failure
|
||||
assert_output --partial "working directory not clean"
|
||||
}
|
||||
|
||||
@test "release with next release note" {
|
||||
_mkfile "$ABRA_DIR/recipes/$TEST_RECIPE/release/next" "those are some release notes for the next release"
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" checkout main
|
||||
assert_success
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" add release/next
|
||||
assert_success
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" commit -m "added some release notes"
|
||||
assert_success
|
||||
|
||||
run $ABRA recipe sync "$TEST_RECIPE" --no-input --patch
|
||||
run $ABRA recipe release "$TEST_RECIPE" --no-input --minor
|
||||
assert_success
|
||||
|
||||
run $ABRA recipe release "$TEST_RECIPE" --no-input --minor
|
||||
assert_success
|
||||
assert_output --partial 'no -p/--publish passed, not publishing'
|
||||
assert_output --partial 'new release published:'
|
||||
|
||||
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/release/next"
|
||||
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/release/0.4.0+1.21.0"
|
||||
@ -146,14 +145,75 @@ teardown() {
|
||||
assert_success
|
||||
assert_output --regexp 'nginx:1.29.1'
|
||||
|
||||
run sed -i "s/0.2.0+1.21.0/0.2.0+1.29.1/g" "$ABRA_DIR/recipes/$TEST_RECIPE/compose.yml"
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" commit -am "updated nginx"
|
||||
assert_success
|
||||
|
||||
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
|
||||
run $ABRA recipe release "$TEST_RECIPE" --no-input "0.2.0+1.29.1"
|
||||
assert_failure
|
||||
assert_output --partial '0.2.0+... conflicts with a previous release: 0.2.0+1.21.0'
|
||||
}
|
||||
|
||||
@test "error if recipe release --no-input and no initial version" {
|
||||
_remove_tags
|
||||
|
||||
run $ABRA recipe release "$TEST_RECIPE" --no-input --patch
|
||||
assert_failure
|
||||
assert_output --partial 'unable to continue'
|
||||
assert_output --partial 'initial version'
|
||||
}
|
||||
|
||||
@test "recipe release without input fails with prompt" {
|
||||
run $ABRA recipe new foobar
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/recipes/foobar"
|
||||
|
||||
run $ABRA recipe release foobar --no-input --patch
|
||||
assert_failure
|
||||
assert_output --partial "input required for initial version"
|
||||
}
|
||||
|
||||
@test "release new recipe: fail without input" {
|
||||
run $ABRA recipe new foobar
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/recipes/foobar"
|
||||
|
||||
run bash -c "$ABRA recipe release foobar --no-input"
|
||||
assert_failure
|
||||
assert_output --partial 'unable to continue, input required for initial version'
|
||||
}
|
||||
|
||||
# note: piping 0.1.0 from stdin is not testable right now because release notes also wants input
|
||||
# survey lib used for prompts breaks multi-line stdin for multi-prompt
|
||||
@test "release new recipe: development release" {
|
||||
run $ABRA recipe new foobar
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/recipes/foobar"
|
||||
|
||||
# fake origin
|
||||
git clone "$ABRA_DIR/recipes/foobar" "$ABRA_DIR/origin-recipes/foobar.git" --bare
|
||||
assert_success
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/foobar" remote add origin-ssh "$ABRA_DIR/origin-recipes/foobar.git"
|
||||
assert_success
|
||||
|
||||
run bash -c "$ABRA recipe release foobar 0.1.0+1.2.0 --no-input"
|
||||
assert_success
|
||||
assert_output --regexp 'coop-cloud\.\$\{STACK_NAME\}\.version=0\.1\.0\+1\.2.*'
|
||||
}
|
||||
|
||||
@test "release newly created recipe with no version label" {
|
||||
run $ABRA recipe new foobar
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/recipes/foobar"
|
||||
|
||||
run sed -i 's/- "coop-cloud.${STACK_NAME}.version="/#- "coop-cloud.${STACK_NAME}.version="/g' \
|
||||
"$ABRA_DIR/recipes/foobar/compose.yml"
|
||||
assert_success
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/foobar" commit -am "updated nginx"
|
||||
assert_success
|
||||
|
||||
run bash -c "echo 0.1.0 | $ABRA recipe release foobar --patch"
|
||||
assert_failure
|
||||
assert_output --partial "automagic insertion not supported yet"
|
||||
}
|
||||
|
||||
@ -1,128 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
setup_file(){
|
||||
load "$PWD/tests/integration/helpers/common"
|
||||
_common_setup
|
||||
_add_server
|
||||
_new_app
|
||||
}
|
||||
|
||||
teardown_file(){
|
||||
_rm_server
|
||||
}
|
||||
|
||||
setup(){
|
||||
load "$PWD/tests/integration/helpers/common"
|
||||
_common_setup
|
||||
}
|
||||
|
||||
teardown(){
|
||||
_reset_recipe
|
||||
_reset_tags
|
||||
}
|
||||
|
||||
@test "validate recipe argument" {
|
||||
run $ABRA recipe sync --no-input
|
||||
assert_failure
|
||||
|
||||
run $ABRA recipe sync DOESNTEXIST --no-input
|
||||
assert_failure
|
||||
}
|
||||
|
||||
@test "allow unstaged changes" {
|
||||
run echo "unstaged changes" >> "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
assert_success
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
|
||||
assert_success
|
||||
assert_output --partial 'foo'
|
||||
|
||||
run $ABRA recipe sync "$TEST_RECIPE" --no-input --patch
|
||||
assert_success
|
||||
|
||||
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
assert_equal "$(_git_status)" "M compose.yml ?? foo"
|
||||
|
||||
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
assert_success
|
||||
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
}
|
||||
|
||||
@test "detect unstaged label changes" {
|
||||
run $ABRA recipe fetch "$TEST_RECIPE"
|
||||
assert_success
|
||||
|
||||
run $ABRA recipe sync "$TEST_RECIPE" --patch
|
||||
assert_success
|
||||
|
||||
run $ABRA recipe sync "$TEST_RECIPE" --patch
|
||||
assert_success
|
||||
assert_output --partial 'is already set, nothing to do?'
|
||||
}
|
||||
|
||||
@test "sync patch label bump" {
|
||||
run $ABRA recipe upgrade "$TEST_RECIPE" --no-input --patch
|
||||
assert_success
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" diff
|
||||
assert_success
|
||||
assert_output --partial 'image: nginx:1.21.6'
|
||||
|
||||
# NOTE(d1): ensure the latest tag is the one we expect
|
||||
_remove_tags
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag \
|
||||
-a "0.3.0+1.21.0" -m "fake: 0.3.0+1.21.0"
|
||||
assert_success
|
||||
|
||||
run $ABRA recipe sync "$TEST_RECIPE" --no-input --patch
|
||||
assert_success
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" diff
|
||||
assert_success
|
||||
assert_output --regexp 'coop-cloud\.\$\{STACK_NAME\}\.version=0\.3\.1\+1\.2.*'
|
||||
}
|
||||
|
||||
@test "sync minor label bump" {
|
||||
run $ABRA recipe upgrade "$TEST_RECIPE" --no-input --minor
|
||||
assert_success
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" diff
|
||||
assert_success
|
||||
assert_output --regexp 'image: nginx:1.2.*'
|
||||
|
||||
# NOTE(d1): ensure the latest tag is the one we expect
|
||||
_remove_tags
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag \
|
||||
-a "0.3.0+1.21.0" -m "fake: 0.3.0+1.21.0"
|
||||
assert_success
|
||||
|
||||
run $ABRA recipe sync "$TEST_RECIPE" --no-input --minor
|
||||
assert_success
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" diff
|
||||
assert_success
|
||||
assert_output --regexp 'coop-cloud\.\$\{STACK_NAME\}\.version=0\.4\.0\+1\.2.*'
|
||||
}
|
||||
|
||||
@test "error if --no-input and no initial version" {
|
||||
_remove_tags
|
||||
|
||||
run $ABRA recipe sync "$TEST_RECIPE" --no-input --patch
|
||||
assert_failure
|
||||
assert_output --partial 'unable to continue'
|
||||
assert_output --partial 'initial version'
|
||||
}
|
||||
|
||||
@test "output label sync only once" {
|
||||
run $ABRA recipe upgrade "$TEST_RECIPE" --no-input --minor
|
||||
assert_success
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" diff
|
||||
assert_success
|
||||
assert_output --regexp 'image: nginx:1.2.*'
|
||||
|
||||
run $ABRA recipe sync "$TEST_RECIPE" --no-input --minor
|
||||
assert_success
|
||||
assert_line --index 0 --partial 'synced label'
|
||||
refute_line --index 1 --partial 'synced label'
|
||||
}
|
||||
@ -106,3 +106,37 @@ teardown(){
|
||||
assert_success
|
||||
assert_output --regexp 'image: nginx:1.2.*'
|
||||
}
|
||||
|
||||
@test "upgrade and commit" {
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" rev-list --count HEAD
|
||||
assert_success
|
||||
expected_count="$((output + 1))"
|
||||
|
||||
run $ABRA recipe upgrade "$TEST_RECIPE" --no-input --minor --commit
|
||||
assert_success
|
||||
assert_output --partial 'committed changes as'
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" diff --quiet
|
||||
assert_success
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" rev-list --count HEAD
|
||||
assert_success
|
||||
assert_output "$expected_count"
|
||||
}
|
||||
|
||||
@test "upgrade nothing, skip commit" {
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" rev-list --count HEAD
|
||||
assert_success
|
||||
expected_count="$output"
|
||||
|
||||
run $ABRA recipe upgrade "$TEST_RECIPE" --no-input --commit
|
||||
assert_success
|
||||
assert_output --partial "no changes, skip creating commit"
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" diff --quiet
|
||||
assert_success
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" rev-list --count HEAD
|
||||
assert_success
|
||||
assert_output "$expected_count"
|
||||
}
|
||||
|
||||
21
vendor/github.com/charmbracelet/bubbles/LICENSE
generated
vendored
21
vendor/github.com/charmbracelet/bubbles/LICENSE
generated
vendored
@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020-2023 Charmbracelet, Inc
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
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.
|
||||
140
vendor/github.com/charmbracelet/bubbles/key/key.go
generated
vendored
140
vendor/github.com/charmbracelet/bubbles/key/key.go
generated
vendored
@ -1,140 +0,0 @@
|
||||
// Package key provides some types and functions for generating user-definable
|
||||
// keymappings useful in Bubble Tea components. There are a few different ways
|
||||
// you can define a keymapping with this package. Here's one example:
|
||||
//
|
||||
// type KeyMap struct {
|
||||
// Up key.Binding
|
||||
// Down key.Binding
|
||||
// }
|
||||
//
|
||||
// var DefaultKeyMap = KeyMap{
|
||||
// Up: key.NewBinding(
|
||||
// key.WithKeys("k", "up"), // actual keybindings
|
||||
// key.WithHelp("↑/k", "move up"), // corresponding help text
|
||||
// ),
|
||||
// Down: key.NewBinding(
|
||||
// key.WithKeys("j", "down"),
|
||||
// key.WithHelp("↓/j", "move down"),
|
||||
// ),
|
||||
// }
|
||||
//
|
||||
// func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
// switch msg := msg.(type) {
|
||||
// case tea.KeyMsg:
|
||||
// switch {
|
||||
// case key.Matches(msg, DefaultKeyMap.Up):
|
||||
// // The user pressed up
|
||||
// case key.Matches(msg, DefaultKeyMap.Down):
|
||||
// // The user pressed down
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // ...
|
||||
// }
|
||||
//
|
||||
// The help information, which is not used in the example above, can be used
|
||||
// to render help text for keystrokes in your views.
|
||||
package key
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Binding describes a set of keybindings and, optionally, their associated
|
||||
// help text.
|
||||
type Binding struct {
|
||||
keys []string
|
||||
help Help
|
||||
disabled bool
|
||||
}
|
||||
|
||||
// BindingOpt is an initialization option for a keybinding. It's used as an
|
||||
// argument to NewBinding.
|
||||
type BindingOpt func(*Binding)
|
||||
|
||||
// NewBinding returns a new keybinding from a set of BindingOpt options.
|
||||
func NewBinding(opts ...BindingOpt) Binding {
|
||||
b := &Binding{}
|
||||
for _, opt := range opts {
|
||||
opt(b)
|
||||
}
|
||||
return *b
|
||||
}
|
||||
|
||||
// WithKeys initializes a keybinding with the given keystrokes.
|
||||
func WithKeys(keys ...string) BindingOpt {
|
||||
return func(b *Binding) {
|
||||
b.keys = keys
|
||||
}
|
||||
}
|
||||
|
||||
// WithHelp initializes a keybinding with the given help text.
|
||||
func WithHelp(key, desc string) BindingOpt {
|
||||
return func(b *Binding) {
|
||||
b.help = Help{Key: key, Desc: desc}
|
||||
}
|
||||
}
|
||||
|
||||
// WithDisabled initializes a disabled keybinding.
|
||||
func WithDisabled() BindingOpt {
|
||||
return func(b *Binding) {
|
||||
b.disabled = true
|
||||
}
|
||||
}
|
||||
|
||||
// SetKeys sets the keys for the keybinding.
|
||||
func (b *Binding) SetKeys(keys ...string) {
|
||||
b.keys = keys
|
||||
}
|
||||
|
||||
// Keys returns the keys for the keybinding.
|
||||
func (b Binding) Keys() []string {
|
||||
return b.keys
|
||||
}
|
||||
|
||||
// SetHelp sets the help text for the keybinding.
|
||||
func (b *Binding) SetHelp(key, desc string) {
|
||||
b.help = Help{Key: key, Desc: desc}
|
||||
}
|
||||
|
||||
// Help returns the Help information for the keybinding.
|
||||
func (b Binding) Help() Help {
|
||||
return b.help
|
||||
}
|
||||
|
||||
// Enabled returns whether or not the keybinding is enabled. Disabled
|
||||
// keybindings won't be activated and won't show up in help. Keybindings are
|
||||
// enabled by default.
|
||||
func (b Binding) Enabled() bool {
|
||||
return !b.disabled && b.keys != nil
|
||||
}
|
||||
|
||||
// SetEnabled enables or disables the keybinding.
|
||||
func (b *Binding) SetEnabled(v bool) {
|
||||
b.disabled = !v
|
||||
}
|
||||
|
||||
// Unbind removes the keys and help from this binding, effectively nullifying
|
||||
// it. This is a step beyond disabling it, since applications can enable
|
||||
// or disable key bindings based on application state.
|
||||
func (b *Binding) Unbind() {
|
||||
b.keys = nil
|
||||
b.help = Help{}
|
||||
}
|
||||
|
||||
// Help is help information for a given keybinding.
|
||||
type Help struct {
|
||||
Key string
|
||||
Desc string
|
||||
}
|
||||
|
||||
// Matches checks if the given key matches the given bindings.
|
||||
func Matches[Key fmt.Stringer](k Key, b ...Binding) bool {
|
||||
keys := k.String()
|
||||
for _, binding := range b {
|
||||
for _, v := range binding.keys {
|
||||
if keys == v && binding.Enabled() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
60
vendor/github.com/charmbracelet/bubbles/viewport/keymap.go
generated
vendored
60
vendor/github.com/charmbracelet/bubbles/viewport/keymap.go
generated
vendored
@ -1,60 +0,0 @@
|
||||
// Package viewport provides a component for rendering a viewport in a Bubble
|
||||
// Tea.
|
||||
package viewport
|
||||
|
||||
import "github.com/charmbracelet/bubbles/key"
|
||||
|
||||
const spacebar = " "
|
||||
|
||||
// KeyMap defines the keybindings for the viewport. Note that you don't
|
||||
// necessary need to use keybindings at all; the viewport can be controlled
|
||||
// programmatically with methods like Model.LineDown(1). See the GoDocs for
|
||||
// details.
|
||||
type KeyMap struct {
|
||||
PageDown key.Binding
|
||||
PageUp key.Binding
|
||||
HalfPageUp key.Binding
|
||||
HalfPageDown key.Binding
|
||||
Down key.Binding
|
||||
Up key.Binding
|
||||
Left key.Binding
|
||||
Right key.Binding
|
||||
}
|
||||
|
||||
// DefaultKeyMap returns a set of pager-like default keybindings.
|
||||
func DefaultKeyMap() KeyMap {
|
||||
return KeyMap{
|
||||
PageDown: key.NewBinding(
|
||||
key.WithKeys("pgdown", spacebar, "f"),
|
||||
key.WithHelp("f/pgdn", "page down"),
|
||||
),
|
||||
PageUp: key.NewBinding(
|
||||
key.WithKeys("pgup", "b"),
|
||||
key.WithHelp("b/pgup", "page up"),
|
||||
),
|
||||
HalfPageUp: key.NewBinding(
|
||||
key.WithKeys("u", "ctrl+u"),
|
||||
key.WithHelp("u", "½ page up"),
|
||||
),
|
||||
HalfPageDown: key.NewBinding(
|
||||
key.WithKeys("d", "ctrl+d"),
|
||||
key.WithHelp("d", "½ page down"),
|
||||
),
|
||||
Up: key.NewBinding(
|
||||
key.WithKeys("up", "k"),
|
||||
key.WithHelp("↑/k", "up"),
|
||||
),
|
||||
Down: key.NewBinding(
|
||||
key.WithKeys("down", "j"),
|
||||
key.WithHelp("↓/j", "down"),
|
||||
),
|
||||
Left: key.NewBinding(
|
||||
key.WithKeys("left", "h"),
|
||||
key.WithHelp("←/h", "move left"),
|
||||
),
|
||||
Right: key.NewBinding(
|
||||
key.WithKeys("right", "l"),
|
||||
key.WithHelp("→/l", "move right"),
|
||||
),
|
||||
}
|
||||
}
|
||||
544
vendor/github.com/charmbracelet/bubbles/viewport/viewport.go
generated
vendored
544
vendor/github.com/charmbracelet/bubbles/viewport/viewport.go
generated
vendored
@ -1,544 +0,0 @@
|
||||
package viewport
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/bubbles/key"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
)
|
||||
|
||||
// New returns a new model with the given width and height as well as default
|
||||
// key mappings.
|
||||
func New(width, height int) (m Model) {
|
||||
m.Width = width
|
||||
m.Height = height
|
||||
m.setInitialValues()
|
||||
return m
|
||||
}
|
||||
|
||||
// Model is the Bubble Tea model for this viewport element.
|
||||
type Model struct {
|
||||
Width int
|
||||
Height int
|
||||
KeyMap KeyMap
|
||||
|
||||
// Whether or not to respond to the mouse. The mouse must be enabled in
|
||||
// Bubble Tea for this to work. For details, see the Bubble Tea docs.
|
||||
MouseWheelEnabled bool
|
||||
|
||||
// The number of lines the mouse wheel will scroll. By default, this is 3.
|
||||
MouseWheelDelta int
|
||||
|
||||
// YOffset is the vertical scroll position.
|
||||
YOffset int
|
||||
|
||||
// xOffset is the horizontal scroll position.
|
||||
xOffset int
|
||||
|
||||
// horizontalStep is the number of columns we move left or right during a
|
||||
// default horizontal scroll.
|
||||
horizontalStep int
|
||||
|
||||
// YPosition is the position of the viewport in relation to the terminal
|
||||
// window. It's used in high performance rendering only.
|
||||
YPosition int
|
||||
|
||||
// Style applies a lipgloss style to the viewport. Realistically, it's most
|
||||
// useful for setting borders, margins and padding.
|
||||
Style lipgloss.Style
|
||||
|
||||
// HighPerformanceRendering bypasses the normal Bubble Tea renderer to
|
||||
// provide higher performance rendering. Most of the time the normal Bubble
|
||||
// Tea rendering methods will suffice, but if you're passing content with
|
||||
// a lot of ANSI escape codes you may see improved rendering in certain
|
||||
// terminals with this enabled.
|
||||
//
|
||||
// This should only be used in program occupying the entire terminal,
|
||||
// which is usually via the alternate screen buffer.
|
||||
//
|
||||
// Deprecated: high performance rendering is now deprecated in Bubble Tea.
|
||||
HighPerformanceRendering bool
|
||||
|
||||
initialized bool
|
||||
lines []string
|
||||
longestLineWidth int
|
||||
}
|
||||
|
||||
func (m *Model) setInitialValues() {
|
||||
m.KeyMap = DefaultKeyMap()
|
||||
m.MouseWheelEnabled = true
|
||||
m.MouseWheelDelta = 3
|
||||
m.initialized = true
|
||||
}
|
||||
|
||||
// Init exists to satisfy the tea.Model interface for composability purposes.
|
||||
func (m Model) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
// AtTop returns whether or not the viewport is at the very top position.
|
||||
func (m Model) AtTop() bool {
|
||||
return m.YOffset <= 0
|
||||
}
|
||||
|
||||
// AtBottom returns whether or not the viewport is at or past the very bottom
|
||||
// position.
|
||||
func (m Model) AtBottom() bool {
|
||||
return m.YOffset >= m.maxYOffset()
|
||||
}
|
||||
|
||||
// PastBottom returns whether or not the viewport is scrolled beyond the last
|
||||
// line. This can happen when adjusting the viewport height.
|
||||
func (m Model) PastBottom() bool {
|
||||
return m.YOffset > m.maxYOffset()
|
||||
}
|
||||
|
||||
// ScrollPercent returns the amount scrolled as a float between 0 and 1.
|
||||
func (m Model) ScrollPercent() float64 {
|
||||
if m.Height >= len(m.lines) {
|
||||
return 1.0
|
||||
}
|
||||
y := float64(m.YOffset)
|
||||
h := float64(m.Height)
|
||||
t := float64(len(m.lines))
|
||||
v := y / (t - h)
|
||||
return math.Max(0.0, math.Min(1.0, v))
|
||||
}
|
||||
|
||||
// HorizontalScrollPercent returns the amount horizontally scrolled as a float
|
||||
// between 0 and 1.
|
||||
func (m Model) HorizontalScrollPercent() float64 {
|
||||
if m.xOffset >= m.longestLineWidth-m.Width {
|
||||
return 1.0
|
||||
}
|
||||
y := float64(m.xOffset)
|
||||
h := float64(m.Width)
|
||||
t := float64(m.longestLineWidth)
|
||||
v := y / (t - h)
|
||||
return math.Max(0.0, math.Min(1.0, v))
|
||||
}
|
||||
|
||||
// SetContent set the pager's text content.
|
||||
func (m *Model) SetContent(s string) {
|
||||
s = strings.ReplaceAll(s, "\r\n", "\n") // normalize line endings
|
||||
m.lines = strings.Split(s, "\n")
|
||||
m.longestLineWidth = findLongestLineWidth(m.lines)
|
||||
|
||||
if m.YOffset > len(m.lines)-1 {
|
||||
m.GotoBottom()
|
||||
}
|
||||
}
|
||||
|
||||
// maxYOffset returns the maximum possible value of the y-offset based on the
|
||||
// viewport's content and set height.
|
||||
func (m Model) maxYOffset() int {
|
||||
return max(0, len(m.lines)-m.Height+m.Style.GetVerticalFrameSize())
|
||||
}
|
||||
|
||||
// visibleLines returns the lines that should currently be visible in the
|
||||
// viewport.
|
||||
func (m Model) visibleLines() (lines []string) {
|
||||
h := m.Height - m.Style.GetVerticalFrameSize()
|
||||
w := m.Width - m.Style.GetHorizontalFrameSize()
|
||||
|
||||
if len(m.lines) > 0 {
|
||||
top := max(0, m.YOffset)
|
||||
bottom := clamp(m.YOffset+h, top, len(m.lines))
|
||||
lines = m.lines[top:bottom]
|
||||
}
|
||||
|
||||
if (m.xOffset == 0 && m.longestLineWidth <= w) || w == 0 {
|
||||
return lines
|
||||
}
|
||||
|
||||
cutLines := make([]string, len(lines))
|
||||
for i := range lines {
|
||||
cutLines[i] = ansi.Cut(lines[i], m.xOffset, m.xOffset+w)
|
||||
}
|
||||
return cutLines
|
||||
}
|
||||
|
||||
// scrollArea returns the scrollable boundaries for high performance rendering.
|
||||
//
|
||||
// Deprecated: high performance rendering is deprecated in Bubble Tea.
|
||||
func (m Model) scrollArea() (top, bottom int) {
|
||||
top = max(0, m.YPosition)
|
||||
bottom = max(top, top+m.Height)
|
||||
if top > 0 && bottom > top {
|
||||
bottom--
|
||||
}
|
||||
return top, bottom
|
||||
}
|
||||
|
||||
// SetYOffset sets the Y offset.
|
||||
func (m *Model) SetYOffset(n int) {
|
||||
m.YOffset = clamp(n, 0, m.maxYOffset())
|
||||
}
|
||||
|
||||
// ViewDown moves the view down by the number of lines in the viewport.
|
||||
// Basically, "page down".
|
||||
//
|
||||
// Deprecated: use [Model.PageDown] instead.
|
||||
func (m *Model) ViewDown() []string {
|
||||
return m.PageDown()
|
||||
}
|
||||
|
||||
// PageDown moves the view down by the number of lines in the viewport.
|
||||
func (m *Model) PageDown() []string {
|
||||
if m.AtBottom() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return m.ScrollDown(m.Height)
|
||||
}
|
||||
|
||||
// ViewUp moves the view up by one height of the viewport.
|
||||
// Basically, "page up".
|
||||
//
|
||||
// Deprecated: use [Model.PageUp] instead.
|
||||
func (m *Model) ViewUp() []string {
|
||||
return m.PageUp()
|
||||
}
|
||||
|
||||
// PageUp moves the view up by one height of the viewport.
|
||||
func (m *Model) PageUp() []string {
|
||||
if m.AtTop() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return m.ScrollUp(m.Height)
|
||||
}
|
||||
|
||||
// HalfViewDown moves the view down by half the height of the viewport.
|
||||
//
|
||||
// Deprecated: use [Model.HalfPageDown] instead.
|
||||
func (m *Model) HalfViewDown() (lines []string) {
|
||||
return m.HalfPageDown()
|
||||
}
|
||||
|
||||
// HalfPageDown moves the view down by half the height of the viewport.
|
||||
func (m *Model) HalfPageDown() (lines []string) {
|
||||
if m.AtBottom() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return m.ScrollDown(m.Height / 2) //nolint:mnd
|
||||
}
|
||||
|
||||
// HalfViewUp moves the view up by half the height of the viewport.
|
||||
//
|
||||
// Deprecated: use [Model.HalfPageUp] instead.
|
||||
func (m *Model) HalfViewUp() (lines []string) {
|
||||
return m.HalfPageUp()
|
||||
}
|
||||
|
||||
// HalfPageUp moves the view up by half the height of the viewport.
|
||||
func (m *Model) HalfPageUp() (lines []string) {
|
||||
if m.AtTop() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return m.ScrollUp(m.Height / 2) //nolint:mnd
|
||||
}
|
||||
|
||||
// LineDown moves the view down by the given number of lines.
|
||||
//
|
||||
// Deprecated: use [Model.ScrollDown] instead.
|
||||
func (m *Model) LineDown(n int) (lines []string) {
|
||||
return m.ScrollDown(n)
|
||||
}
|
||||
|
||||
// ScrollDown moves the view down by the given number of lines.
|
||||
func (m *Model) ScrollDown(n int) (lines []string) {
|
||||
if m.AtBottom() || n == 0 || len(m.lines) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Make sure the number of lines by which we're going to scroll isn't
|
||||
// greater than the number of lines we actually have left before we reach
|
||||
// the bottom.
|
||||
m.SetYOffset(m.YOffset + n)
|
||||
|
||||
// Gather lines to send off for performance scrolling.
|
||||
//
|
||||
// XXX: high performance rendering is deprecated in Bubble Tea.
|
||||
bottom := clamp(m.YOffset+m.Height, 0, len(m.lines))
|
||||
top := clamp(m.YOffset+m.Height-n, 0, bottom)
|
||||
return m.lines[top:bottom]
|
||||
}
|
||||
|
||||
// LineUp moves the view down by the given number of lines. Returns the new
|
||||
// lines to show.
|
||||
//
|
||||
// Deprecated: use [Model.ScrollUp] instead.
|
||||
func (m *Model) LineUp(n int) (lines []string) {
|
||||
return m.ScrollUp(n)
|
||||
}
|
||||
|
||||
// ScrollUp moves the view down by the given number of lines. Returns the new
|
||||
// lines to show.
|
||||
func (m *Model) ScrollUp(n int) (lines []string) {
|
||||
if m.AtTop() || n == 0 || len(m.lines) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Make sure the number of lines by which we're going to scroll isn't
|
||||
// greater than the number of lines we are from the top.
|
||||
m.SetYOffset(m.YOffset - n)
|
||||
|
||||
// Gather lines to send off for performance scrolling.
|
||||
//
|
||||
// XXX: high performance rendering is deprecated in Bubble Tea.
|
||||
top := max(0, m.YOffset)
|
||||
bottom := clamp(m.YOffset+n, 0, m.maxYOffset())
|
||||
return m.lines[top:bottom]
|
||||
}
|
||||
|
||||
// SetHorizontalStep sets the default amount of columns to scroll left or right
|
||||
// with the default viewport key map.
|
||||
//
|
||||
// If set to 0 or less, horizontal scrolling is disabled.
|
||||
//
|
||||
// On v1, horizontal scrolling is disabled by default.
|
||||
func (m *Model) SetHorizontalStep(n int) {
|
||||
m.horizontalStep = max(n, 0)
|
||||
}
|
||||
|
||||
// SetXOffset sets the X offset.
|
||||
func (m *Model) SetXOffset(n int) {
|
||||
m.xOffset = clamp(n, 0, m.longestLineWidth-m.Width)
|
||||
}
|
||||
|
||||
// ScrollLeft moves the viewport to the left by the given number of columns.
|
||||
func (m *Model) ScrollLeft(n int) {
|
||||
m.SetXOffset(m.xOffset - n)
|
||||
}
|
||||
|
||||
// ScrollRight moves viewport to the right by the given number of columns.
|
||||
func (m *Model) ScrollRight(n int) {
|
||||
m.SetXOffset(m.xOffset + n)
|
||||
}
|
||||
|
||||
// TotalLineCount returns the total number of lines (both hidden and visible) within the viewport.
|
||||
func (m Model) TotalLineCount() int {
|
||||
return len(m.lines)
|
||||
}
|
||||
|
||||
// VisibleLineCount returns the number of the visible lines within the viewport.
|
||||
func (m Model) VisibleLineCount() int {
|
||||
return len(m.visibleLines())
|
||||
}
|
||||
|
||||
// GotoTop sets the viewport to the top position.
|
||||
func (m *Model) GotoTop() (lines []string) {
|
||||
if m.AtTop() {
|
||||
return nil
|
||||
}
|
||||
|
||||
m.SetYOffset(0)
|
||||
return m.visibleLines()
|
||||
}
|
||||
|
||||
// GotoBottom sets the viewport to the bottom position.
|
||||
func (m *Model) GotoBottom() (lines []string) {
|
||||
m.SetYOffset(m.maxYOffset())
|
||||
return m.visibleLines()
|
||||
}
|
||||
|
||||
// Sync tells the renderer where the viewport will be located and requests
|
||||
// a render of the current state of the viewport. It should be called for the
|
||||
// first render and after a window resize.
|
||||
//
|
||||
// For high performance rendering only.
|
||||
//
|
||||
// Deprecated: high performance rendering is deprecated in Bubble Tea.
|
||||
func Sync(m Model) tea.Cmd {
|
||||
if len(m.lines) == 0 {
|
||||
return nil
|
||||
}
|
||||
top, bottom := m.scrollArea()
|
||||
return tea.SyncScrollArea(m.visibleLines(), top, bottom)
|
||||
}
|
||||
|
||||
// ViewDown is a high performance command that moves the viewport up by a given
|
||||
// number of lines. Use Model.ViewDown to get the lines that should be rendered.
|
||||
// For example:
|
||||
//
|
||||
// lines := model.ViewDown(1)
|
||||
// cmd := ViewDown(m, lines)
|
||||
//
|
||||
// Deprecated: high performance rendering is deprecated in Bubble Tea.
|
||||
func ViewDown(m Model, lines []string) tea.Cmd {
|
||||
if len(lines) == 0 {
|
||||
return nil
|
||||
}
|
||||
top, bottom := m.scrollArea()
|
||||
|
||||
// XXX: high performance rendering is deprecated in Bubble Tea. In a v2 we
|
||||
// won't need to return a command here.
|
||||
return tea.ScrollDown(lines, top, bottom)
|
||||
}
|
||||
|
||||
// ViewUp is a high performance command the moves the viewport down by a given
|
||||
// number of lines height. Use Model.ViewUp to get the lines that should be
|
||||
// rendered.
|
||||
//
|
||||
// Deprecated: high performance rendering is deprecated in Bubble Tea.
|
||||
func ViewUp(m Model, lines []string) tea.Cmd {
|
||||
if len(lines) == 0 {
|
||||
return nil
|
||||
}
|
||||
top, bottom := m.scrollArea()
|
||||
|
||||
// XXX: high performance rendering is deprecated in Bubble Tea. In a v2 we
|
||||
// won't need to return a command here.
|
||||
return tea.ScrollUp(lines, top, bottom)
|
||||
}
|
||||
|
||||
// Update handles standard message-based viewport updates.
|
||||
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
||||
var cmd tea.Cmd
|
||||
m, cmd = m.updateAsModel(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
// Author's note: this method has been broken out to make it easier to
|
||||
// potentially transition Update to satisfy tea.Model.
|
||||
func (m Model) updateAsModel(msg tea.Msg) (Model, tea.Cmd) {
|
||||
if !m.initialized {
|
||||
m.setInitialValues()
|
||||
}
|
||||
|
||||
var cmd tea.Cmd
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyMsg:
|
||||
switch {
|
||||
case key.Matches(msg, m.KeyMap.PageDown):
|
||||
lines := m.PageDown()
|
||||
if m.HighPerformanceRendering {
|
||||
cmd = ViewDown(m, lines)
|
||||
}
|
||||
|
||||
case key.Matches(msg, m.KeyMap.PageUp):
|
||||
lines := m.PageUp()
|
||||
if m.HighPerformanceRendering {
|
||||
cmd = ViewUp(m, lines)
|
||||
}
|
||||
|
||||
case key.Matches(msg, m.KeyMap.HalfPageDown):
|
||||
lines := m.HalfPageDown()
|
||||
if m.HighPerformanceRendering {
|
||||
cmd = ViewDown(m, lines)
|
||||
}
|
||||
|
||||
case key.Matches(msg, m.KeyMap.HalfPageUp):
|
||||
lines := m.HalfPageUp()
|
||||
if m.HighPerformanceRendering {
|
||||
cmd = ViewUp(m, lines)
|
||||
}
|
||||
|
||||
case key.Matches(msg, m.KeyMap.Down):
|
||||
lines := m.ScrollDown(1)
|
||||
if m.HighPerformanceRendering {
|
||||
cmd = ViewDown(m, lines)
|
||||
}
|
||||
|
||||
case key.Matches(msg, m.KeyMap.Up):
|
||||
lines := m.ScrollUp(1)
|
||||
if m.HighPerformanceRendering {
|
||||
cmd = ViewUp(m, lines)
|
||||
}
|
||||
|
||||
case key.Matches(msg, m.KeyMap.Left):
|
||||
m.ScrollLeft(m.horizontalStep)
|
||||
|
||||
case key.Matches(msg, m.KeyMap.Right):
|
||||
m.ScrollRight(m.horizontalStep)
|
||||
}
|
||||
|
||||
case tea.MouseMsg:
|
||||
if !m.MouseWheelEnabled || msg.Action != tea.MouseActionPress {
|
||||
break
|
||||
}
|
||||
switch msg.Button { //nolint:exhaustive
|
||||
case tea.MouseButtonWheelUp:
|
||||
if msg.Shift {
|
||||
// Note that not every terminal emulator sends the shift event for mouse actions by default (looking at you Konsole)
|
||||
m.ScrollLeft(m.horizontalStep)
|
||||
} else {
|
||||
lines := m.ScrollUp(m.MouseWheelDelta)
|
||||
if m.HighPerformanceRendering {
|
||||
cmd = ViewUp(m, lines)
|
||||
}
|
||||
}
|
||||
|
||||
case tea.MouseButtonWheelDown:
|
||||
if msg.Shift {
|
||||
m.ScrollRight(m.horizontalStep)
|
||||
} else {
|
||||
lines := m.ScrollDown(m.MouseWheelDelta)
|
||||
if m.HighPerformanceRendering {
|
||||
cmd = ViewDown(m, lines)
|
||||
}
|
||||
}
|
||||
// Note that not every terminal emulator sends the horizontal wheel events by default (looking at you Konsole)
|
||||
case tea.MouseButtonWheelLeft:
|
||||
m.ScrollLeft(m.horizontalStep)
|
||||
case tea.MouseButtonWheelRight:
|
||||
m.ScrollRight(m.horizontalStep)
|
||||
}
|
||||
}
|
||||
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
// View renders the viewport into a string.
|
||||
func (m Model) View() string {
|
||||
if m.HighPerformanceRendering {
|
||||
// Just send newlines since we're going to be rendering the actual
|
||||
// content separately. We still need to send something that equals the
|
||||
// height of this view so that the Bubble Tea standard renderer can
|
||||
// position anything below this view properly.
|
||||
return strings.Repeat("\n", max(0, m.Height-1))
|
||||
}
|
||||
|
||||
w, h := m.Width, m.Height
|
||||
if sw := m.Style.GetWidth(); sw != 0 {
|
||||
w = min(w, sw)
|
||||
}
|
||||
if sh := m.Style.GetHeight(); sh != 0 {
|
||||
h = min(h, sh)
|
||||
}
|
||||
contentWidth := w - m.Style.GetHorizontalFrameSize()
|
||||
contentHeight := h - m.Style.GetVerticalFrameSize()
|
||||
contents := lipgloss.NewStyle().
|
||||
Width(contentWidth). // pad to width.
|
||||
Height(contentHeight). // pad to height.
|
||||
MaxHeight(contentHeight). // truncate height if taller.
|
||||
MaxWidth(contentWidth). // truncate width if wider.
|
||||
Render(strings.Join(m.visibleLines(), "\n"))
|
||||
return m.Style.
|
||||
UnsetWidth().UnsetHeight(). // Style size already applied in contents.
|
||||
Render(contents)
|
||||
}
|
||||
|
||||
func clamp(v, low, high int) int {
|
||||
if high < low {
|
||||
low, high = high, low
|
||||
}
|
||||
return min(high, max(low, v))
|
||||
}
|
||||
|
||||
func findLongestLineWidth(lines []string) int {
|
||||
w := 0
|
||||
for _, l := range lines {
|
||||
if ww := ansi.StringWidth(l); ww > w {
|
||||
w = ww
|
||||
}
|
||||
}
|
||||
return w
|
||||
}
|
||||
2
vendor/github.com/docker/cli/cli-plugins/metadata/metadata.go
generated
vendored
2
vendor/github.com/docker/cli/cli-plugins/metadata/metadata.go
generated
vendored
@ -33,4 +33,6 @@ type Metadata struct {
|
||||
ShortDescription string `json:",omitempty"`
|
||||
// URL is a pointer to the plugin's homepage.
|
||||
URL string `json:",omitempty"`
|
||||
// Hidden hides the plugin in completion and help message output.
|
||||
Hidden bool `json:",omitempty"`
|
||||
}
|
||||
|
||||
7
vendor/github.com/docker/cli/cli/cobra.go
generated
vendored
7
vendor/github.com/docker/cli/cli/cobra.go
generated
vendored
@ -376,13 +376,10 @@ func orchestratorSubCommands(cmd *cobra.Command) []*cobra.Command {
|
||||
func allManagementSubCommands(cmd *cobra.Command) []*cobra.Command {
|
||||
cmds := []*cobra.Command{}
|
||||
for _, sub := range cmd.Commands() {
|
||||
if isPlugin(sub) {
|
||||
if invalidPluginReason(sub) == "" {
|
||||
cmds = append(cmds, sub)
|
||||
}
|
||||
if invalidPluginReason(sub) != "" {
|
||||
continue
|
||||
}
|
||||
if sub.IsAvailableCommand() && sub.HasSubCommands() {
|
||||
if sub.IsAvailableCommand() && (isPlugin(sub) || sub.HasSubCommands()) {
|
||||
cmds = append(cmds, sub)
|
||||
}
|
||||
}
|
||||
|
||||
40
vendor/github.com/docker/cli/cli/command/cli.go
generated
vendored
40
vendor/github.com/docker/cli/cli/command/cli.go
generated
vendored
@ -48,9 +48,7 @@ type Cli interface {
|
||||
Apply(ops ...CLIOption) error
|
||||
config.Provider
|
||||
ServerInfo() ServerInfo
|
||||
DefaultVersion() string
|
||||
CurrentVersion() string
|
||||
ContentTrustEnabled() bool
|
||||
BuildKitEnabled() (bool, error)
|
||||
ContextStore() store.Store
|
||||
CurrentContext() string
|
||||
@ -78,6 +76,7 @@ type DockerCli struct {
|
||||
dockerEndpoint docker.Endpoint
|
||||
contextStoreConfig *store.Config
|
||||
initTimeout time.Duration
|
||||
userAgent string
|
||||
res telemetryResource
|
||||
|
||||
// baseCtx is the base context used for internal operations. In the future
|
||||
@ -89,6 +88,8 @@ type DockerCli struct {
|
||||
}
|
||||
|
||||
// DefaultVersion returns [api.DefaultVersion].
|
||||
//
|
||||
// Deprecated: this function is no longer used and will be removed in the next release.
|
||||
func (*DockerCli) DefaultVersion() string {
|
||||
return api.DefaultVersion
|
||||
}
|
||||
@ -159,6 +160,8 @@ func (cli *DockerCli) ServerInfo() ServerInfo {
|
||||
|
||||
// ContentTrustEnabled returns whether content trust has been enabled by an
|
||||
// environment variable.
|
||||
//
|
||||
// Deprecated: check the value of the DOCKER_CONTENT_TRUST environment variable to detect whether content-trust is enabled.
|
||||
func (cli *DockerCli) ContentTrustEnabled() bool {
|
||||
return cli.contentTrust
|
||||
}
|
||||
@ -269,7 +272,7 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption)
|
||||
cli.contextStore = &ContextStoreWithDefault{
|
||||
Store: store.New(config.ContextStoreDir(), *cli.contextStoreConfig),
|
||||
Resolver: func() (*DefaultContext, error) {
|
||||
return ResolveDefaultContext(cli.options, *cli.contextStoreConfig)
|
||||
return resolveDefaultContext(cli.options, *cli.contextStoreConfig)
|
||||
},
|
||||
}
|
||||
|
||||
@ -306,17 +309,17 @@ func NewAPIClientFromFlags(opts *cliflags.ClientOptions, configFile *configfile.
|
||||
contextStore := &ContextStoreWithDefault{
|
||||
Store: store.New(config.ContextStoreDir(), storeConfig),
|
||||
Resolver: func() (*DefaultContext, error) {
|
||||
return ResolveDefaultContext(opts, storeConfig)
|
||||
return resolveDefaultContext(opts, storeConfig)
|
||||
},
|
||||
}
|
||||
endpoint, err := resolveDockerEndpoint(contextStore, resolveContextName(opts, configFile))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to resolve docker endpoint")
|
||||
}
|
||||
return newAPIClientFromEndpoint(endpoint, configFile)
|
||||
return newAPIClientFromEndpoint(endpoint, configFile, client.WithUserAgent(UserAgent()))
|
||||
}
|
||||
|
||||
func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigFile) (client.APIClient, error) {
|
||||
func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigFile, extraOpts ...client.Opt) (client.APIClient, error) {
|
||||
opts, err := ep.ClientOpts()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -324,7 +327,14 @@ func newAPIClientFromEndpoint(ep docker.Endpoint, configFile *configfile.ConfigF
|
||||
if len(configFile.HTTPHeaders) > 0 {
|
||||
opts = append(opts, client.WithHTTPHeaders(configFile.HTTPHeaders))
|
||||
}
|
||||
opts = append(opts, withCustomHeadersFromEnv(), client.WithUserAgent(UserAgent()))
|
||||
withCustomHeaders, err := withCustomHeadersFromEnv()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if withCustomHeaders != nil {
|
||||
opts = append(opts, withCustomHeaders)
|
||||
}
|
||||
opts = append(opts, extraOpts...)
|
||||
return client.NewClientWithOpts(opts...)
|
||||
}
|
||||
|
||||
@ -545,7 +555,8 @@ func (cli *DockerCli) initialize() error {
|
||||
return
|
||||
}
|
||||
if cli.client == nil {
|
||||
if cli.client, cli.initErr = newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile); cli.initErr != nil {
|
||||
ops := []client.Opt{client.WithUserAgent(cli.userAgent)}
|
||||
if cli.client, cli.initErr = newAPIClientFromEndpoint(cli.dockerEndpoint, cli.configFile, ops...); cli.initErr != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -558,6 +569,8 @@ func (cli *DockerCli) initialize() error {
|
||||
}
|
||||
|
||||
// Apply all the operation on the cli
|
||||
//
|
||||
// Deprecated: this method is no longer used and will be removed in the next release if there are no remaining users.
|
||||
func (cli *DockerCli) Apply(ops ...CLIOption) error {
|
||||
for _, op := range ops {
|
||||
if err := op(cli); err != nil {
|
||||
@ -589,15 +602,18 @@ type ServerInfo struct {
|
||||
// environment.
|
||||
func NewDockerCli(ops ...CLIOption) (*DockerCli, error) {
|
||||
defaultOps := []CLIOption{
|
||||
WithContentTrustFromEnv(),
|
||||
withContentTrustFromEnv(),
|
||||
WithDefaultContextStoreConfig(),
|
||||
WithStandardStreams(),
|
||||
WithUserAgent(UserAgent()),
|
||||
}
|
||||
ops = append(defaultOps, ops...)
|
||||
|
||||
cli := &DockerCli{baseCtx: context.Background()}
|
||||
if err := cli.Apply(ops...); err != nil {
|
||||
return nil, err
|
||||
for _, op := range ops {
|
||||
if err := op(cli); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return cli, nil
|
||||
}
|
||||
@ -613,7 +629,7 @@ func getServerHost(hosts []string, defaultToTLS bool) (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// UserAgent returns the user agent string used for making API requests
|
||||
// UserAgent returns the default user agent string used for making API requests.
|
||||
func UserAgent() string {
|
||||
return "Docker-Client/" + version.Version + " (" + runtime.GOOS + ")"
|
||||
}
|
||||
|
||||
118
vendor/github.com/docker/cli/cli/command/cli_options.go
generated
vendored
118
vendor/github.com/docker/cli/cli/command/cli_options.go
generated
vendored
@ -75,8 +75,8 @@ func WithErrorStream(err io.Writer) CLIOption {
|
||||
}
|
||||
}
|
||||
|
||||
// WithContentTrustFromEnv enables content trust on a cli from environment variable DOCKER_CONTENT_TRUST value.
|
||||
func WithContentTrustFromEnv() CLIOption {
|
||||
// withContentTrustFromEnv enables content trust on a cli from environment variable DOCKER_CONTENT_TRUST value.
|
||||
func withContentTrustFromEnv() CLIOption {
|
||||
return func(cli *DockerCli) error {
|
||||
cli.contentTrust = false
|
||||
if e := os.Getenv("DOCKER_CONTENT_TRUST"); e != "" {
|
||||
@ -89,7 +89,16 @@ func WithContentTrustFromEnv() CLIOption {
|
||||
}
|
||||
}
|
||||
|
||||
// WithContentTrustFromEnv enables content trust on a cli from environment variable DOCKER_CONTENT_TRUST value.
|
||||
//
|
||||
// Deprecated: this option is no longer used, and will be removed in the next release.
|
||||
func WithContentTrustFromEnv() CLIOption {
|
||||
return withContentTrustFromEnv()
|
||||
}
|
||||
|
||||
// WithContentTrust enables content trust on a cli.
|
||||
//
|
||||
// Deprecated: this option is no longer used, and will be removed in the next release.
|
||||
func WithContentTrust(enabled bool) CLIOption {
|
||||
return func(cli *DockerCli) error {
|
||||
cli.contentTrust = enabled
|
||||
@ -180,61 +189,70 @@ const envOverrideHTTPHeaders = "DOCKER_CUSTOM_HEADERS"
|
||||
// override headers with the same name).
|
||||
//
|
||||
// TODO(thaJeztah): this is a client Option, and should be moved to the client. It is non-exported for that reason.
|
||||
func withCustomHeadersFromEnv() client.Opt {
|
||||
return func(apiClient *client.Client) error {
|
||||
value := os.Getenv(envOverrideHTTPHeaders)
|
||||
if value == "" {
|
||||
return nil
|
||||
}
|
||||
csvReader := csv.NewReader(strings.NewReader(value))
|
||||
fields, err := csvReader.Read()
|
||||
if err != nil {
|
||||
return invalidParameter(errors.Errorf(
|
||||
"failed to parse custom headers from %s environment variable: value must be formatted as comma-separated key=value pairs",
|
||||
envOverrideHTTPHeaders,
|
||||
func withCustomHeadersFromEnv() (client.Opt, error) {
|
||||
value := os.Getenv(envOverrideHTTPHeaders)
|
||||
if value == "" {
|
||||
return nil, nil
|
||||
}
|
||||
csvReader := csv.NewReader(strings.NewReader(value))
|
||||
fields, err := csvReader.Read()
|
||||
if err != nil {
|
||||
return nil, invalidParameter(errors.Errorf(
|
||||
"failed to parse custom headers from %s environment variable: value must be formatted as comma-separated key=value pairs",
|
||||
envOverrideHTTPHeaders,
|
||||
))
|
||||
}
|
||||
if len(fields) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
env := map[string]string{}
|
||||
for _, kv := range fields {
|
||||
k, v, hasValue := strings.Cut(kv, "=")
|
||||
|
||||
// Only strip whitespace in keys; preserve whitespace in values.
|
||||
k = strings.TrimSpace(k)
|
||||
|
||||
if k == "" {
|
||||
return nil, invalidParameter(errors.Errorf(
|
||||
`failed to set custom headers from %s environment variable: value contains a key=value pair with an empty key: '%s'`,
|
||||
envOverrideHTTPHeaders, kv,
|
||||
))
|
||||
}
|
||||
if len(fields) == 0 {
|
||||
return nil
|
||||
|
||||
// We don't currently allow empty key=value pairs, and produce an error.
|
||||
// This is something we could allow in future (e.g. to read value
|
||||
// from an environment variable with the same name). In the meantime,
|
||||
// produce an error to prevent users from depending on this.
|
||||
if !hasValue {
|
||||
return nil, invalidParameter(errors.Errorf(
|
||||
`failed to set custom headers from %s environment variable: missing "=" in key=value pair: '%s'`,
|
||||
envOverrideHTTPHeaders, kv,
|
||||
))
|
||||
}
|
||||
|
||||
env := map[string]string{}
|
||||
for _, kv := range fields {
|
||||
k, v, hasValue := strings.Cut(kv, "=")
|
||||
env[http.CanonicalHeaderKey(k)] = v
|
||||
}
|
||||
|
||||
// Only strip whitespace in keys; preserve whitespace in values.
|
||||
k = strings.TrimSpace(k)
|
||||
if len(env) == 0 {
|
||||
// We should probably not hit this case, as we don't skip values
|
||||
// (only return errors), but we don't want to discard existing
|
||||
// headers with an empty set.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if k == "" {
|
||||
return invalidParameter(errors.Errorf(
|
||||
`failed to set custom headers from %s environment variable: value contains a key=value pair with an empty key: '%s'`,
|
||||
envOverrideHTTPHeaders, kv,
|
||||
))
|
||||
}
|
||||
// TODO(thaJeztah): add a client.WithExtraHTTPHeaders() function to allow these headers to be _added_ to existing ones, instead of _replacing_
|
||||
// see https://github.com/docker/cli/pull/5098#issuecomment-2147403871 (when updating, also update the WARNING in the function and env-var GoDoc)
|
||||
return client.WithHTTPHeaders(env), nil
|
||||
}
|
||||
|
||||
// We don't currently allow empty key=value pairs, and produce an error.
|
||||
// This is something we could allow in future (e.g. to read value
|
||||
// from an environment variable with the same name). In the meantime,
|
||||
// produce an error to prevent users from depending on this.
|
||||
if !hasValue {
|
||||
return invalidParameter(errors.Errorf(
|
||||
`failed to set custom headers from %s environment variable: missing "=" in key=value pair: '%s'`,
|
||||
envOverrideHTTPHeaders, kv,
|
||||
))
|
||||
}
|
||||
|
||||
env[http.CanonicalHeaderKey(k)] = v
|
||||
// WithUserAgent configures the User-Agent string for cli HTTP requests.
|
||||
func WithUserAgent(userAgent string) CLIOption {
|
||||
return func(cli *DockerCli) error {
|
||||
if userAgent == "" {
|
||||
return errors.New("user agent cannot be blank")
|
||||
}
|
||||
|
||||
if len(env) == 0 {
|
||||
// We should probably not hit this case, as we don't skip values
|
||||
// (only return errors), but we don't want to discard existing
|
||||
// headers with an empty set.
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO(thaJeztah): add a client.WithExtraHTTPHeaders() function to allow these headers to be _added_ to existing ones, instead of _replacing_
|
||||
// see https://github.com/docker/cli/pull/5098#issuecomment-2147403871 (when updating, also update the WARNING in the function and env-var GoDoc)
|
||||
return client.WithHTTPHeaders(env)(apiClient)
|
||||
cli.userAgent = userAgent
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
7
vendor/github.com/docker/cli/cli/command/defaultcontextstore.go
generated
vendored
7
vendor/github.com/docker/cli/cli/command/defaultcontextstore.go
generated
vendored
@ -52,7 +52,14 @@ type EndpointDefaultResolver interface {
|
||||
}
|
||||
|
||||
// ResolveDefaultContext creates a Metadata for the current CLI invocation parameters
|
||||
//
|
||||
// Deprecated: this function is exported for testing and meant for internal use. It will be removed in the next release.
|
||||
func ResolveDefaultContext(opts *cliflags.ClientOptions, config store.Config) (*DefaultContext, error) {
|
||||
return resolveDefaultContext(opts, config)
|
||||
}
|
||||
|
||||
// resolveDefaultContext creates a Metadata for the current CLI invocation parameters
|
||||
func resolveDefaultContext(opts *cliflags.ClientOptions, config store.Config) (*DefaultContext, error) {
|
||||
contextTLSData := store.ContextTLSData{
|
||||
Endpoints: make(map[string]store.EndpointTLSData),
|
||||
}
|
||||
|
||||
2
vendor/github.com/docker/cli/cli/command/formatter/tabwriter/tabwriter.go
generated
vendored
2
vendor/github.com/docker/cli/cli/command/formatter/tabwriter/tabwriter.go
generated
vendored
@ -12,7 +12,7 @@
|
||||
|
||||
// based on https://github.com/golang/go/blob/master/src/text/tabwriter/tabwriter.go Last modified 690ac40 on 31 Jan
|
||||
|
||||
//nolint:gocyclo,nakedret,unused // ignore linting errors, so that we can stick close to upstream
|
||||
//nolint:gocyclo,gofumpt,nakedret,unused // ignore linting errors, so that we can stick close to upstream
|
||||
package tabwriter
|
||||
|
||||
import (
|
||||
|
||||
40
vendor/github.com/docker/cli/cli/command/registry.go
generated
vendored
40
vendor/github.com/docker/cli/cli/command/registry.go
generated
vendored
@ -77,7 +77,16 @@ func ResolveAuthConfig(cfg *configfile.ConfigFile, index *registrytypes.IndexInf
|
||||
}
|
||||
|
||||
a, _ := cfg.GetAuthConfig(configKey)
|
||||
return registrytypes.AuthConfig(a)
|
||||
return registrytypes.AuthConfig{
|
||||
Username: a.Username,
|
||||
Password: a.Password,
|
||||
ServerAddress: a.ServerAddress,
|
||||
|
||||
// TODO(thaJeztah): Are these expected to be included?
|
||||
Auth: a.Auth,
|
||||
IdentityToken: a.IdentityToken,
|
||||
RegistryToken: a.RegistryToken,
|
||||
}
|
||||
}
|
||||
|
||||
// GetDefaultAuthConfig gets the default auth config given a serverAddress
|
||||
@ -86,19 +95,27 @@ func GetDefaultAuthConfig(cfg *configfile.ConfigFile, checkCredStore bool, serve
|
||||
if !isDefaultRegistry {
|
||||
serverAddress = credentials.ConvertToHostname(serverAddress)
|
||||
}
|
||||
authconfig := configtypes.AuthConfig{}
|
||||
authCfg := configtypes.AuthConfig{}
|
||||
var err error
|
||||
if checkCredStore {
|
||||
authconfig, err = cfg.GetAuthConfig(serverAddress)
|
||||
authCfg, err = cfg.GetAuthConfig(serverAddress)
|
||||
if err != nil {
|
||||
return registrytypes.AuthConfig{
|
||||
ServerAddress: serverAddress,
|
||||
}, err
|
||||
}
|
||||
}
|
||||
authconfig.ServerAddress = serverAddress
|
||||
authconfig.IdentityToken = ""
|
||||
return registrytypes.AuthConfig(authconfig), nil
|
||||
|
||||
return registrytypes.AuthConfig{
|
||||
Username: authCfg.Username,
|
||||
Password: authCfg.Password,
|
||||
ServerAddress: serverAddress,
|
||||
|
||||
// TODO(thaJeztah): Are these expected to be included?
|
||||
Auth: authCfg.Auth,
|
||||
IdentityToken: "",
|
||||
RegistryToken: authCfg.RegistryToken,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// PromptUserForCredentials handles the CLI prompt for the user to input
|
||||
@ -213,7 +230,16 @@ func RetrieveAuthTokenFromImage(cfg *configfile.ConfigFile, image string) (strin
|
||||
return "", err
|
||||
}
|
||||
|
||||
encodedAuth, err := registrytypes.EncodeAuthConfig(registrytypes.AuthConfig(authConfig))
|
||||
encodedAuth, err := registrytypes.EncodeAuthConfig(registrytypes.AuthConfig{
|
||||
Username: authConfig.Username,
|
||||
Password: authConfig.Password,
|
||||
ServerAddress: authConfig.ServerAddress,
|
||||
|
||||
// TODO(thaJeztah): Are these expected to be included?
|
||||
Auth: authConfig.Auth,
|
||||
IdentityToken: authConfig.IdentityToken,
|
||||
RegistryToken: authConfig.RegistryToken,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
7
vendor/github.com/docker/cli/cli/compose/loader/merge.go
generated
vendored
7
vendor/github.com/docker/cli/cli/compose/loader/merge.go
generated
vendored
@ -4,6 +4,7 @@
|
||||
package loader
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
|
||||
@ -116,7 +117,11 @@ func toServicePortConfigsMap(s any) (map[any]any, error) {
|
||||
}
|
||||
m := map[any]any{}
|
||||
for _, p := range ports {
|
||||
m[p.Published] = p
|
||||
protocol := "tcp"
|
||||
if p.Protocol != "" {
|
||||
protocol = p.Protocol
|
||||
}
|
||||
m[fmt.Sprintf("%d%s", p.Published, protocol)] = p
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
12
vendor/github.com/docker/cli/cli/config/memorystore/store.go
generated
vendored
12
vendor/github.com/docker/cli/cli/config/memorystore/store.go
generated
vendored
@ -3,7 +3,6 @@
|
||||
package memorystore
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"os"
|
||||
@ -13,12 +12,17 @@ import (
|
||||
"github.com/docker/cli/cli/config/types"
|
||||
)
|
||||
|
||||
var errValueNotFound = errors.New("value not found")
|
||||
// notFoundErr is the error returned when a plugin could not be found.
|
||||
type notFoundErr string
|
||||
|
||||
func IsErrValueNotFound(err error) bool {
|
||||
return errors.Is(err, errValueNotFound)
|
||||
func (notFoundErr) NotFound() {}
|
||||
|
||||
func (e notFoundErr) Error() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
var errValueNotFound notFoundErr = "value not found"
|
||||
|
||||
type Config struct {
|
||||
lock sync.RWMutex
|
||||
memoryCredentials map[string]types.AuthConfig
|
||||
|
||||
36
vendor/github.com/docker/cli/cli/context/docker/load.go
generated
vendored
36
vendor/github.com/docker/cli/cli/context/docker/load.go
generated
vendored
@ -101,7 +101,22 @@ func (ep *Endpoint) ClientOpts() ([]client.Opt, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, withHTTPClient(tlsConfig))
|
||||
|
||||
// If there's no tlsConfig available, we use the default HTTPClient.
|
||||
if tlsConfig != nil {
|
||||
result = append(result,
|
||||
client.WithHTTPClient(&http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: tlsConfig,
|
||||
DialContext: (&net.Dialer{
|
||||
KeepAlive: 30 * time.Second,
|
||||
Timeout: 30 * time.Second,
|
||||
}).DialContext,
|
||||
},
|
||||
CheckRedirect: client.CheckRedirect,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
result = append(result, client.WithHost(ep.Host))
|
||||
} else {
|
||||
@ -133,25 +148,6 @@ func isSocket(addr string) bool {
|
||||
}
|
||||
}
|
||||
|
||||
func withHTTPClient(tlsConfig *tls.Config) func(*client.Client) error {
|
||||
return func(c *client.Client) error {
|
||||
if tlsConfig == nil {
|
||||
// Use the default HTTPClient
|
||||
return nil
|
||||
}
|
||||
return client.WithHTTPClient(&http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: tlsConfig,
|
||||
DialContext: (&net.Dialer{
|
||||
KeepAlive: 30 * time.Second,
|
||||
Timeout: 30 * time.Second,
|
||||
}).DialContext,
|
||||
},
|
||||
CheckRedirect: client.CheckRedirect,
|
||||
})(c)
|
||||
}
|
||||
}
|
||||
|
||||
// EndpointFromContext parses a context docker endpoint metadata into a typed EndpointMeta structure
|
||||
func EndpointFromContext(metadata store.Metadata) (EndpointMeta, error) {
|
||||
ep, ok := metadata.Endpoints[DockerEndpoint]
|
||||
|
||||
4
vendor/github.com/docker/cli/opts/opts.go
generated
vendored
4
vendor/github.com/docker/cli/opts/opts.go
generated
vendored
@ -60,6 +60,8 @@ func (opts *ListOpts) Set(value string) error {
|
||||
}
|
||||
|
||||
// Delete removes the specified element from the slice.
|
||||
//
|
||||
// Deprecated: this method is no longer used and will be removed in the next release.
|
||||
func (opts *ListOpts) Delete(key string) {
|
||||
for i, k := range *opts.values {
|
||||
if k == key {
|
||||
@ -264,6 +266,8 @@ func ValidateIPAddress(val string) (string, error) {
|
||||
}
|
||||
|
||||
// ValidateMACAddress validates a MAC address.
|
||||
//
|
||||
// Deprecated: use [net.ParseMAC]. This function will be removed in the next release.
|
||||
func ValidateMACAddress(val string) (string, error) {
|
||||
_, err := net.ParseMAC(strings.TrimSpace(val))
|
||||
if err != nil {
|
||||
|
||||
6
vendor/github.com/docker/cli/templates/templates.go
generated
vendored
6
vendor/github.com/docker/cli/templates/templates.go
generated
vendored
@ -71,7 +71,7 @@ var HeaderFunctions = template.FuncMap{
|
||||
// Parse creates a new anonymous template with the basic functions
|
||||
// and parses the given format.
|
||||
func Parse(format string) (*template.Template, error) {
|
||||
return NewParse("", format)
|
||||
return template.New("").Funcs(basicFunctions).Parse(format)
|
||||
}
|
||||
|
||||
// New creates a new empty template with the provided tag and built-in
|
||||
@ -82,8 +82,10 @@ func New(tag string) *template.Template {
|
||||
|
||||
// NewParse creates a new tagged template with the basic functions
|
||||
// and parses the given format.
|
||||
//
|
||||
// Deprecated: this function is unused and will be removed in the next release. Use [New] if you need to set a tag, or [Parse] instead.
|
||||
func NewParse(tag, format string) (*template.Template, error) {
|
||||
return New(tag).Parse(format)
|
||||
return template.New(tag).Funcs(basicFunctions).Parse(format)
|
||||
}
|
||||
|
||||
// padWithSpace adds whitespace to the input if the input is non-empty
|
||||
|
||||
103
vendor/github.com/docker/docker/api/swagger.yaml
generated
vendored
103
vendor/github.com/docker/docker/api/swagger.yaml
generated
vendored
@ -81,7 +81,6 @@ info:
|
||||
{
|
||||
"username": "string",
|
||||
"password": "string",
|
||||
"email": "string",
|
||||
"serveraddress": "string"
|
||||
}
|
||||
```
|
||||
@ -637,6 +636,9 @@ definitions:
|
||||
by the default (runc) runtime.
|
||||
|
||||
This field is omitted when empty.
|
||||
|
||||
**Deprecated**: This field is deprecated as kernel 6.12 has deprecated `memory.kmem.tcp.limit_in_bytes` field
|
||||
for cgroups v1. This field will be removed in a future release.
|
||||
type: "integer"
|
||||
format: "int64"
|
||||
MemoryReservation:
|
||||
@ -1531,37 +1533,6 @@ definitions:
|
||||
items:
|
||||
type: "string"
|
||||
example: ["/bin/sh", "-c"]
|
||||
# FIXME(thaJeztah): temporarily using a full example to remove some "omitempty" fields. Remove once the fields are removed.
|
||||
example:
|
||||
"User": "web:web"
|
||||
"ExposedPorts": {
|
||||
"80/tcp": {},
|
||||
"443/tcp": {}
|
||||
}
|
||||
"Env": ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"]
|
||||
"Cmd": ["/bin/sh"]
|
||||
"Healthcheck": {
|
||||
"Test": ["string"],
|
||||
"Interval": 0,
|
||||
"Timeout": 0,
|
||||
"Retries": 0,
|
||||
"StartPeriod": 0,
|
||||
"StartInterval": 0
|
||||
}
|
||||
"ArgsEscaped": true
|
||||
"Volumes": {
|
||||
"/app/data": {},
|
||||
"/app/config": {}
|
||||
}
|
||||
"WorkingDir": "/public/"
|
||||
"Entrypoint": []
|
||||
"OnBuild": []
|
||||
"Labels": {
|
||||
"com.example.some-label": "some-value",
|
||||
"com.example.some-other-label": "some-other-value"
|
||||
}
|
||||
"StopSignal": "SIGTERM"
|
||||
"Shell": ["/bin/sh", "-c"]
|
||||
|
||||
NetworkingConfig:
|
||||
description: |
|
||||
@ -1967,6 +1938,11 @@ definitions:
|
||||
Depending on how the image was created, this field may be empty and
|
||||
is only set for images that were built/created locally. This field
|
||||
is empty if the image was pulled from an image registry.
|
||||
|
||||
> **Deprecated**: This field is only set when using the deprecated
|
||||
> legacy builder. It is included in API responses for informational
|
||||
> purposes, but should not be depended on as it will be omitted
|
||||
> once the legacy builder is removed.
|
||||
type: "string"
|
||||
x-nullable: false
|
||||
example: ""
|
||||
@ -1992,6 +1968,11 @@ definitions:
|
||||
The version of Docker that was used to build the image.
|
||||
|
||||
Depending on how the image was created, this field may be empty.
|
||||
|
||||
> **Deprecated**: This field is only set when using the deprecated
|
||||
> legacy builder. It is included in API responses for informational
|
||||
> purposes, but should not be depended on as it will be omitted
|
||||
> once the legacy builder is removed.
|
||||
type: "string"
|
||||
x-nullable: false
|
||||
example: "27.0.1"
|
||||
@ -2036,14 +2017,6 @@ definitions:
|
||||
format: "int64"
|
||||
x-nullable: false
|
||||
example: 1239828
|
||||
VirtualSize:
|
||||
description: |
|
||||
Total size of the image including all layers it is composed of.
|
||||
|
||||
Deprecated: this field is omitted in API v1.44, but kept for backward compatibility. Use Size instead.
|
||||
type: "integer"
|
||||
format: "int64"
|
||||
example: 1239828
|
||||
GraphDriver:
|
||||
$ref: "#/definitions/DriverData"
|
||||
RootFS:
|
||||
@ -2176,14 +2149,6 @@ definitions:
|
||||
format: "int64"
|
||||
x-nullable: false
|
||||
example: 1239828
|
||||
VirtualSize:
|
||||
description: |-
|
||||
Total size of the image including all layers it is composed of.
|
||||
|
||||
Deprecated: this field is omitted in API v1.44, but kept for backward compatibility. Use Size instead.
|
||||
type: "integer"
|
||||
format: "int64"
|
||||
example: 172064416
|
||||
Labels:
|
||||
description: "User-defined key/value metadata."
|
||||
type: "object"
|
||||
@ -2688,14 +2653,6 @@ definitions:
|
||||
description: |
|
||||
Unique ID of the build cache record.
|
||||
example: "ndlpt0hhvkqcdfkputsk4cq9c"
|
||||
Parent:
|
||||
description: |
|
||||
ID of the parent build cache record.
|
||||
|
||||
> **Deprecated**: This field is deprecated, and omitted if empty.
|
||||
type: "string"
|
||||
x-nullable: true
|
||||
example: ""
|
||||
Parents:
|
||||
description: |
|
||||
List of parent build cache record IDs.
|
||||
@ -3177,10 +3134,15 @@ definitions:
|
||||
- Args
|
||||
properties:
|
||||
DockerVersion:
|
||||
description: "Docker Version used to create the plugin"
|
||||
description: |-
|
||||
Docker Version used to create the plugin.
|
||||
|
||||
Depending on how the plugin was created, this field may be empty or omitted.
|
||||
|
||||
Deprecated: this field is no longer set, and will be removed in the next API version.
|
||||
type: "string"
|
||||
x-nullable: false
|
||||
example: "17.06.0-ce"
|
||||
x-omitempty: true
|
||||
Description:
|
||||
type: "string"
|
||||
x-nullable: false
|
||||
@ -6382,6 +6344,8 @@ definitions:
|
||||
|
||||
Kernel memory TCP limits are not supported when using cgroups v2, which
|
||||
does not support the corresponding `memory.kmem.tcp.limit_in_bytes` cgroup.
|
||||
|
||||
**Deprecated**: This field is deprecated as kernel 6.12 has deprecated kernel memory TCP accounting.
|
||||
type: "boolean"
|
||||
example: true
|
||||
CpuCfsPeriod:
|
||||
@ -6419,29 +6383,6 @@ definitions:
|
||||
description: "Indicates IPv4 forwarding is enabled."
|
||||
type: "boolean"
|
||||
example: true
|
||||
BridgeNfIptables:
|
||||
description: |
|
||||
Indicates if `bridge-nf-call-iptables` is available on the host when
|
||||
the daemon was started.
|
||||
|
||||
<p><br /></p>
|
||||
|
||||
> **Deprecated**: netfilter module is now loaded on-demand and no longer
|
||||
> during daemon startup, making this field obsolete. This field is always
|
||||
> `false` and will be removed in a API v1.49.
|
||||
type: "boolean"
|
||||
example: false
|
||||
BridgeNfIp6tables:
|
||||
description: |
|
||||
Indicates if `bridge-nf-call-ip6tables` is available on the host.
|
||||
|
||||
<p><br /></p>
|
||||
|
||||
> **Deprecated**: netfilter module is now loaded on-demand, and no longer
|
||||
> during daemon startup, making this field obsolete. This field is always
|
||||
> `false` and will be removed in a API v1.49.
|
||||
type: "boolean"
|
||||
example: false
|
||||
Debug:
|
||||
description: |
|
||||
Indicates if the daemon is running in debug-mode / with debug-level
|
||||
|
||||
7
vendor/github.com/docker/docker/api/types/container/hostconfig.go
generated
vendored
7
vendor/github.com/docker/docker/api/types/container/hostconfig.go
generated
vendored
@ -394,7 +394,12 @@ type Resources struct {
|
||||
|
||||
// KernelMemory specifies the kernel memory limit (in bytes) for the container.
|
||||
// Deprecated: kernel 5.4 deprecated kmem.limit_in_bytes.
|
||||
KernelMemory int64 `json:",omitempty"`
|
||||
KernelMemory int64 `json:",omitempty"`
|
||||
// Hard limit for kernel TCP buffer memory (in bytes).
|
||||
//
|
||||
// Deprecated: This field is deprecated and will be removed in the next release.
|
||||
// Starting with 6.12, the kernel has deprecated kernel memory tcp accounting
|
||||
// for cgroups v1.
|
||||
KernelMemoryTCP int64 `json:",omitempty"` // Hard limit for kernel TCP buffer memory (in bytes)
|
||||
MemoryReservation int64 // Memory soft limit (in bytes)
|
||||
MemorySwap int64 // Total memory usage (memory + swap); set `-1` to enable unlimited swap
|
||||
|
||||
4
vendor/github.com/docker/docker/api/types/image/image_inspect.go
generated
vendored
4
vendor/github.com/docker/docker/api/types/image/image_inspect.go
generated
vendored
@ -48,6 +48,8 @@ type InspectResponse struct {
|
||||
// Depending on how the image was created, this field may be empty and
|
||||
// is only set for images that were built/created locally. This field
|
||||
// is empty if the image was pulled from an image registry.
|
||||
//
|
||||
// Deprecated: this field is deprecated, and will be removed in the next release.
|
||||
Parent string
|
||||
|
||||
// Comment is an optional message that can be set when committing or
|
||||
@ -80,6 +82,8 @@ type InspectResponse struct {
|
||||
// DockerVersion is the version of Docker that was used to build the image.
|
||||
//
|
||||
// Depending on how the image was created, this field may be empty.
|
||||
//
|
||||
// Deprecated: this field is deprecated, and will be removed in the next release.
|
||||
DockerVersion string
|
||||
|
||||
// Author is the name of the author that was specified when committing the
|
||||
|
||||
6
vendor/github.com/docker/docker/api/types/plugin.go
generated
vendored
6
vendor/github.com/docker/docker/api/types/plugin.go
generated
vendored
@ -42,7 +42,11 @@ type PluginConfig struct {
|
||||
// Required: true
|
||||
Description string `json:"Description"`
|
||||
|
||||
// Docker Version used to create the plugin
|
||||
// Docker Version used to create the plugin.
|
||||
//
|
||||
// Depending on how the plugin was created, this field may be empty or omitted.
|
||||
//
|
||||
// Deprecated: this field is no longer set, and will be removed in the next API version.
|
||||
DockerVersion string `json:"DockerVersion,omitempty"`
|
||||
|
||||
// documentation
|
||||
|
||||
30
vendor/github.com/docker/docker/api/types/system/info.go
generated
vendored
30
vendor/github.com/docker/docker/api/types/system/info.go
generated
vendored
@ -9,19 +9,23 @@ import (
|
||||
// Info contains response of Engine API:
|
||||
// GET "/info"
|
||||
type Info struct {
|
||||
ID string
|
||||
Containers int
|
||||
ContainersRunning int
|
||||
ContainersPaused int
|
||||
ContainersStopped int
|
||||
Images int
|
||||
Driver string
|
||||
DriverStatus [][2]string
|
||||
SystemStatus [][2]string `json:",omitempty"` // SystemStatus is only propagated by the Swarm standalone API
|
||||
Plugins PluginsInfo
|
||||
MemoryLimit bool
|
||||
SwapLimit bool
|
||||
KernelMemory bool `json:",omitempty"` // Deprecated: kernel 5.4 deprecated kmem.limit_in_bytes
|
||||
ID string
|
||||
Containers int
|
||||
ContainersRunning int
|
||||
ContainersPaused int
|
||||
ContainersStopped int
|
||||
Images int
|
||||
Driver string
|
||||
DriverStatus [][2]string
|
||||
SystemStatus [][2]string `json:",omitempty"` // SystemStatus is only propagated by the Swarm standalone API
|
||||
Plugins PluginsInfo
|
||||
MemoryLimit bool
|
||||
SwapLimit bool
|
||||
KernelMemory bool `json:",omitempty"` // Deprecated: kernel 5.4 deprecated kmem.limit_in_bytes
|
||||
// KernelMemoryLimit is not supported on cgroups v2.
|
||||
//
|
||||
// Deprecated: This field is deprecated and will be removed in the next release.
|
||||
// Starting with kernel 6.12, the kernel has deprecated kernel memory tcp accounting
|
||||
KernelMemoryTCP bool `json:",omitempty"` // KernelMemoryTCP is not supported on cgroups v2.
|
||||
CPUCfsPeriod bool `json:"CpuCfsPeriod"`
|
||||
CPUCfsQuota bool `json:"CpuCfsQuota"`
|
||||
|
||||
10
vendor/modules.txt
vendored
10
vendor/modules.txt
vendored
@ -65,10 +65,6 @@ github.com/cenkalti/backoff/v5
|
||||
# github.com/cespare/xxhash/v2 v2.3.0
|
||||
## explicit; go 1.11
|
||||
github.com/cespare/xxhash/v2
|
||||
# github.com/charmbracelet/bubbles v0.21.0
|
||||
## explicit; go 1.23.0
|
||||
github.com/charmbracelet/bubbles/key
|
||||
github.com/charmbracelet/bubbles/viewport
|
||||
# github.com/charmbracelet/bubbletea v1.3.10
|
||||
## explicit; go 1.24.0
|
||||
github.com/charmbracelet/bubbletea
|
||||
@ -89,6 +85,8 @@ github.com/charmbracelet/x/ansi/parser
|
||||
# github.com/charmbracelet/x/cellbuf v0.0.13
|
||||
## explicit; go 1.18
|
||||
github.com/charmbracelet/x/cellbuf
|
||||
# github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91
|
||||
## explicit; go 1.19
|
||||
# github.com/charmbracelet/x/term v0.2.1
|
||||
## explicit; go 1.18
|
||||
github.com/charmbracelet/x/term
|
||||
@ -165,7 +163,7 @@ github.com/decentral1se/passgen
|
||||
# github.com/distribution/reference v0.6.0
|
||||
## explicit; go 1.20
|
||||
github.com/distribution/reference
|
||||
# github.com/docker/cli v28.4.0+incompatible
|
||||
# github.com/docker/cli v28.4.0+incompatible => git.coopcloud.tech/toolshed/docker-cli v28.5.3-0.20260202112816-30df2d0b3a00+incompatible
|
||||
## explicit
|
||||
github.com/docker/cli/cli
|
||||
github.com/docker/cli/cli-plugins/metadata
|
||||
@ -215,7 +213,7 @@ github.com/docker/distribution/registry/client/auth/challenge
|
||||
github.com/docker/distribution/registry/client/transport
|
||||
github.com/docker/distribution/registry/storage/cache
|
||||
github.com/docker/distribution/registry/storage/cache/memory
|
||||
# github.com/docker/docker v28.4.0+incompatible
|
||||
# github.com/docker/docker v28.5.2+incompatible
|
||||
## explicit
|
||||
github.com/docker/docker/api
|
||||
github.com/docker/docker/api/types
|
||||
|
||||
Reference in New Issue
Block a user