316 lines
7.6 KiB
Go
316 lines
7.6 KiB
Go
package recipe
|
|
|
|
import (
|
|
"fmt"
|
|
"path"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"coopcloud.tech/abra/cli/internal"
|
|
"coopcloud.tech/abra/pkg/config"
|
|
"coopcloud.tech/abra/pkg/recipe"
|
|
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/sirupsen/logrus"
|
|
"github.com/urfave/cli/v2"
|
|
)
|
|
|
|
var Push bool
|
|
var PushFlag = &cli.BoolFlag{
|
|
Name: "push",
|
|
Value: false,
|
|
Destination: &Push,
|
|
}
|
|
|
|
var Dry bool
|
|
var DryFlag = &cli.BoolFlag{
|
|
Name: "dry-run",
|
|
Value: false,
|
|
Aliases: []string{"d"},
|
|
Destination: &Dry,
|
|
}
|
|
|
|
var CommitMessage string
|
|
var CommitMessageFlag = &cli.StringFlag{
|
|
Name: "commit-message",
|
|
Usage: "commit message. Implies --commit",
|
|
Aliases: []string{"cm"},
|
|
Destination: &CommitMessage,
|
|
}
|
|
|
|
var Commit bool
|
|
var CommitFlag = &cli.BoolFlag{
|
|
Name: "commit",
|
|
Usage: "add compose.yml to staging area and commit changes",
|
|
Value: false,
|
|
Aliases: []string{"c"},
|
|
Destination: &Commit,
|
|
}
|
|
|
|
var TagMessage string
|
|
var TagMessageFlag = &cli.StringFlag{
|
|
Name: "tag-comment",
|
|
Usage: "tag comment. If not given, user will be asked for it",
|
|
Aliases: []string{"t", "tm"},
|
|
Destination: &TagMessage,
|
|
}
|
|
|
|
var recipeReleaseCommand = &cli.Command{
|
|
Name: "release",
|
|
Usage: "tag a recipe",
|
|
Aliases: []string{"rl"},
|
|
ArgsUsage: "<recipe> [<tag>]",
|
|
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
|
|
|
|
Where the "a.b.c" part is maintained as 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).
|
|
|
|
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.
|
|
`,
|
|
Flags: []cli.Flag{
|
|
DryFlag,
|
|
PatchFlag,
|
|
MinorFlag,
|
|
MajorFlag,
|
|
PushFlag,
|
|
CommitFlag,
|
|
CommitMessageFlag,
|
|
TagMessageFlag,
|
|
},
|
|
Action: func(c *cli.Context) error {
|
|
recipe := internal.ValidateRecipe(c)
|
|
directory := path.Join(config.APPS_DIR, recipe.Name)
|
|
tagstring := c.Args().Get(1)
|
|
imagesTmp := getImageVersions(recipe)
|
|
mainApp := getMainApp(recipe)
|
|
mainAppVersion := imagesTmp[mainApp]
|
|
|
|
if err := recipePkg.EnsureExists(recipe.Name); err != nil {
|
|
logrus.Fatal(err)
|
|
}
|
|
|
|
if mainAppVersion == "" {
|
|
logrus.Fatal("main app version is empty?")
|
|
}
|
|
|
|
if tagstring != "" {
|
|
if _, err := tagcmp.Parse(tagstring); err != nil {
|
|
logrus.Fatal("invalid tag specified")
|
|
}
|
|
}
|
|
|
|
if TagMessage == "" {
|
|
prompt := &survey.Input{
|
|
Message: "tag message",
|
|
}
|
|
survey.AskOne(prompt, &TagMessage)
|
|
}
|
|
|
|
var createTagOptions git.CreateTagOptions
|
|
createTagOptions.Message = TagMessage
|
|
|
|
if Commit || (CommitMessage != "") {
|
|
commitRepo, err := git.PlainOpen(directory)
|
|
if err != nil {
|
|
logrus.Fatal(err)
|
|
}
|
|
commitWorktree, err := commitRepo.Worktree()
|
|
if err != nil {
|
|
logrus.Fatal(err)
|
|
}
|
|
|
|
if CommitMessage == "" {
|
|
prompt := &survey.Input{
|
|
Message: "commit message",
|
|
}
|
|
survey.AskOne(prompt, &CommitMessage)
|
|
}
|
|
err = commitWorktree.AddGlob("compose.**yml")
|
|
if err != nil {
|
|
logrus.Fatal(err)
|
|
}
|
|
logrus.Debug("staged compose.**yml for commit")
|
|
|
|
_, err = commitWorktree.Commit(CommitMessage, &git.CommitOptions{})
|
|
if err != nil {
|
|
logrus.Fatal(err)
|
|
}
|
|
logrus.Info("changes commited")
|
|
}
|
|
|
|
repo, err := git.PlainOpen(directory)
|
|
if err != nil {
|
|
logrus.Fatal(err)
|
|
}
|
|
head, err := repo.Head()
|
|
if err != nil {
|
|
logrus.Fatal(err)
|
|
}
|
|
|
|
// bumpType is used to decide what part of the tag should be incremented
|
|
bumpType := btoi(Major)*4 + btoi(Minor)*2 + btoi(Patch)
|
|
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.")
|
|
}
|
|
}
|
|
|
|
if tagstring != "" {
|
|
if bumpType > 0 {
|
|
logrus.Warn("user specified a version number and --major/--minor/--patch at the same time! using version number...")
|
|
}
|
|
tag, err := tagcmp.Parse(tagstring)
|
|
if err != nil {
|
|
logrus.Fatal(err)
|
|
}
|
|
if tag.MissingMinor {
|
|
tag.Minor = "0"
|
|
tag.MissingMinor = false
|
|
}
|
|
if tag.MissingPatch {
|
|
tag.Patch = "0"
|
|
tag.MissingPatch = false
|
|
}
|
|
tagstring = fmt.Sprintf("%s+%s", tag.String(), mainAppVersion)
|
|
if Dry {
|
|
logrus.Info(fmt.Sprintf("dry run only: NOT creating tag %s at %s", tagstring, head.Hash()))
|
|
return nil
|
|
}
|
|
|
|
repo.CreateTag(tagstring, head.Hash(), &createTagOptions)
|
|
logrus.Info(fmt.Sprintf("created tag %s at %s", tagstring, head.Hash()))
|
|
if Push {
|
|
if err := repo.Push(&git.PushOptions{}); err != nil {
|
|
logrus.Fatal(err)
|
|
}
|
|
logrus.Info(fmt.Sprintf("pushed tag %s to remote", tagstring))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// get the latest tag with its hash, name etc
|
|
var lastGitTag tagcmp.Tag
|
|
iter, err := repo.Tags()
|
|
if err != nil {
|
|
logrus.Fatal(err)
|
|
}
|
|
if err := iter.ForEach(func(ref *plumbing.Reference) error {
|
|
obj, err := repo.TagObject(ref.Hash())
|
|
if err != nil {
|
|
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 {
|
|
logrus.Fatal(err)
|
|
}
|
|
|
|
fmt.Println(lastGitTag)
|
|
|
|
newTag := lastGitTag
|
|
var newTagString string
|
|
if bumpType > 0 {
|
|
if Patch {
|
|
now, err := strconv.Atoi(newTag.Patch)
|
|
if err != nil {
|
|
logrus.Fatal(err)
|
|
}
|
|
newTag.Patch = strconv.Itoa(now + 1)
|
|
} else if Minor {
|
|
now, err := strconv.Atoi(newTag.Minor)
|
|
if err != nil {
|
|
logrus.Fatal(err)
|
|
}
|
|
newTag.Patch = "0"
|
|
newTag.Minor = strconv.Itoa(now + 1)
|
|
} else if Major {
|
|
now, err := strconv.Atoi(newTag.Major)
|
|
if err != nil {
|
|
logrus.Fatal(err)
|
|
}
|
|
newTag.Patch = "0"
|
|
newTag.Minor = "0"
|
|
newTag.Major = strconv.Itoa(now + 1)
|
|
}
|
|
} else {
|
|
logrus.Fatal("we don't support automatic tag generation yet - specify a version or use one of: --major --minor --patch")
|
|
}
|
|
newTag.Metadata = mainAppVersion
|
|
newTagString = newTag.String()
|
|
if Dry {
|
|
logrus.Info(fmt.Sprintf("dry run only: NOT creating tag %s at %s", newTagString, head.Hash()))
|
|
return nil
|
|
}
|
|
|
|
repo.CreateTag(newTagString, head.Hash(), &createTagOptions)
|
|
logrus.Info(fmt.Sprintf("created tag %s at %s", newTagString, head.Hash()))
|
|
if Push {
|
|
if err := repo.Push(&git.PushOptions{}); err != nil {
|
|
logrus.Fatal(err)
|
|
}
|
|
logrus.Info(fmt.Sprintf("pushed tag %s to remote", newTagString))
|
|
}
|
|
|
|
return nil
|
|
|
|
},
|
|
}
|
|
|
|
func getImageVersions(recipe recipe.Recipe) map[string]string {
|
|
|
|
var services = make(map[string]string)
|
|
for _, service := range recipe.Config.Services {
|
|
if (service.Image == "") {
|
|
continue
|
|
}
|
|
srv := strings.Split(service.Image, ":")
|
|
services[srv[0]] = srv[1]
|
|
}
|
|
|
|
return services
|
|
}
|
|
|
|
func getMainApp(recipe recipe.Recipe) string {
|
|
for _, service := range recipe.Config.Services {
|
|
name := service.Name
|
|
if name == "app" {
|
|
return strings.Split(service.Image, ":")[0]
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func btoi(b bool) int {
|
|
if b {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|