Compare commits
58 Commits
0.12.0-bet
...
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 |
@ -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 {
|
||||
@ -281,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)
|
||||
}
|
||||
|
||||
@ -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,
|
||||
)
|
||||
|
||||
4
go.mod
4
go.mod
@ -13,7 +13,7 @@ require (
|
||||
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
|
||||
@ -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
|
||||
|
||||
8
go.sum
8
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=
|
||||
@ -318,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=
|
||||
|
||||
@ -8,7 +8,6 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"coopcloud.tech/abra/pkg/config"
|
||||
contextPkg "coopcloud.tech/abra/pkg/context"
|
||||
@ -84,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
|
||||
}
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
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.12.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
|
||||
@ -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" {
|
||||
@ -602,3 +614,89 @@ teardown(){
|
||||
refute_output --partial "WITH_COMMENT=foo"
|
||||
assert_output --partial "WITH_COMMENT=bar"
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
@test "deploy with udp and tcp on same port" {
|
||||
run sed -i 's/TYPE=abra-test-recipe:.*/TYPE=git.coopcloud.tech\/p4u1\/abra-test-recipe:030e8a1cb1a0f17281847b3e55d829220ad32c50/g' \
|
||||
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||
assert_success
|
||||
|
||||
run bash -c "printf '\nCOMPOSE_FILE=\"\$COMPOSE_FILE:compose.udp-and-tcp.yml\"' >> $ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||
assert_success
|
||||
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input
|
||||
assert_success
|
||||
|
||||
run docker service inspect --format '{{ range .Endpoint.Ports }}{{ .Protocol }}={{ .PublishedPort }}{{ end }}' \
|
||||
"${TEST_APP_DOMAIN//./_}_app"
|
||||
assert_success
|
||||
assert_output --partial "tcp=1312"
|
||||
assert_output --partial "udp=1312"
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
@test "does not crash when docker image has no tag" {
|
||||
run sed -i 's/TYPE=abra-test-recipe:.*/TYPE=git.coopcloud.tech\/p4u1\/abra-test-recipe:b29422d5a344ea45df271443182f775ea82b4da8/g' \
|
||||
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||
assert_success
|
||||
|
||||
run bash -c "printf '\nCOMPOSE_FILE=\"\$COMPOSE_FILE:compose.no-image-tag.yml\"' >> $ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
|
||||
assert_success
|
||||
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input
|
||||
assert_success
|
||||
}
|
||||
|
||||
|
||||
@test "does not use old recipe version when recipe is broken" {
|
||||
run $ABRA app new zammad \
|
||||
--no-input \
|
||||
--server "$TEST_SERVER" \
|
||||
--domain "zammad.$TEST_SERVER" \
|
||||
--secrets
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/servers/$TEST_SERVER/zammad.$TEST_SERVER.env"
|
||||
|
||||
# NOTE(d1): --no-converge-checks because the zammad recipe is a beast and we
|
||||
# mostly only care about the correct version being used
|
||||
run $ABRA app deploy "zammad.$TEST_SERVER" \
|
||||
--no-input --no-converge-checks
|
||||
assert_success
|
||||
refute_output --partial "1.0.0+6.3.1-95"
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
@test "unable to deploy borked tag" {
|
||||
_remove_tags
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag \
|
||||
-a "2.4.8_1" -m "feat: completely borked tag"
|
||||
assert_success
|
||||
|
||||
run $ABRA app deploy "$TEST_APP_DOMAIN" "2.4.8_1" \
|
||||
--no-input --no-converge-checks --debug
|
||||
assert_failure
|
||||
assert_output --partial "unable to parse"
|
||||
}
|
||||
|
||||
# bats test_tags=slow
|
||||
@test "app deploy with borked sample env gives useful error" {
|
||||
run $ABRA recipe new foo --no-input
|
||||
assert_success
|
||||
|
||||
run $ABRA app new foo \
|
||||
--no-input \
|
||||
--server "$TEST_SERVER" \
|
||||
--domain "foo.$TEST_SERVER" \
|
||||
--chaos
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/servers/$TEST_SERVER/foo.$TEST_SERVER.env"
|
||||
|
||||
run bash -c "printf '\nEVIL-VAR=EVIL' >> $ABRA_DIR/recipes/foo/.env.sample"
|
||||
assert_success
|
||||
|
||||
run $ABRA app deploy "foo.$TEST_SERVER" \
|
||||
--no-input --no-converge-checks --chaos
|
||||
assert_failure
|
||||
assert_output --partial "unexpected character"
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -180,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,200 +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
|
||||
if [[ -d "$ABRA_DIR/recipes/foobar" ]]; then
|
||||
run rm -rf "$ABRA_DIR/recipes/foobar"
|
||||
assert_success
|
||||
fi
|
||||
}
|
||||
|
||||
@test "validate recipe argument" {
|
||||
run $ABRA recipe sync --no-input
|
||||
assert_failure
|
||||
|
||||
run $ABRA recipe sync DOESNTEXIST --no-input
|
||||
assert_failure
|
||||
}
|
||||
|
||||
@test "allow unstaged changes" {
|
||||
run echo "unstaged changes" >> "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
assert_success
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" status
|
||||
assert_success
|
||||
assert_output --partial 'foo'
|
||||
|
||||
run $ABRA recipe sync "$TEST_RECIPE" --no-input --patch
|
||||
assert_success
|
||||
|
||||
assert_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
assert_equal "$(_git_status)" "M compose.yml ?? foo"
|
||||
|
||||
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
assert_success
|
||||
assert_not_exists "$ABRA_DIR/recipes/$TEST_RECIPE/foo"
|
||||
}
|
||||
|
||||
@test "detect unstaged label changes" {
|
||||
run $ABRA recipe fetch "$TEST_RECIPE"
|
||||
assert_success
|
||||
|
||||
run $ABRA recipe sync "$TEST_RECIPE" --patch
|
||||
assert_success
|
||||
|
||||
run $ABRA recipe sync "$TEST_RECIPE" --patch
|
||||
assert_success
|
||||
assert_output --partial 'is already set, nothing to do?'
|
||||
}
|
||||
|
||||
@test "sync patch label bump" {
|
||||
run $ABRA recipe upgrade "$TEST_RECIPE" --no-input --patch
|
||||
assert_success
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" diff
|
||||
assert_success
|
||||
assert_output --partial 'image: nginx:1.21.6'
|
||||
|
||||
# NOTE(d1): ensure the latest tag is the one we expect
|
||||
_remove_tags
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag \
|
||||
-a "0.3.0+1.21.0" -m "fake: 0.3.0+1.21.0"
|
||||
assert_success
|
||||
|
||||
run $ABRA recipe sync "$TEST_RECIPE" --no-input --patch
|
||||
assert_success
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" diff
|
||||
assert_success
|
||||
assert_output --regexp 'coop-cloud\.\$\{STACK_NAME\}\.version=0\.3\.1\+1\.2.*'
|
||||
}
|
||||
|
||||
@test "sync minor label bump" {
|
||||
run $ABRA recipe upgrade "$TEST_RECIPE" --no-input --minor
|
||||
assert_success
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" diff
|
||||
assert_success
|
||||
assert_output --regexp 'image: nginx:1.2.*'
|
||||
|
||||
# NOTE(d1): ensure the latest tag is the one we expect
|
||||
_remove_tags
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag \
|
||||
-a "0.3.0+1.21.0" -m "fake: 0.3.0+1.21.0"
|
||||
assert_success
|
||||
|
||||
run $ABRA recipe sync "$TEST_RECIPE" --no-input --minor
|
||||
assert_success
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" diff
|
||||
assert_success
|
||||
assert_output --regexp 'coop-cloud\.\$\{STACK_NAME\}\.version=0\.4\.0\+1\.2.*'
|
||||
}
|
||||
|
||||
@test "error if --no-input and no initial version" {
|
||||
_remove_tags
|
||||
|
||||
run $ABRA recipe sync "$TEST_RECIPE" --no-input --patch
|
||||
assert_failure
|
||||
assert_output --partial 'unable to continue'
|
||||
assert_output --partial 'initial version'
|
||||
}
|
||||
|
||||
@test "output label sync only once" {
|
||||
run $ABRA recipe upgrade "$TEST_RECIPE" --no-input --minor
|
||||
assert_success
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" diff
|
||||
assert_success
|
||||
assert_output --regexp 'image: nginx:1.2.*'
|
||||
|
||||
run $ABRA recipe sync "$TEST_RECIPE" --no-input --minor
|
||||
assert_success
|
||||
assert_line --index 0 --partial 'synced label'
|
||||
refute_line --index 1 --partial 'synced label'
|
||||
}
|
||||
|
||||
@test "sync with no tags or previous release" {
|
||||
_remove_tags
|
||||
|
||||
run $ABRA recipe upgrade "$TEST_RECIPE" --no-input --patch
|
||||
assert_success
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" diff
|
||||
assert_success
|
||||
assert_output --partial 'image: nginx:1.21.6'
|
||||
|
||||
# NOTE(d1): ensure the latest tag is the one we expect
|
||||
_remove_tags
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" tag \
|
||||
-a "0.3.0+1.21.0" -m "fake: 0.3.0+1.21.0"
|
||||
assert_success
|
||||
|
||||
run $ABRA recipe sync "$TEST_RECIPE" --no-input --patch
|
||||
assert_success
|
||||
|
||||
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" diff
|
||||
assert_success
|
||||
assert_output --regexp 'coop-cloud\.\$\{STACK_NAME\}\.version=0\.3\.1\+1\.2.*'
|
||||
}
|
||||
|
||||
@test "sync recipe without input fails with prompt" {
|
||||
run $ABRA recipe new foobar
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/recipes/foobar"
|
||||
|
||||
run $ABRA recipe sync foobar --no-input --patch
|
||||
assert_failure
|
||||
assert_output --partial "input required for initial version"
|
||||
}
|
||||
|
||||
@test "sync new recipe: development release" {
|
||||
run $ABRA recipe new foobar
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/recipes/foobar"
|
||||
|
||||
run bash -c "echo 0.1.0 | $ABRA recipe sync foobar --patch"
|
||||
assert_success
|
||||
assert_output --regexp 'coop-cloud\.\$\{STACK_NAME\}\.version=0\.1\.0\+1\.2.*'
|
||||
}
|
||||
|
||||
@test "sync new recipe: public release" {
|
||||
run $ABRA recipe new foobar
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/recipes/foobar"
|
||||
|
||||
run bash -c "echo 1.0.0 | $ABRA recipe sync foobar --patch"
|
||||
assert_success
|
||||
assert_output --regexp 'coop-cloud\.\$\{STACK_NAME\}\.version=1\.0\.0\+1\.2.*'
|
||||
}
|
||||
|
||||
@test "sync newly created recipe with no version label" {
|
||||
run $ABRA recipe new foobar
|
||||
assert_success
|
||||
assert_exists "$ABRA_DIR/recipes/foobar"
|
||||
|
||||
run sed -i 's/- "coop-cloud.${STACK_NAME}.version="/#- "coop-cloud.${STACK_NAME}.version="/g' \
|
||||
"$ABRA_DIR/recipes/foobar/compose.yml"
|
||||
assert_success
|
||||
|
||||
run bash -c "echo 0.1.0 | $ABRA recipe sync foobar --patch"
|
||||
assert_failure
|
||||
assert_output --partial "automagic insertion not supported yet"
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
|
||||
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"`
|
||||
|
||||
4
vendor/modules.txt
vendored
4
vendor/modules.txt
vendored
@ -163,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
|
||||
@ -213,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