package recipe import ( "fmt" "path" "strconv" "strings" "coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/pkg/config" "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/go-git/go-git/v5/plumbing/object" "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", Aliases: []string{"cm"}, Destination: &CommitMessage, } var Commit bool var CommitFlag = &cli.BoolFlag{ Name: "commit", Value: false, Aliases: []string{"c"}, Destination: &Commit, } var recipeReleaseCommand = &cli.Command{ Name: "release", Usage: "tag a recipe", Aliases: []string{"rl"}, ArgsUsage: " []", 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, }, 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 mainAppVersion == "" { logrus.Fatal("main app version is empty?") } if tagstring != "" { _, err := tagcmp.Parse(tagstring) if err != nil { logrus.Fatal("invalid tag specified") } } 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.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(), nil) /* &git.CreateTagOptions{ Message: tag, })*/ 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 *object.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 { lastGitTag = obj return nil } return err }); err != nil { logrus.Fatal(err) } newTag, err := tagcmp.Parse(lastGitTag.Name) if err != nil { logrus.Fatal(err) } 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.Minor = strconv.Itoa(now + 1) } else if Major { now, err := strconv.Atoi(newTag.Major) if err != nil { logrus.Fatal(err) } newTag.Major = strconv.Itoa(now + 1) } newTagString = newTag.String() } else { logrus.Fatal("we don't support automatic tag generation yet - specify a version or use one of: --major --minor --patch") } newTagString = fmt.Sprintf("%s+%s", newTagString, mainAppVersion) 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(), nil) /* &git.CreateTagOptions{ Message: tag, })*/ 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 { 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 }