2021-09-22 14:03:56 +00:00
|
|
|
package recipe
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"path"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
|
2021-11-06 21:36:01 +00:00
|
|
|
abraFormatter "coopcloud.tech/abra/cli/formatter"
|
2021-09-22 14:03:56 +00:00
|
|
|
"coopcloud.tech/abra/cli/internal"
|
2021-12-21 01:04:31 +00:00
|
|
|
"coopcloud.tech/abra/pkg/autocomplete"
|
2021-09-22 14:03:56 +00:00
|
|
|
"coopcloud.tech/abra/pkg/config"
|
2021-12-19 22:50:15 +00:00
|
|
|
gitPkg "coopcloud.tech/abra/pkg/git"
|
2021-09-22 14:03:56 +00:00
|
|
|
"coopcloud.tech/abra/pkg/recipe"
|
2021-10-10 22:34:23 +00:00
|
|
|
recipePkg "coopcloud.tech/abra/pkg/recipe"
|
2021-09-23 16:27:19 +00:00
|
|
|
"coopcloud.tech/tagcmp"
|
2021-09-29 14:06:43 +00:00
|
|
|
"github.com/AlecAivazis/survey/v2"
|
2021-11-06 21:36:01 +00:00
|
|
|
"github.com/docker/distribution/reference"
|
2021-09-22 14:03:56 +00:00
|
|
|
"github.com/go-git/go-git/v5"
|
2021-10-11 22:56:52 +00:00
|
|
|
"github.com/go-git/go-git/v5/plumbing"
|
2021-09-22 14:03:56 +00:00
|
|
|
"github.com/sirupsen/logrus"
|
|
|
|
"github.com/urfave/cli/v2"
|
|
|
|
)
|
|
|
|
|
|
|
|
var recipeReleaseCommand = &cli.Command{
|
|
|
|
Name: "release",
|
2021-11-06 21:38:29 +00:00
|
|
|
Usage: "Release a new recipe version",
|
2021-09-22 14:03:56 +00:00
|
|
|
Aliases: []string{"rl"},
|
2021-11-06 21:38:29 +00:00
|
|
|
ArgsUsage: "<recipe> [<version>]",
|
2021-09-29 20:36:43 +00:00
|
|
|
Description: `
|
|
|
|
This command is used to specify a new tag for a recipe. These tags are used to
|
|
|
|
identify different versions of the recipe and are published on the Co-op Cloud
|
|
|
|
recipe catalogue.
|
|
|
|
|
|
|
|
These tags take the following form:
|
|
|
|
|
|
|
|
a.b.c+x.y.z
|
|
|
|
|
2021-12-21 01:04:40 +00:00
|
|
|
Where the "a.b.c" part is a semantic version of the recipe by the recipe
|
|
|
|
maintainer. And the "x.y.z" part is the image tag of the recipe "app" service
|
|
|
|
(the main container which contains the software to be used).
|
2021-09-29 20:36:43 +00:00
|
|
|
|
|
|
|
We maintain a semantic versioning scheme ("a.b.c") alongside the libre app
|
|
|
|
versioning scheme in order to maximise the chances that the nature of recipe
|
|
|
|
updates are properly communicated.
|
|
|
|
|
|
|
|
Abra does its best to read the "a.b.c" version scheme and communicate what
|
|
|
|
action needs to be taken when performing different operations such as an update
|
|
|
|
or a rollback of an app.
|
2021-11-06 21:36:01 +00:00
|
|
|
|
|
|
|
You may invoke this command in "wizard" mode and be prompted for input:
|
|
|
|
|
|
|
|
abra recipe release gitea
|
|
|
|
|
2021-09-29 20:36:43 +00:00
|
|
|
`,
|
2021-09-22 14:03:56 +00:00
|
|
|
Flags: []cli.Flag{
|
2021-11-06 22:40:22 +00:00
|
|
|
internal.DryFlag,
|
|
|
|
internal.MajorFlag,
|
|
|
|
internal.MinorFlag,
|
|
|
|
internal.PatchFlag,
|
2021-11-06 23:03:01 +00:00
|
|
|
internal.PushFlag,
|
|
|
|
internal.CommitFlag,
|
|
|
|
internal.CommitMessageFlag,
|
|
|
|
internal.TagMessageFlag,
|
2021-09-22 14:03:56 +00:00
|
|
|
},
|
2021-12-21 01:04:31 +00:00
|
|
|
BashComplete: autocomplete.RecipeNameComplete,
|
2021-09-22 14:03:56 +00:00
|
|
|
Action: func(c *cli.Context) error {
|
2021-11-06 21:36:01 +00:00
|
|
|
recipe := internal.ValidateRecipeWithPrompt(c)
|
2021-09-22 14:03:56 +00:00
|
|
|
directory := path.Join(config.APPS_DIR, recipe.Name)
|
2021-11-06 21:36:01 +00:00
|
|
|
tagString := c.Args().Get(1)
|
2021-11-06 22:40:22 +00:00
|
|
|
mainApp := internal.GetMainApp(recipe)
|
2021-11-06 21:36:01 +00:00
|
|
|
|
|
|
|
imagesTmp, err := getImageVersions(recipe)
|
|
|
|
if err != nil {
|
|
|
|
logrus.Fatal(err)
|
|
|
|
}
|
2021-09-24 08:48:09 +00:00
|
|
|
mainAppVersion := imagesTmp[mainApp]
|
2021-10-10 22:34:23 +00:00
|
|
|
|
|
|
|
if err := recipePkg.EnsureExists(recipe.Name); err != nil {
|
|
|
|
logrus.Fatal(err)
|
|
|
|
}
|
|
|
|
|
2021-09-24 08:48:09 +00:00
|
|
|
if mainAppVersion == "" {
|
2021-11-06 21:36:01 +00:00
|
|
|
logrus.Fatalf("main 'app' service version for %s is empty?", recipe.Name)
|
2021-09-24 08:48:09 +00:00
|
|
|
}
|
2021-09-23 16:27:19 +00:00
|
|
|
|
2021-11-06 21:36:01 +00:00
|
|
|
if tagString != "" {
|
|
|
|
if _, err := tagcmp.Parse(tagString); err != nil {
|
2021-09-29 14:25:39 +00:00
|
|
|
logrus.Fatal("invalid tag specified")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-06 22:40:22 +00:00
|
|
|
if (!internal.Major && !internal.Minor && !internal.Patch) && tagString != "" {
|
2021-11-06 21:38:29 +00:00
|
|
|
logrus.Fatal("please specify <version> or bump type (--major/--minor/--patch)")
|
2021-11-06 21:36:01 +00:00
|
|
|
}
|
|
|
|
|
2021-11-06 22:40:22 +00:00
|
|
|
if (internal.Major || internal.Minor || internal.Patch) && tagString != "" {
|
2021-11-06 21:36:01 +00:00
|
|
|
logrus.Fatal("cannot specify tag and bump type at the same time")
|
|
|
|
}
|
|
|
|
|
|
|
|
// bumpType is used to decide what part of the tag should be incremented
|
2021-11-06 22:40:22 +00:00
|
|
|
bumpType := btoi(internal.Major)*4 + btoi(internal.Minor)*2 + btoi(internal.Patch)
|
2021-11-06 21:36:01 +00:00
|
|
|
if bumpType != 0 {
|
|
|
|
// a bitwise check if the number is a power of 2
|
|
|
|
if (bumpType & (bumpType - 1)) != 0 {
|
|
|
|
logrus.Fatal("you can only use one of: --major, --minor, --patch.")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-06 22:40:22 +00:00
|
|
|
if err := internal.PromptBumpType(tagString); err != nil {
|
|
|
|
logrus.Fatal(err)
|
2021-11-06 21:36:01 +00:00
|
|
|
}
|
|
|
|
|
2021-11-06 23:03:01 +00:00
|
|
|
if internal.TagMessage == "" {
|
2021-10-07 12:52:48 +00:00
|
|
|
prompt := &survey.Input{
|
|
|
|
Message: "tag message",
|
2021-11-06 22:40:22 +00:00
|
|
|
Default: fmt.Sprintf("chore: publish new %s version", internal.GetBumpType()),
|
2021-10-07 12:52:48 +00:00
|
|
|
}
|
2021-11-06 23:03:01 +00:00
|
|
|
if err := survey.AskOne(prompt, &internal.TagMessage); err != nil {
|
2021-10-18 18:35:32 +00:00
|
|
|
logrus.Fatal(err)
|
|
|
|
}
|
2021-10-07 12:52:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var createTagOptions git.CreateTagOptions
|
2021-11-06 23:03:01 +00:00
|
|
|
createTagOptions.Message = internal.TagMessage
|
2021-10-07 12:52:48 +00:00
|
|
|
|
2021-12-19 22:36:03 +00:00
|
|
|
if !internal.Commit && !internal.NoInput {
|
2021-11-06 21:36:01 +00:00
|
|
|
prompt := &survey.Confirm{
|
|
|
|
Message: "git commit changes also?",
|
|
|
|
}
|
2021-11-06 23:03:01 +00:00
|
|
|
if err := survey.AskOne(prompt, &internal.Commit); err != nil {
|
2021-11-06 21:36:01 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-19 22:36:03 +00:00
|
|
|
if !internal.Push && !internal.NoInput {
|
2021-11-06 21:36:01 +00:00
|
|
|
prompt := &survey.Confirm{
|
|
|
|
Message: "git push changes also?",
|
|
|
|
}
|
2021-11-06 23:03:01 +00:00
|
|
|
if err := survey.AskOne(prompt, &internal.Push); err != nil {
|
2021-11-06 21:36:01 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-06 23:03:01 +00:00
|
|
|
if internal.Commit || internal.CommitMessage != "" {
|
2021-12-19 22:36:03 +00:00
|
|
|
if internal.CommitMessage == "" && !internal.NoInput {
|
2021-09-29 14:06:43 +00:00
|
|
|
prompt := &survey.Input{
|
|
|
|
Message: "commit message",
|
2021-11-06 22:40:22 +00:00
|
|
|
Default: fmt.Sprintf("chore: publish new %s version", internal.GetBumpType()),
|
2021-09-29 14:06:43 +00:00
|
|
|
}
|
2021-11-06 23:03:01 +00:00
|
|
|
if err := survey.AskOne(prompt, &internal.CommitMessage); err != nil {
|
2021-10-18 18:35:32 +00:00
|
|
|
logrus.Fatal(err)
|
|
|
|
}
|
2021-09-29 14:06:43 +00:00
|
|
|
}
|
2021-11-06 21:36:01 +00:00
|
|
|
|
2021-12-19 22:50:15 +00:00
|
|
|
if internal.CommitMessage == "" {
|
|
|
|
logrus.Fatal("no commit message specified?")
|
2021-10-05 14:06:17 +00:00
|
|
|
}
|
|
|
|
|
2021-12-19 22:59:40 +00:00
|
|
|
if err := gitPkg.Commit("compose.**yml", internal.CommitMessage, internal.Dry, false); err != nil {
|
2021-12-19 22:50:15 +00:00
|
|
|
logrus.Fatal(err)
|
2021-09-29 14:06:43 +00:00
|
|
|
}
|
2021-12-19 22:50:15 +00:00
|
|
|
|
2021-09-29 14:06:43 +00:00
|
|
|
}
|
|
|
|
|
2021-09-23 16:27:19 +00:00
|
|
|
repo, err := git.PlainOpen(directory)
|
|
|
|
if err != nil {
|
|
|
|
logrus.Fatal(err)
|
|
|
|
}
|
|
|
|
head, err := repo.Head()
|
|
|
|
if err != nil {
|
|
|
|
logrus.Fatal(err)
|
|
|
|
}
|
|
|
|
|
2021-11-06 21:36:01 +00:00
|
|
|
if tagString != "" {
|
|
|
|
tag, err := tagcmp.Parse(tagString)
|
2021-09-29 14:25:39 +00:00
|
|
|
if err != nil {
|
|
|
|
logrus.Fatal(err)
|
|
|
|
}
|
|
|
|
if tag.MissingMinor {
|
|
|
|
tag.Minor = "0"
|
|
|
|
tag.MissingMinor = false
|
2021-09-24 08:48:09 +00:00
|
|
|
}
|
2021-09-29 14:25:39 +00:00
|
|
|
if tag.MissingPatch {
|
|
|
|
tag.Patch = "0"
|
|
|
|
tag.MissingPatch = false
|
|
|
|
}
|
2021-11-06 21:36:01 +00:00
|
|
|
tagString = fmt.Sprintf("%s+%s", tag.String(), mainAppVersion)
|
2021-11-06 22:40:22 +00:00
|
|
|
if internal.Dry {
|
2021-11-06 21:36:01 +00:00
|
|
|
hash := abraFormatter.SmallSHA(head.Hash().String())
|
2021-12-19 22:50:29 +00:00
|
|
|
logrus.Info(fmt.Sprintf("dry run: not creating tag %s at %s", tagString, hash))
|
2021-09-23 16:27:19 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-11-06 21:36:01 +00:00
|
|
|
repo.CreateTag(tagString, head.Hash(), &createTagOptions)
|
|
|
|
hash := abraFormatter.SmallSHA(head.Hash().String())
|
|
|
|
logrus.Info(fmt.Sprintf("created tag %s at %s", tagString, hash))
|
2021-11-06 23:03:01 +00:00
|
|
|
if internal.Push && !internal.Dry {
|
2021-09-23 16:52:21 +00:00
|
|
|
if err := repo.Push(&git.PushOptions{}); err != nil {
|
|
|
|
logrus.Fatal(err)
|
|
|
|
}
|
2021-11-06 21:36:01 +00:00
|
|
|
logrus.Info(fmt.Sprintf("pushed tag %s to remote", tagString))
|
|
|
|
} else {
|
2021-12-19 22:50:29 +00:00
|
|
|
logrus.Info("dry run: no changes pushed")
|
2021-09-23 16:52:21 +00:00
|
|
|
}
|
2021-09-23 16:27:19 +00:00
|
|
|
|
2021-09-22 14:03:56 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-09-23 16:27:19 +00:00
|
|
|
// get the latest tag with its hash, name etc
|
2021-10-07 12:52:48 +00:00
|
|
|
var lastGitTag tagcmp.Tag
|
2021-10-11 22:56:52 +00:00
|
|
|
iter, err := repo.Tags()
|
2021-09-22 14:03:56 +00:00
|
|
|
if err != nil {
|
|
|
|
logrus.Fatal(err)
|
|
|
|
}
|
2021-10-11 22:56:52 +00:00
|
|
|
if err := iter.ForEach(func(ref *plumbing.Reference) error {
|
|
|
|
obj, err := repo.TagObject(ref.Hash())
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2021-10-07 12:52:48 +00:00
|
|
|
tagcmpTag, err := tagcmp.Parse(obj.Name)
|
|
|
|
if err != nil {
|
2021-10-11 22:56:52 +00:00
|
|
|
return err
|
2021-10-07 12:52:48 +00:00
|
|
|
}
|
|
|
|
if (lastGitTag == tagcmp.Tag{}) {
|
|
|
|
lastGitTag = tagcmpTag
|
|
|
|
} else if tagcmpTag.IsGreaterThan(lastGitTag) {
|
|
|
|
lastGitTag = tagcmpTag
|
2021-09-23 16:27:19 +00:00
|
|
|
}
|
2021-10-07 12:52:48 +00:00
|
|
|
return nil
|
2021-09-23 16:27:19 +00:00
|
|
|
}); err != nil {
|
|
|
|
logrus.Fatal(err)
|
|
|
|
}
|
|
|
|
|
2021-10-07 12:52:48 +00:00
|
|
|
newTag := lastGitTag
|
2021-11-06 21:36:01 +00:00
|
|
|
var newtagString string
|
2021-09-23 16:27:19 +00:00
|
|
|
if bumpType > 0 {
|
2021-11-06 22:40:22 +00:00
|
|
|
if internal.Patch {
|
2021-09-23 16:27:19 +00:00
|
|
|
now, err := strconv.Atoi(newTag.Patch)
|
|
|
|
if err != nil {
|
|
|
|
logrus.Fatal(err)
|
|
|
|
}
|
|
|
|
newTag.Patch = strconv.Itoa(now + 1)
|
2021-11-06 22:40:22 +00:00
|
|
|
} else if internal.Minor {
|
2021-09-23 16:27:19 +00:00
|
|
|
now, err := strconv.Atoi(newTag.Minor)
|
|
|
|
if err != nil {
|
|
|
|
logrus.Fatal(err)
|
|
|
|
}
|
2021-10-05 14:06:17 +00:00
|
|
|
newTag.Patch = "0"
|
2021-09-23 16:27:19 +00:00
|
|
|
newTag.Minor = strconv.Itoa(now + 1)
|
2021-11-06 22:40:22 +00:00
|
|
|
} else if internal.Major {
|
2021-09-23 16:27:19 +00:00
|
|
|
now, err := strconv.Atoi(newTag.Major)
|
|
|
|
if err != nil {
|
|
|
|
logrus.Fatal(err)
|
|
|
|
}
|
2021-10-05 14:06:17 +00:00
|
|
|
newTag.Patch = "0"
|
|
|
|
newTag.Minor = "0"
|
2021-09-23 16:27:19 +00:00
|
|
|
newTag.Major = strconv.Itoa(now + 1)
|
2021-09-22 14:03:56 +00:00
|
|
|
}
|
|
|
|
}
|
2021-11-06 21:36:01 +00:00
|
|
|
|
2021-10-07 12:52:48 +00:00
|
|
|
newTag.Metadata = mainAppVersion
|
2021-11-06 21:36:01 +00:00
|
|
|
newtagString = newTag.String()
|
2021-11-06 22:40:22 +00:00
|
|
|
if internal.Dry {
|
2021-11-06 21:36:01 +00:00
|
|
|
hash := abraFormatter.SmallSHA(head.Hash().String())
|
2021-12-19 22:50:29 +00:00
|
|
|
logrus.Info(fmt.Sprintf("dry run: not creating tag %s at %s", newtagString, hash))
|
2021-09-22 14:03:56 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2021-11-06 21:36:01 +00:00
|
|
|
repo.CreateTag(newtagString, head.Hash(), &createTagOptions)
|
|
|
|
hash := abraFormatter.SmallSHA(head.Hash().String())
|
|
|
|
logrus.Info(fmt.Sprintf("created tag %s at %s", newtagString, hash))
|
2021-11-06 23:03:01 +00:00
|
|
|
if internal.Push && !internal.Dry {
|
2021-09-23 16:52:21 +00:00
|
|
|
if err := repo.Push(&git.PushOptions{}); err != nil {
|
|
|
|
logrus.Fatal(err)
|
|
|
|
}
|
2021-11-06 21:36:01 +00:00
|
|
|
logrus.Info(fmt.Sprintf("pushed tag %s to remote", newtagString))
|
|
|
|
} else {
|
2021-12-19 22:50:29 +00:00
|
|
|
logrus.Info("dry run: no changes pushed")
|
2021-09-23 16:52:21 +00:00
|
|
|
}
|
2021-09-22 14:03:56 +00:00
|
|
|
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2021-11-06 21:36:01 +00:00
|
|
|
// getImageVersions retrieves image versions for a recipe
|
|
|
|
func getImageVersions(recipe recipe.Recipe) (map[string]string, error) {
|
2021-09-22 14:03:56 +00:00
|
|
|
var services = make(map[string]string)
|
2021-11-06 21:36:01 +00:00
|
|
|
|
2021-09-22 14:03:56 +00:00
|
|
|
for _, service := range recipe.Config.Services {
|
2021-10-18 06:27:39 +00:00
|
|
|
if service.Image == "" {
|
2021-10-16 16:56:45 +00:00
|
|
|
continue
|
|
|
|
}
|
2021-11-06 21:36:01 +00:00
|
|
|
|
|
|
|
img, err := reference.ParseNormalizedNamed(service.Image)
|
|
|
|
if err != nil {
|
|
|
|
return services, err
|
|
|
|
}
|
|
|
|
|
|
|
|
path := reference.Path(img)
|
|
|
|
if strings.Contains(path, "library") {
|
|
|
|
path = strings.Split(path, "/")[1]
|
|
|
|
}
|
|
|
|
|
|
|
|
var tag string
|
|
|
|
switch img.(type) {
|
|
|
|
case reference.NamedTagged:
|
|
|
|
tag = img.(reference.NamedTagged).Tag()
|
|
|
|
case reference.Named:
|
2021-12-21 00:47:50 +00:00
|
|
|
return services, fmt.Errorf("%s service is missing image tag?", path)
|
2021-11-06 21:36:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
services[path] = tag
|
2021-09-22 14:03:56 +00:00
|
|
|
}
|
|
|
|
|
2021-11-06 21:36:01 +00:00
|
|
|
return services, nil
|
2021-09-22 14:03:56 +00:00
|
|
|
}
|
|
|
|
|
2021-11-06 21:36:01 +00:00
|
|
|
// btoi converts a boolean value into an integer
|
2021-09-23 16:27:19 +00:00
|
|
|
func btoi(b bool) int {
|
|
|
|
if b {
|
|
|
|
return 1
|
2021-09-22 14:03:56 +00:00
|
|
|
}
|
2021-11-06 21:36:01 +00:00
|
|
|
|
2021-09-23 16:27:19 +00:00
|
|
|
return 0
|
2021-09-22 14:03:56 +00:00
|
|
|
}
|