diff --git a/cli/internal/recipe.go b/cli/internal/recipe.go new file mode 100644 index 00000000..3e9424e9 --- /dev/null +++ b/cli/internal/recipe.go @@ -0,0 +1,118 @@ +package internal + +import ( + "fmt" + "strings" + + "coopcloud.tech/abra/pkg/recipe" + "github.com/AlecAivazis/survey/v2" + "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" +) + +var Major bool +var MajorFlag = &cli.BoolFlag{ + Name: "major", + Usage: "Increase the major part of the version", + Value: false, + Aliases: []string{"ma", "x"}, + Destination: &Major, +} + +var Minor bool +var MinorFlag = &cli.BoolFlag{ + Name: "minor", + Usage: "Increase the minor part of the version", + Value: false, + Aliases: []string{"mi", "y"}, + Destination: &Minor, +} + +var Patch bool +var PatchFlag = &cli.BoolFlag{ + Name: "patch", + Usage: "Increase the patch part of the version", + Value: false, + Aliases: []string{"p", "z"}, + Destination: &Patch, +} + +var Dry bool +var DryFlag = &cli.BoolFlag{ + Name: "dry-run", + Usage: "No changes are made, only reports changes that would be made", + Value: false, + Aliases: []string{"d"}, + Destination: &Dry, +} + +// PromptBumpType prompts for version bump type +func PromptBumpType(tagString string) error { + if (!Major && !Minor && !Patch) && tagString == "" { + fmt.Printf(` +semver cheat sheet (more via semver.org): + major: new features/bug fixes, backwards incompatible + minor: new features/bug fixes, backwards compatible + patch: bug fixes, backwards compatible + +`) + var chosenBumpType string + prompt := &survey.Select{ + Message: fmt.Sprintf("select recipe version increment type"), + Options: []string{"major", "minor", "patch"}, + } + if err := survey.AskOne(prompt, &chosenBumpType); err != nil { + return err + } + SetBumpType(chosenBumpType) + } + return nil +} + +// GetBumpType figures out which bump type is specified +func GetBumpType() string { + var bumpType string + + if Major { + bumpType = "major" + } else if Minor { + bumpType = "minor" + } else if Patch { + bumpType = "patch" + } else { + logrus.Fatal("no version bump type specififed?") + } + + return bumpType +} + +// SetBumpType figures out which bump type is specified +func SetBumpType(bumpType string) { + if bumpType == "major" { + Major = true + } else if bumpType == "minor" { + Minor = true + } else if bumpType == "patch" { + Patch = true + } else { + logrus.Fatal("no version bump type specififed?") + } +} + +// GetMainApp retrieves the main 'app' image name +func GetMainApp(recipe recipe.Recipe) string { + var app string + + for _, service := range recipe.Config.Services { + name := service.Name + if name == "app" { + app = strings.Split(service.Image, ":")[0] + } + } + + if app == "" { + logrus.Fatalf("%s has no main 'app' service?", recipe.Name) + } + + return app +} diff --git a/cli/recipe/recipe.go b/cli/recipe/recipe.go index 744d5786..328cbe4f 100644 --- a/cli/recipe/recipe.go +++ b/cli/recipe/recipe.go @@ -4,33 +4,6 @@ import ( "github.com/urfave/cli/v2" ) -var Major bool -var MajorFlag = &cli.BoolFlag{ - Name: "major", - Usage: "Increase the major part of the version (new functionality, backwards incompatible, x of x.y.z)", - Value: false, - Aliases: []string{"ma", "x"}, - Destination: &Major, -} - -var Minor bool -var MinorFlag = &cli.BoolFlag{ - Name: "minor", - Usage: "Increase the minor part of the version (new functionality, backwards compatible, y of x.y.z)", - Value: false, - Aliases: []string{"mi", "y"}, - Destination: &Minor, -} - -var Patch bool -var PatchFlag = &cli.BoolFlag{ - Name: "patch", - Usage: "Increase the patch part of the version (bug fixes, backwards compatible, z of x.y.z)", - Value: false, - Aliases: []string{"p", "z"}, - Destination: &Patch, -} - // RecipeCommand defines all recipe related sub-commands. var RecipeCommand = &cli.Command{ Name: "recipe", diff --git a/cli/recipe/release.go b/cli/recipe/release.go index b422f4cb..461797e1 100644 --- a/cli/recipe/release.go +++ b/cli/recipe/release.go @@ -29,15 +29,6 @@ var PushFlag = &cli.BoolFlag{ Destination: &Push, } -var Dry bool -var DryFlag = &cli.BoolFlag{ - Name: "dry-run", - Usage: "No changes are made, only reports changes that would be made", - Value: false, - Aliases: []string{"d"}, - Destination: &Dry, -} - var CommitMessage string var CommitMessageFlag = &cli.StringFlag{ Name: "commit-message", @@ -95,10 +86,10 @@ You may invoke this command in "wizard" mode and be prompted for input: `, Flags: []cli.Flag{ - DryFlag, - MajorFlag, - MinorFlag, - PatchFlag, + internal.DryFlag, + internal.MajorFlag, + internal.MinorFlag, + internal.PatchFlag, PushFlag, CommitFlag, CommitMessageFlag, @@ -108,7 +99,7 @@ You may invoke this command in "wizard" mode and be prompted for input: recipe := internal.ValidateRecipeWithPrompt(c) directory := path.Join(config.APPS_DIR, recipe.Name) tagString := c.Args().Get(1) - mainApp := getMainApp(recipe) + mainApp := internal.GetMainApp(recipe) imagesTmp, err := getImageVersions(recipe) if err != nil { @@ -130,16 +121,16 @@ You may invoke this command in "wizard" mode and be prompted for input: } } - if (!Major && !Minor && !Patch) && tagString != "" { + if (!internal.Major && !internal.Minor && !internal.Patch) && tagString != "" { logrus.Fatal("please specify or bump type (--major/--minor/--patch)") } - if (Major || Minor || Patch) && tagString != "" { + if (internal.Major || internal.Minor || internal.Patch) && tagString != "" { 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 - bumpType := btoi(Major)*4 + btoi(Minor)*2 + btoi(Patch) + 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 { @@ -147,29 +138,14 @@ You may invoke this command in "wizard" mode and be prompted for input: } } - if (!Major && !Minor && !Patch) && tagString == "" { - fmt.Printf(` -semver cheat sheet (more via semver.org): - major: new features/bug fixes, backwards incompatible - minor: new features/bug fixes, backwards compatible - patch: bug fixes, backwards compatible - -`) - var chosenBumpType string - prompt := &survey.Select{ - Message: fmt.Sprintf("select recipe version increment type"), - Options: []string{"major", "minor", "patch"}, - } - if err := survey.AskOne(prompt, &chosenBumpType); err != nil { - logrus.Fatal(err) - } - setBumpType(chosenBumpType) + if err := internal.PromptBumpType(tagString); err != nil { + logrus.Fatal(err) } if TagMessage == "" { prompt := &survey.Input{ Message: "tag message", - Default: fmt.Sprintf("chore: publish new %s version", getBumpType()), + Default: fmt.Sprintf("chore: publish new %s version", internal.GetBumpType()), } if err := survey.AskOne(prompt, &TagMessage); err != nil { logrus.Fatal(err) @@ -210,7 +186,7 @@ semver cheat sheet (more via semver.org): if CommitMessage == "" { prompt := &survey.Input{ Message: "commit message", - Default: fmt.Sprintf("chore: publish new %s version", getBumpType()), + Default: fmt.Sprintf("chore: publish new %s version", internal.GetBumpType()), } if err := survey.AskOne(prompt, &CommitMessage); err != nil { logrus.Fatal(err) @@ -223,7 +199,7 @@ semver cheat sheet (more via semver.org): } logrus.Debug("staged compose.**yml for commit") - if !Dry { + if !internal.Dry { _, err = commitWorktree.Commit(CommitMessage, &git.CommitOptions{}) if err != nil { logrus.Fatal(err) @@ -257,7 +233,7 @@ semver cheat sheet (more via semver.org): tag.MissingPatch = false } tagString = fmt.Sprintf("%s+%s", tag.String(), mainAppVersion) - if Dry { + if internal.Dry { hash := abraFormatter.SmallSHA(head.Hash().String()) logrus.Info(fmt.Sprintf("dry run only: NOT creating tag %s at %s", tagString, hash)) return nil @@ -266,7 +242,7 @@ semver cheat sheet (more via semver.org): repo.CreateTag(tagString, head.Hash(), &createTagOptions) hash := abraFormatter.SmallSHA(head.Hash().String()) logrus.Info(fmt.Sprintf("created tag %s at %s", tagString, hash)) - if Push && !Dry { + if Push && !internal.Dry { if err := repo.Push(&git.PushOptions{}); err != nil { logrus.Fatal(err) } @@ -306,20 +282,20 @@ semver cheat sheet (more via semver.org): newTag := lastGitTag var newtagString string if bumpType > 0 { - if Patch { + if internal.Patch { now, err := strconv.Atoi(newTag.Patch) if err != nil { logrus.Fatal(err) } newTag.Patch = strconv.Itoa(now + 1) - } else if Minor { + } else if internal.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 { + } else if internal.Major { now, err := strconv.Atoi(newTag.Major) if err != nil { logrus.Fatal(err) @@ -332,7 +308,7 @@ semver cheat sheet (more via semver.org): newTag.Metadata = mainAppVersion newtagString = newTag.String() - if Dry { + if internal.Dry { hash := abraFormatter.SmallSHA(head.Hash().String()) logrus.Info(fmt.Sprintf("dry run only: NOT creating tag %s at %s", newtagString, hash)) return nil @@ -341,13 +317,13 @@ semver cheat sheet (more via semver.org): repo.CreateTag(newtagString, head.Hash(), &createTagOptions) hash := abraFormatter.SmallSHA(head.Hash().String()) logrus.Info(fmt.Sprintf("created tag %s at %s", newtagString, hash)) - if Push && !Dry { + if Push && !internal.Dry { if err := repo.Push(&git.PushOptions{}); err != nil { logrus.Fatal(err) } logrus.Info(fmt.Sprintf("pushed tag %s to remote", newtagString)) } else { - logrus.Info("dry run only: NOT pushing changes") + logrus.Info("gry run only: NOT pushing changes") } return nil @@ -387,18 +363,6 @@ func getImageVersions(recipe recipe.Recipe) (map[string]string, error) { return services, nil } -// getMainApp retrieves the main 'app' image name -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 "" -} - // btoi converts a boolean value into an integer func btoi(b bool) int { if b { @@ -407,33 +371,3 @@ func btoi(b bool) int { return 0 } - -// getBumpType figures out which bump type is specified -func getBumpType() string { - var bumpType string - - if Major { - bumpType = "major" - } else if Minor { - bumpType = "minor" - } else if Patch { - bumpType = "patch" - } else { - logrus.Fatal("no version bump type specififed?") - } - - return bumpType -} - -// setBumpType figures out which bump type is specified -func setBumpType(bumpType string) { - if bumpType == "major" { - Major = true - } else if bumpType == "minor" { - Minor = true - } else if bumpType == "patch" { - Patch = true - } else { - logrus.Fatal("no version bump type specififed?") - } -} diff --git a/cli/recipe/sync.go b/cli/recipe/sync.go index d1e237ab..3f1a03df 100644 --- a/cli/recipe/sync.go +++ b/cli/recipe/sync.go @@ -1,12 +1,17 @@ package recipe import ( - "errors" "fmt" + "path" + "strconv" "coopcloud.tech/abra/cli/internal" "coopcloud.tech/abra/pkg/catalogue" + "coopcloud.tech/abra/pkg/config" + "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" ) @@ -16,17 +21,166 @@ var recipeSyncCommand = &cli.Command{ Usage: "Ensure recipe version labels are up-to-date", Aliases: []string{"s"}, ArgsUsage: " []", + Flags: []cli.Flag{ + internal.DryFlag, + internal.MajorFlag, + internal.MinorFlag, + internal.PatchFlag, + }, Description: ` This command will generate labels for the main recipe service (i.e. by -convention, typically the service named "app") which corresponds to the -following format: +convention, the service named "app") which corresponds to the following format: coop-cloud.${STACK_NAME}.version= The is determined by the recipe maintainer and is specified on the command-line. The configuration will be updated on the local file system. + +You may invoke this command in "wizard" mode and be prompted for input: + + abra recipe sync gitea + `, + Action: func(c *cli.Context) error { + recipe := internal.ValidateRecipeWithPrompt(c) + + mainApp := internal.GetMainApp(recipe) + + imagesTmp, err := getImageVersions(recipe) + if err != nil { + logrus.Fatal(err) + } + mainAppVersion := imagesTmp[mainApp] + + tags, err := recipe.Tags() + if err != nil { + logrus.Fatal(err) + } + + nextTag := c.Args().Get(1) + if len(tags) == 0 && nextTag == "" { + logrus.Warnf("no tags found for %s", recipe.Name) + var chosenVersion string + edPrompt := &survey.Select{ + Message: "which version do you want to begin with?", + Options: []string{"0.1.0", "1.0.0"}, + } + if err := survey.AskOne(edPrompt, &chosenVersion); err != nil { + logrus.Fatal(err) + } + nextTag = fmt.Sprintf("%s+%s", chosenVersion, mainAppVersion) + } + + if nextTag == "" && (!internal.Major && !internal.Minor && !internal.Patch) { + if err := internal.PromptBumpType(""); err != nil { + logrus.Fatal(err) + } + } + + if nextTag == "" { + recipeDir := path.Join(config.APPS_DIR, recipe.Name) + repo, err := git.PlainOpen(recipeDir) + if err != nil { + logrus.Fatal(err) + } + 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) + } + + // 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 { + logrus.Fatal("you can only use one of: --major, --minor, --patch.") + } + } + + newTag := lastGitTag + if bumpType > 0 { + if internal.Patch { + now, err := strconv.Atoi(newTag.Patch) + if err != nil { + logrus.Fatal(err) + } + newTag.Patch = strconv.Itoa(now + 1) + } else if internal.Minor { + now, err := strconv.Atoi(newTag.Minor) + if err != nil { + logrus.Fatal(err) + } + newTag.Patch = "0" + newTag.Minor = strconv.Itoa(now + 1) + } else if internal.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) + } + } + + newTag.Metadata = mainAppVersion + logrus.Debugf("choosing %s as new version for %s", newTag.String(), recipe.Name) + nextTag = newTag.String() + } + + if _, err := tagcmp.Parse(nextTag); err != nil { + logrus.Fatalf("invalid version %s specified", nextTag) + } + + mainService := "app" + var services []string + hasAppService := false + for _, service := range recipe.Config.Services { + services = append(services, service.Name) + if service.Name == "app" { + hasAppService = true + logrus.Debugf("detected app service in %s", recipe.Name) + } + } + + if !hasAppService { + logrus.Fatalf("%s has no main 'app' service?", recipe.Name) + } + + logrus.Debugf("selecting %s as the service to sync version label", mainService) + + label := fmt.Sprintf("coop-cloud.${STACK_NAME}.version=%s", nextTag) + if !internal.Dry { + if err := recipe.UpdateLabel(mainService, label); err != nil { + logrus.Fatal(err) + } + logrus.Infof("synced label '%s' to service '%s'", label, mainService) + } else { + logrus.Infof("dry run only: NOT syncing label %s for recipe %s", nextTag, recipe.Name) + } + + return nil + }, BashComplete: func(c *cli.Context) { catl, err := catalogue.ReadRecipeCatalogue() if err != nil { @@ -39,50 +193,4 @@ system. fmt.Println(name) } }, - Action: func(c *cli.Context) error { - if c.Args().Len() != 2 { - internal.ShowSubcommandHelpAndError(c, errors.New("missing / arguments?")) - } - - recipe := internal.ValidateRecipe(c) - - // TODO: validate with tagcmp when new commits come in - // See https://git.coopcloud.tech/coop-cloud/abra/pulls/109 - nextTag := c.Args().Get(1) - - mainService := "app" - var services []string - hasAppService := false - for _, service := range recipe.Config.Services { - services = append(services, service.Name) - if service.Name == "app" { - hasAppService = true - logrus.Debugf("detected app service in '%s'", recipe.Name) - } - } - - if !hasAppService { - logrus.Warnf("no 'app' service defined in '%s'", recipe.Name) - var chosenService string - prompt := &survey.Select{ - Message: fmt.Sprintf("what is the main service name for '%s'?", recipe.Name), - Options: services, - } - if err := survey.AskOne(prompt, &chosenService); err != nil { - logrus.Fatal(err) - } - mainService = chosenService - } - - logrus.Debugf("selecting '%s' as the service to sync version labels", mainService) - - label := fmt.Sprintf("coop-cloud.${STACK_NAME}.version=%s", nextTag) - if err := recipe.UpdateLabel(mainService, label); err != nil { - logrus.Fatal(err) - } - - logrus.Infof("synced label '%s' to service '%s'", label, mainService) - - return nil - }, } diff --git a/cli/recipe/upgrade.go b/cli/recipe/upgrade.go index fa536520..aaf93e54 100644 --- a/cli/recipe/upgrade.go +++ b/cli/recipe/upgrade.go @@ -39,14 +39,14 @@ is up to the end-user to decide. `, ArgsUsage: "", Flags: []cli.Flag{ - PatchFlag, - MinorFlag, - MajorFlag, + internal.PatchFlag, + internal.MinorFlag, + internal.MajorFlag, }, Action: func(c *cli.Context) error { recipe := internal.ValidateRecipe(c) - bumpType := btoi(Major)*4 + btoi(Minor)*2 + btoi(Patch) + 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 { @@ -179,11 +179,11 @@ is up to the end-user to decide. if contains { logrus.Infof("Upgrading service %s from %s to %s (pinned tag: %s)", service.Name, tag.String(), upgradeTag, pinnedTagString) } else { - logrus.Infof("service %s, image %s pinned to %s. No compatible upgrade found", service.Name, servicePins[service.Name].image, pinnedTagString) + logrus.Infof("service %s, image %s pinned to %s, no compatible upgrade found", service.Name, servicePins[service.Name].image, pinnedTagString) continue } } else { - logrus.Fatalf("Service %s is at version %s, but pinned to %s. Please correct your compose.yml file manually!", service.Name, tag.String(), pinnedTag.String()) + logrus.Fatalf("Service %s is at version %s, but pinned to %s, please correct your compose.yml file manually!", service.Name, tag.String(), pinnedTag.String()) continue } } else { diff --git a/pkg/compose/compose.go b/pkg/compose/compose.go index dda1b96f..1fa11d16 100644 --- a/pkg/compose/compose.go +++ b/pkg/compose/compose.go @@ -117,8 +117,11 @@ func UpdateLabel(pattern, serviceName, label, recipeName string) error { continue } + discovered := false for oldLabel, value := range service.Deploy.Labels { if strings.HasPrefix(oldLabel, "coop-cloud") { + discovered = true + bytes, err := ioutil.ReadFile(composeFile) if err != nil { return err @@ -127,13 +130,19 @@ func UpdateLabel(pattern, serviceName, label, recipeName string) error { old := fmt.Sprintf("coop-cloud.${STACK_NAME}.version=%s", value) replacedBytes := strings.Replace(string(bytes), old, label, -1) - logrus.Debugf("updating '%s' to '%s' in '%s'", old, label, compose.Filename) + logrus.Debugf("updating %s to %s in %s", old, label, compose.Filename) if err := ioutil.WriteFile(compose.Filename, []byte(replacedBytes), 0644); err != nil { return err } } } + + if !discovered { + logrus.Warn("no existing label found, cannot continue...") + logrus.Fatalf("add '%s' manually, automagic insertion not supported yet", label) + } + } return nil