feat: translation support
All checks were successful
continuous-integration/drone/push Build is passing

See #483
This commit is contained in:
2025-08-19 11:22:52 +02:00
parent 5cf6048ecb
commit 4e205cf13e
108 changed files with 11217 additions and 1645 deletions

View File

@ -4,15 +4,16 @@ import (
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"github.com/spf13/cobra"
)
var RecipeDiffCommand = &cobra.Command{
Use: "diff <recipe> [flags]",
Aliases: []string{"d"},
Short: "Show unstaged changes in recipe config",
Long: "This command requires /usr/bin/git.",
Use: i18n.G("diff <recipe> [flags]"),
Aliases: []string{i18n.G("d")},
Short: i18n.G("Show unstaged changes in recipe config"),
Long: i18n.G("This command requires /usr/bin/git."),
Args: cobra.MinimumNArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,

View File

@ -6,6 +6,7 @@ import (
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"github.com/go-git/go-git/v5"
@ -14,19 +15,19 @@ import (
)
var RecipeFetchCommand = &cobra.Command{
Use: "fetch [recipe | --all] [flags]",
Aliases: []string{"f"},
Short: "Clone recipe(s) locally",
Long: `Using "--force/-f" Git syncs an existing recipe. It does not erase unstaged changes.`,
Use: i18n.G("fetch [recipe | --all] [flags]"),
Aliases: []string{i18n.G("f")},
Short: i18n.G("Clone recipe(s) locally"),
Long: i18n.G(`Using "--force/-f" Git syncs an existing recipe. It does not erase unstaged changes.`),
Args: cobra.RangeArgs(0, 1),
Example: ` # fetch from recipe catalogue
Example: i18n.G(` # fetch from recipe catalogue
abra recipe fetch gitea
# fetch from remote recipe
abra recipe fetch git.foo.org/recipes/myrecipe
# fetch with ssh remote for hacking
abra recipe fetch gitea --ssh`,
abra recipe fetch gitea --ssh`),
ValidArgsFunction: func(
cmd *cobra.Command,
args []string,
@ -40,18 +41,18 @@ var RecipeFetchCommand = &cobra.Command{
}
if recipeName == "" && !fetchAllRecipes {
log.Fatal("missing [recipe] or --all/-a")
log.Fatal(i18n.G("missing [recipe] or --all/-a"))
}
if recipeName != "" && fetchAllRecipes {
log.Fatal("cannot use [recipe] and --all/-a together")
log.Fatal(i18n.G("cannot use [recipe] and --all/-a together"))
}
if recipeName != "" {
r := recipe.Get(recipeName)
if _, err := os.Stat(r.Dir); !os.IsNotExist(err) {
if !force {
log.Warnf("%s is already fetched", r.Name)
log.Warn(i18n.G("%s is already fetched", r.Name))
return
}
}
@ -60,24 +61,24 @@ var RecipeFetchCommand = &cobra.Command{
if sshRemote {
if r.SSHURL == "" {
log.Warnf("unable to discover SSH remote for %s", r.Name)
log.Warn(i18n.G("unable to discover SSH remote for %s", r.Name))
return
}
repo, err := git.PlainOpen(r.Dir)
if err != nil {
log.Fatalf("unable to open %s: %s", r.Dir, err)
log.Fatal(i18n.G("unable to open %s: %s", r.Dir, err))
}
if err = repo.DeleteRemote("origin"); err != nil {
log.Fatalf("unable to remove default remote in %s: %s", r.Dir, err)
log.Fatal(i18n.G("unable to remove default remote in %s: %s", r.Dir, err))
}
if _, err := repo.CreateRemote(&gitCfg.RemoteConfig{
Name: "origin",
URLs: []string{r.SSHURL},
}); err != nil {
log.Fatalf("unable to set SSH remote in %s: %s", r.Dir, err)
log.Fatal(i18n.G("unable to set SSH remote in %s: %s", r.Dir, err))
}
}
@ -89,7 +90,7 @@ var RecipeFetchCommand = &cobra.Command{
log.Fatal(err)
}
catlBar := formatter.CreateProgressbar(len(catalogue), "fetching latest recipes...")
catlBar := formatter.CreateProgressbar(len(catalogue), i18n.G("fetching latest recipes..."))
ensureCtx := internal.GetEnsureContext()
for recipeName := range catalogue {
r := recipe.Get(recipeName)
@ -110,25 +111,25 @@ var (
func init() {
RecipeFetchCommand.Flags().BoolVarP(
&fetchAllRecipes,
"all",
"a",
i18n.G("all"),
i18n.G("a"),
false,
"fetch all recipes",
i18n.G("fetch all recipes"),
)
RecipeFetchCommand.Flags().BoolVarP(
&sshRemote,
"ssh",
"s",
i18n.G("ssh"),
i18n.G("s"),
false,
"automatically set ssh remote",
i18n.G("automatically set ssh remote"),
)
RecipeFetchCommand.Flags().BoolVarP(
&force,
"force",
"f",
i18n.G("force"),
i18n.G("f"),
false,
"force re-fetch",
i18n.G("force re-fetch"),
)
}

View File

@ -4,15 +4,16 @@ import (
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/lint"
"coopcloud.tech/abra/pkg/log"
"github.com/spf13/cobra"
)
var RecipeLintCommand = &cobra.Command{
Use: "lint <recipe> [flags]",
Short: "Lint a recipe",
Aliases: []string{"l"},
Use: i18n.G("lint <recipe> [flags]"),
Short: i18n.G("Lint a recipe"),
Aliases: []string{i18n.G("l")},
Args: cobra.MinimumNArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,
@ -28,12 +29,12 @@ var RecipeLintCommand = &cobra.Command{
}
headers := []string{
"ref",
"rule",
"severity",
"satisfied",
"skipped",
"resolve",
i18n.G("ref"),
i18n.G("rule"),
i18n.G("severity"),
i18n.G("satisfied"),
i18n.G("skipped"),
i18n.G("resolve"),
}
table, err := formatter.CreateTable()
@ -49,7 +50,7 @@ var RecipeLintCommand = &cobra.Command{
for level := range lint.LintRules {
for _, rule := range lint.LintRules[level] {
if onlyError && rule.Level != "error" {
log.Debugf("skipping %s, does not have level \"error\"", rule.Ref)
log.Debug(i18n.G("skipping %s, does not have level \"error\"", rule.Ref))
continue
}
@ -70,7 +71,7 @@ var RecipeLintCommand = &cobra.Command{
warnMessages = append(warnMessages, err.Error())
}
if !ok && rule.Level == "error" {
if !ok && rule.Level == i18n.G("error") {
hasError = true
}
@ -111,7 +112,7 @@ var RecipeLintCommand = &cobra.Command{
}
if hasError {
log.Warnf("critical errors present in %s config", recipe.Name)
log.Warn(i18n.G("critical errors present in %s config", recipe.Name))
}
}
},
@ -124,17 +125,17 @@ var (
func init() {
RecipeLintCommand.Flags().BoolVarP(
&internal.Chaos,
"chaos",
"C",
i18n.G("chaos"),
i18n.G("C"),
false,
"ignore uncommitted recipes changes",
i18n.G("ignore uncommitted recipes changes"),
)
RecipeLintCommand.Flags().BoolVarP(
&onlyError,
"error",
"e",
i18n.G("error"),
i18n.G("e"),
false,
"only show errors",
i18n.G("only show errors"),
)
}

View File

@ -8,15 +8,16 @@ import (
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"github.com/spf13/cobra"
)
var RecipeListCommand = &cobra.Command{
Use: "list",
Short: "List recipes",
Aliases: []string{"ls"},
Use: i18n.G("list"),
Short: i18n.G("List recipes"),
Aliases: []string{i18n.G("ls")},
Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) {
catl, err := recipe.ReadRecipeCatalogue(internal.Offline)
@ -33,14 +34,14 @@ var RecipeListCommand = &cobra.Command{
}
headers := []string{
"name",
"category",
"status",
"healthcheck",
"backups",
"email",
"tests",
"SSO",
i18n.G("name"),
i18n.G("category"),
i18n.G("status"),
i18n.G("healthcheck"),
i18n.G("backups"),
i18n.G("email"),
i18n.G("tests"),
i18n.G("SSO"),
}
table.Headers(headers...)
@ -73,7 +74,7 @@ var RecipeListCommand = &cobra.Command{
if internal.MachineReadable {
out, err := formatter.ToJSON(headers, rows)
if err != nil {
log.Fatal("unable to render to JSON: %s", err)
log.Fatal(i18n.G("unable to render to JSON: %s", err))
}
fmt.Println(out)
return
@ -93,17 +94,17 @@ var (
func init() {
RecipeListCommand.Flags().BoolVarP(
&internal.MachineReadable,
"machine",
"m",
i18n.G("machine"),
i18n.G("m"),
false,
"print machine-readable output",
i18n.G("print machine-readable output"),
)
RecipeListCommand.Flags().StringVarP(
&pattern,
"pattern",
"p",
i18n.G("pattern"),
i18n.G("p"),
"",
"filter by recipe",
i18n.G("filter by recipe"),
)
}

View File

@ -10,6 +10,7 @@ import (
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/abra/pkg/recipe"
"github.com/spf13/cobra"
@ -30,10 +31,10 @@ type recipeMetadata struct {
}
var RecipeNewCommand = &cobra.Command{
Use: "new <recipe> [flags]",
Aliases: []string{"n"},
Short: "Create a new recipe",
Long: `A community managed recipe template is used.`,
Use: i18n.G("new <recipe> [flags]"),
Aliases: []string{i18n.G("n")},
Short: i18n.G("Create a new recipe"),
Long: i18n.G(`A community managed recipe template is used.`),
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,
@ -46,10 +47,10 @@ var RecipeNewCommand = &cobra.Command{
r := recipe.Get(recipeName)
if _, err := os.Stat(r.Dir); !os.IsNotExist(err) {
log.Fatalf("%s recipe directory already exists?", r.Dir)
log.Fatal(i18n.G("%s recipe directory already exists?", r.Dir))
}
url := fmt.Sprintf("%s/example.git", config.REPOS_BASE_URL)
url := i18n.G("%s/example.git", config.REPOS_BASE_URL)
if err := git.Clone(r.Dir, url); err != nil {
log.Fatal(err)
}
@ -58,7 +59,7 @@ var RecipeNewCommand = &cobra.Command{
if err := os.RemoveAll(gitRepo); err != nil {
log.Fatal(err)
}
log.Debugf("removed .git repo in %s", gitRepo)
log.Debug(i18n.G("removed .git repo in %s", gitRepo))
meta := newRecipeMeta(recipeName)
@ -82,8 +83,8 @@ var RecipeNewCommand = &cobra.Command{
log.Fatal(err)
}
log.Infof("new recipe '%s' created: %s", recipeName, path.Join(r.Dir))
log.Info("happy hacking 🎉")
log.Info(i18n.G("new recipe '%s' created: %s", recipeName, path.Join(r.Dir)))
log.Info(i18n.G("happy hacking 🎉"))
},
}
@ -111,17 +112,17 @@ var (
func init() {
RecipeNewCommand.Flags().StringVarP(
&gitName,
"git-name",
"N",
i18n.G("git-name"),
i18n.G("N"),
"",
"Git (user) name to do commits with",
i18n.G("Git (user) name to do commits with"),
)
RecipeNewCommand.Flags().StringVarP(
&gitEmail,
"git-email",
"e",
i18n.G("git-email"),
i18n.G("e"),
"",
"Git email name to do commits with",
i18n.G("Git email name to do commits with"),
)
}

View File

@ -1,13 +1,16 @@
package recipe
import "github.com/spf13/cobra"
import (
"coopcloud.tech/abra/pkg/i18n"
"github.com/spf13/cobra"
)
// RecipeCommand defines all recipe related sub-commands.
var RecipeCommand = &cobra.Command{
Use: "recipe [cmd] [args] [flags]",
Aliases: []string{"r"},
Short: "Manage recipes",
Long: `A recipe is a blueprint for an app.
Use: i18n.G("recipe [cmd] [args] [flags]"),
Aliases: []string{i18n.G("r")},
Short: i18n.G("Manage recipes"),
Long: i18n.G(`A recipe is a blueprint for an app.
It is a bunch of config files which describe how to deploy and maintain an app.
Recipes are maintained by the Co-op Cloud community and you can use Abra to
@ -15,5 +18,5 @@ read them, deploy them and create apps for you.
Anyone who uses a recipe can become a maintainer. Maintainers typically make
sure the recipe is in good working order and the config upgraded in a timely
manner.`,
manner.`),
}

View File

@ -12,6 +12,7 @@ import (
"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"
"coopcloud.tech/abra/pkg/recipe"
"coopcloud.tech/tagcmp"
@ -22,10 +23,10 @@ import (
)
var RecipeReleaseCommand = &cobra.Command{
Use: "release <recipe> [version] [flags]",
Aliases: []string{"rl"},
Short: "Release a new recipe version",
Long: `Create a new version of a recipe.
Use: i18n.G("release <recipe> [version] [flags]"),
Aliases: []string{i18n.G("rl")},
Short: i18n.G("Release a new recipe version"),
Long: i18n.G(`Create a new version of a recipe.
These versions are then published on the Co-op Cloud recipe catalogue. These
versions take the following form:
@ -44,7 +45,7 @@ major and therefore require intervention while doing the upgrade work.
Publish your new release to git.coopcloud.tech with "--publish/-p". This
requires that you have permission to git push to these repositories and have
your SSH keys configured on your account.`,
your SSH keys configured on your account.`),
Args: cobra.RangeArgs(1, 2),
ValidArgsFunction: func(
cmd *cobra.Command,
@ -74,7 +75,7 @@ your SSH keys configured on your account.`,
mainAppVersion := imagesTmp[mainApp]
if mainAppVersion == "" {
log.Fatalf("main app service version for %s is empty?", recipe.Name)
log.Fatal(i18n.G("main app service version for %s is empty?", recipe.Name))
}
var tagString string
@ -84,12 +85,12 @@ your SSH keys configured on your account.`,
if tagString != "" {
if _, err := tagcmp.Parse(tagString); err != nil {
log.Fatalf("cannot parse %s, invalid tag specified?", tagString)
log.Fatal(i18n.G("cannot parse %s, invalid tag specified?", tagString))
}
}
if (internal.Major || internal.Minor || internal.Patch) && tagString != "" {
log.Fatal("cannot specify tag and bump type at the same time")
log.Fatal(i18n.G("cannot specify tag and bump type at the same time"))
}
if tagString != "" {
@ -117,19 +118,19 @@ your SSH keys configured on your account.`,
}
if !isClean {
log.Infof("%s currently has these unstaged changes 👇", recipe.Name)
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.Warnf("previous git tags detected, assuming this is a new semver release")
log.Warn(i18n.G("previous git tags detected, assuming this is a new semver release"))
if err := createReleaseFromPreviousTag(tagString, mainAppVersion, recipe, tags); err != nil {
log.Fatal(err)
}
} else {
log.Warnf("no tag specified and no previous tag available for %s, assuming this is the initial release", recipe.Name)
log.Warn(i18n.G("no tag specified and no previous tag available for %s, assuming this is the initial release", recipe.Name))
if err := createReleaseFromTag(recipe, tagString, mainAppVersion); err != nil {
if cleanUpErr := cleanUpTag(recipe, tagString); err != nil {
@ -181,7 +182,7 @@ func getImageVersions(recipe recipe.Recipe) (map[string]string, error) {
}
if missingTag {
return services, fmt.Errorf("app service is missing image tag?")
return services, errors.New(i18n.G("app service is missing image tag?"))
}
return services, nil
@ -245,7 +246,7 @@ func btoi(b bool) int {
// getTagCreateOptions constructs git tag create options
func getTagCreateOptions(tag string) (git.CreateTagOptions, error) {
msg := fmt.Sprintf("chore: publish %s release", tag)
msg := i18n.G("chore: publish %s release", tag)
return git.CreateTagOptions{Message: msg}, nil
}
@ -273,13 +274,13 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error {
if _, err := os.Stat(nextReleaseNotePath); err == nil {
// release/next note exists. Move it to release/<tag>
if internal.Dry {
log.Debugf("dry run: move release note from 'next' to %s", tag)
log.Debug(i18n.G("dry run: move release note from 'next' to %s", tag))
return nil
}
if !internal.NoInput {
prompt := &survey.Confirm{
Message: "Use release note in release/next?",
Message: i18n.G("Use release note in release/next?"),
}
if err := survey.AskOne(prompt, &addNextAsReleaseNotes); err != nil {
@ -313,7 +314,7 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error {
}
prompt := &survey.Input{
Message: "Release Note (leave empty for no release note)",
Message: i18n.G("Release Note (leave empty for no release note)"),
}
var releaseNote string
@ -338,7 +339,7 @@ func addReleaseNotes(recipe recipe.Recipe, tag string) error {
func commitRelease(recipe recipe.Recipe, tag string) error {
if internal.Dry {
log.Debugf("dry run: no changes committed")
log.Debug(i18n.G("dry run: no changes committed"))
return nil
}
@ -349,7 +350,7 @@ func commitRelease(recipe recipe.Recipe, tag string) error {
if isClean {
if !internal.Dry {
return fmt.Errorf("no changes discovered in %s, nothing to publish?", recipe.Dir)
return errors.New(i18n.G("no changes discovered in %s, nothing to publish?", recipe.Dir))
}
}
@ -363,7 +364,7 @@ func commitRelease(recipe recipe.Recipe, tag string) error {
func tagRelease(tagString string, repo *git.Repository) error {
if internal.Dry {
log.Debugf("dry run: no git tag created (%s)", tagString)
log.Debug(i18n.G("dry run: no git tag created (%s)", tagString))
return nil
}
@ -383,20 +384,20 @@ func tagRelease(tagString string, repo *git.Repository) error {
}
hash := formatter.SmallSHA(head.Hash().String())
log.Debugf(fmt.Sprintf("created tag %s at %s", tagString, hash))
log.Debug(i18n.G("created tag %s at %s", tagString, hash))
return nil
}
func pushRelease(recipe recipe.Recipe, tagString string) error {
if internal.Dry {
log.Info("dry run: no changes published")
log.Info(i18n.G("dry run: no changes published"))
return nil
}
if !publish && !internal.NoInput {
prompt := &survey.Confirm{
Message: "publish new release?",
Message: i18n.G("publish new release?"),
}
if err := survey.AskOne(prompt, &publish); err != nil {
@ -409,9 +410,9 @@ func pushRelease(recipe recipe.Recipe, tagString string) error {
return err
}
url := fmt.Sprintf("%s/src/tag/%s", recipe.GitURL, tagString)
log.Infof("new release published: %s", url)
log.Info(i18n.G("new release published: %s", url))
} else {
log.Info("no -p/--publish passed, not publishing")
log.Info(i18n.G("no -p/--publish passed, not publishing"))
}
return nil
@ -426,7 +427,7 @@ func createReleaseFromPreviousTag(tagString, mainAppVersion string, recipe recip
bumpType := btoi(internal.Major)*4 + btoi(internal.Minor)*2 + btoi(internal.Patch)
if bumpType != 0 {
if (bumpType & (bumpType - 1)) != 0 {
return fmt.Errorf("you can only use one of: --major, --minor, --patch")
return errors.New(i18n.G("you can only use one of: --major, --minor, --patch"))
}
}
@ -483,12 +484,12 @@ func createReleaseFromPreviousTag(tagString, mainAppVersion string, recipe recip
}
if lastGitTag.String() == tagString {
log.Fatalf("latest git tag (%s) and synced label (%s) are the same?", lastGitTag, tagString)
log.Fatal(i18n.G("latest git tag (%s) and synced label (%s) are the same?", lastGitTag, tagString))
}
if !internal.NoInput {
prompt := &survey.Confirm{
Message: fmt.Sprintf("current: %s, new: %s, correct?", lastGitTag, tagString),
Message: i18n.G("current: %s, new: %s, correct?", lastGitTag, tagString),
}
var ok bool
@ -497,7 +498,7 @@ func createReleaseFromPreviousTag(tagString, mainAppVersion string, recipe recip
}
if !ok {
log.Fatal("exiting as requested")
log.Fatal(i18n.G("exiting as requested"))
}
}
@ -506,15 +507,15 @@ func createReleaseFromPreviousTag(tagString, mainAppVersion string, recipe recip
}
if err := commitRelease(recipe, tagString); err != nil {
log.Fatalf("failed to commit changes: %s", err.Error())
log.Fatal(i18n.G("failed to commit changes: %s", err.Error()))
}
if err := tagRelease(tagString, repo); err != nil {
log.Fatalf("failed to tag release: %s", err.Error())
log.Fatal(i18n.G("failed to tag release: %s", err.Error()))
}
if err := pushRelease(recipe, tagString); err != nil {
log.Fatalf("failed to publish new release: %s", err.Error())
log.Fatal(i18n.G("failed to publish new release: %s", err.Error()))
}
return nil
@ -533,7 +534,7 @@ func cleanUpTag(recipe recipe.Recipe, tag string) error {
}
}
log.Debugf("removed freshly created tag %s", tag)
log.Debug(i18n.G("removed freshly created tag %s", tag))
return nil
}
@ -545,20 +546,20 @@ func getLabelVersion(recipe recipe.Recipe, prompt bool) (string, error) {
}
if initTag == "" {
log.Fatalf("unable to read version for %s from synced label. Did you try running \"abra recipe sync %s\" already?", recipe.Name, recipe.Name)
log.Fatal(i18n.G("unable to read version for %s from synced label. Did you try running \"abra recipe sync %s\" already?", recipe.Name, recipe.Name))
}
log.Warnf("discovered %s as currently synced recipe label", initTag)
log.Warn(i18n.G("discovered %s as currently synced recipe label", initTag))
if prompt && !internal.NoInput {
var response bool
prompt := &survey.Confirm{Message: fmt.Sprintf("use %s as the new version?", initTag)}
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 "", fmt.Errorf("please fix your synced label for %s and re-run this command", recipe.Name)
return "", errors.New(i18n.G("please fix your synced label for %s and re-run this command", recipe.Name))
}
}
@ -572,42 +573,41 @@ var (
func init() {
RecipeReleaseCommand.Flags().BoolVarP(
&internal.Dry,
"dry-run",
"r",
i18n.G("dry-run"),
i18n.G("r"),
false,
"report changes that would be made",
i18n.G("report changes that would be made"),
)
RecipeReleaseCommand.Flags().BoolVarP(
&internal.Major,
"major",
"x",
i18n.G("major"),
i18n.G("x"),
false,
"increase the major part of the version",
i18n.G("increase the major part of the version"),
)
RecipeReleaseCommand.Flags().BoolVarP(
&internal.Minor,
"minor",
"y",
i18n.G("minor"),
i18n.G("y"),
false,
"increase the minor part of the version",
i18n.G("increase the minor part of the version"),
)
RecipeReleaseCommand.Flags().BoolVarP(
&internal.Patch,
"patch",
"z",
i18n.G("patch"),
i18n.G("z"),
false,
"increase the patch part of the version",
i18n.G("increase the patch part of the version"),
)
RecipeReleaseCommand.Flags().BoolVarP(
&publish,
"publish",
"p",
i18n.G("publish"),
i18n.G("p"),
false,
"publish changes to git.coopcloud.tech",
i18n.G("publish changes to git.coopcloud.tech"),
)
}

View File

@ -3,16 +3,17 @@ package recipe
import (
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"github.com/go-git/go-git/v5"
"github.com/spf13/cobra"
)
var RecipeResetCommand = &cobra.Command{
Use: "reset <recipe> [flags]",
Aliases: []string{"rs"},
Short: "Remove all unstaged changes from recipe config",
Long: "WARNING: this will delete your changes. Be Careful.",
Use: i18n.G("reset <recipe> [flags]"),
Aliases: []string{i18n.G("rs")},
Short: i18n.G("Remove all unstaged changes from recipe config"),
Long: i18n.G("WARNING: this will delete your changes. Be Careful."),
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,

View File

@ -7,6 +7,7 @@ import (
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
gitPkg "coopcloud.tech/abra/pkg/git"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2"
@ -16,10 +17,10 @@ import (
)
var RecipeSyncCommand = &cobra.Command{
Use: "sync <recipe> [version] [flags]",
Aliases: []string{"s"},
Short: "Sync recipe version label",
Long: `Generate labels for the main recipe service.
Use: i18n.G("sync <recipe> [version] [flags]"),
Aliases: []string{i18n.G("s")},
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:
@ -27,7 +28,7 @@ By convention, the service named "app" using the following format:
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.`,
local file system.`),
Args: cobra.RangeArgs(1, 2),
ValidArgsFunction: func(
cmd *cobra.Command,
@ -68,11 +69,11 @@ local file system.`,
}
if len(tags) == 0 && nextTag == "" {
log.Warnf("no git tags found for %s", recipe.Name)
log.Warn(i18n.G("no git tags found for %s", recipe.Name))
if internal.NoInput {
log.Fatalf("unable to continue, input required for initial version")
log.Fatal(i18n.G("unable to continue, input required for initial version"))
}
fmt.Println(fmt.Sprintf(`
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
@ -92,7 +93,7 @@ likely to change.
`, recipe.Name, recipe.Name))
var chosenVersion string
edPrompt := &survey.Select{
Message: "which version do you want to begin with?",
Message: i18n.G("which version do you want to begin with?"),
Options: []string{"0.1.0", "1.0.0"},
}
@ -125,7 +126,7 @@ likely to change.
if err := iter.ForEach(func(ref *plumbing.Reference) error {
obj, err := repo.TagObject(ref.Hash())
if err != nil {
log.Fatal("Tag at commit ", ref.Hash(), " is unannotated or otherwise broken. Please fix it.")
log.Fatal(i18n.G("tag at commit %s is unannotated or otherwise broken", ref.Hash()))
return err
}
@ -150,7 +151,7 @@ likely to change.
if bumpType != 0 {
// a bitwise check if the number is a power of 2
if (bumpType & (bumpType - 1)) != 0 {
log.Fatal("you can only use one version flag: --major, --minor or --patch")
log.Fatal(i18n.G("you can only use one version flag: --major, --minor or --patch"))
}
}
@ -184,22 +185,22 @@ likely to change.
}
newTag.Metadata = mainAppVersion
log.Debugf("choosing %s as new version for %s", newTag.String(), recipe.Name)
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.Fatalf("invalid version %s specified", nextTag)
log.Fatal(i18n.G("invalid version %s specified", nextTag))
}
mainService := "app"
label := fmt.Sprintf("coop-cloud.${STACK_NAME}.version=%s", nextTag)
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.Infof("dry run: not syncing label %s for recipe %s", nextTag, recipe.Name)
log.Info(i18n.G("dry run: not syncing label %s for recipe %s", nextTag, recipe.Name))
}
isClean, err := gitPkg.IsClean(recipe.Dir)
@ -207,7 +208,7 @@ likely to change.
log.Fatal(err)
}
if !isClean {
log.Infof("%s currently has these unstaged changes 👇", recipe.Name)
log.Info(i18n.G("%s currently has these unstaged changes 👇", recipe.Name))
if err := gitPkg.DiffUnstaged(recipe.Dir); err != nil {
log.Fatal(err)
}
@ -218,33 +219,33 @@ likely to change.
func init() {
RecipeSyncCommand.Flags().BoolVarP(
&internal.Dry,
"dry-run",
"r",
i18n.G("dry-run"),
i18n.G("r"),
false,
"report changes that would be made",
i18n.G("report changes that would be made"),
)
RecipeSyncCommand.Flags().BoolVarP(
&internal.Major,
"major",
"x",
i18n.G("major"),
i18n.G("x"),
false,
"increase the major part of the version",
i18n.G("increase the major part of the version"),
)
RecipeSyncCommand.Flags().BoolVarP(
&internal.Minor,
"minor",
"y",
i18n.G("minor"),
i18n.G("y"),
false,
"increase the minor part of the version",
i18n.G("increase the minor part of the version"),
)
RecipeSyncCommand.Flags().BoolVarP(
&internal.Patch,
"patch",
"z",
i18n.G("patch"),
i18n.G("z"),
false,
"increase the patch part of the version",
i18n.G("increase the patch part of the version"),
)
}

View File

@ -14,6 +14,7 @@ import (
"coopcloud.tech/abra/pkg/client"
"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"
@ -37,10 +38,10 @@ type anUpgrade struct {
}
var RecipeUpgradeCommand = &cobra.Command{
Use: "upgrade <recipe> [flags]",
Aliases: []string{"u"},
Short: "Upgrade recipe image tags",
Long: `Upgrade a given <recipe> configuration.
Use: i18n.G("upgrade <recipe> [flags]"),
Aliases: []string{i18n.G("u")},
Short: i18n.G("Upgrade recipe image tags"),
Long: i18n.G(`Upgrade a given <recipe> configuration.
It will update the relevant compose file tags on the local file system.
@ -52,7 +53,7 @@ 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.`,
You may invoke this command in "wizard" mode and be prompted for input.`),
Args: cobra.RangeArgs(0, 1),
ValidArgsFunction: func(
cmd *cobra.Command,
@ -71,7 +72,7 @@ You may invoke this command in "wizard" mode and be prompted for input.`,
if bumpType != 0 {
// a bitwise check if the number is a power of 2
if (bumpType & (bumpType - 1)) != 0 {
log.Fatal("you can only use one of: --major, --minor, --patch.")
log.Fatal(i18n.G("you can only use one of: --major, --minor, --patch."))
}
}
@ -87,7 +88,7 @@ You may invoke this command in "wizard" mode and be prompted for input.`,
versionsPath := path.Join(recipe.Dir, "versions")
servicePins := make(map[string]imgPin)
if _, err := os.Stat(versionsPath); err == nil {
log.Debugf("found versions file for %s", recipe.Name)
log.Debug(i18n.G("found versions file for %s", recipe.Name))
file, err := os.Open(versionsPath)
if err != nil {
log.Fatal(err)
@ -97,7 +98,7 @@ You may invoke this command in "wizard" mode and be prompted for input.`,
line := scanner.Text()
splitLine := strings.Split(line, " ")
if splitLine[0] != "pin" || len(splitLine) != 3 {
log.Fatalf("malformed version pin specification: %s", line)
log.Fatal(i18n.G("malformed version pin specification: %s", line))
}
pinSlice := strings.Split(splitLine[2], ":")
pinTag, err := tagcmp.Parse(pinSlice[1])
@ -115,7 +116,7 @@ You may invoke this command in "wizard" mode and be prompted for input.`,
}
versionsPresent = true
} else {
log.Debugf("did not find versions file for %s", recipe.Name)
log.Debug(i18n.G("did not find versions file for %s", recipe.Name))
}
config, err := recipe.GetComposeConfig(nil)
@ -135,26 +136,26 @@ You may invoke this command in "wizard" mode and be prompted for input.`,
}
image := reference.Path(img)
log.Debugf("retrieved %s from remote registry for %s", regVersions, image)
log.Debug(i18n.G("retrieved %s from remote registry for %s", regVersions, image))
image = formatter.StripTagMeta(image)
switch img.(type) {
case reference.NamedTagged:
if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) {
log.Debugf("%s not considered semver-like", img.(reference.NamedTagged).Tag())
log.Debug(i18n.G("%s not considered semver-like", img.(reference.NamedTagged).Tag()))
}
default:
log.Warnf("unable to read tag for image %s, is it missing? skipping upgrade for %s", image, service.Name)
log.Warn(i18n.G("unable to read tag for image %s, is it missing? skipping upgrade for %s", image, service.Name))
continue
}
tag, err := tagcmp.Parse(img.(reference.NamedTagged).Tag())
if err != nil {
log.Warnf("unable to parse %s, error was: %s, skipping upgrade for %s", image, err.Error(), service.Name)
log.Warn(i18n.G("unable to parse %s, error was: %s, skipping upgrade for %s", image, err.Error(), service.Name))
continue
}
log.Debugf("parsed %s for %s", tag, service.Name)
log.Debug(i18n.G("parsed %s for %s", tag, service.Name))
var compatible []tagcmp.Tag
for _, regVersion := range regVersions {
@ -168,12 +169,12 @@ You may invoke this command in "wizard" mode and be prompted for input.`,
}
}
log.Debugf("detected potential upgradable tags %s for %s", compatible, service.Name)
log.Debug(i18n.G("detected potential upgradable tags %s for %s", compatible, service.Name))
sort.Sort(tagcmp.ByTagDesc(compatible))
if len(compatible) == 0 && !allTags {
log.Info(fmt.Sprintf("no new versions available for %s, assuming %s is the latest (use -a/--all-tags to see all anyway)", image, tag))
log.Info(i18n.G("no new versions available for %s, assuming %s is the latest (use -a/--all-tags to see all anyway)", image, tag))
continue // skip on to the next tag and don't update any compose files
}
@ -195,7 +196,7 @@ You may invoke this command in "wizard" mode and be prompted for input.`,
}
}
log.Debugf("detected compatible upgradable tags %s for %s", compatibleStrings, service.Name)
log.Debug(i18n.G("detected compatible upgradable tags %s for %s", compatibleStrings, service.Name))
var upgradeTag string
_, ok := servicePins[service.Name]
@ -212,13 +213,13 @@ You may invoke this command in "wizard" mode and be prompted for input.`,
}
}
if contains {
log.Infof("upgrading service %s from %s to %s (pinned tag: %s)", service.Name, tag.String(), upgradeTag, pinnedTagString)
log.Info(i18n.G("upgrading service %s from %s to %s (pinned tag: %s)", service.Name, tag.String(), upgradeTag, pinnedTagString))
} else {
log.Infof("service %s, image %s pinned to %s, no compatible upgrade found", service.Name, servicePins[service.Name].image, pinnedTagString)
log.Info(i18n.G("service %s, image %s pinned to %s, no compatible upgrade found", service.Name, servicePins[service.Name].image, pinnedTagString))
continue
}
} else {
log.Fatalf("service %s is at version %s, but pinned to %s, please correct your compose.yml file manually!", service.Name, tag.String(), pinnedTag.String())
log.Fatal(i18n.G("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 {
@ -235,17 +236,17 @@ You may invoke this command in "wizard" mode and be prompted for input.`,
}
}
if upgradeTag == "" {
log.Warnf("not upgrading from %s to %s for %s, because the upgrade type is more serious than what user wants", tag.String(), compatible[0].String(), image)
log.Warn(i18n.G("not upgrading from %s to %s for %s, because the upgrade type is more serious than what user wants", tag.String(), compatible[0].String(), image))
continue
}
} else {
msg := fmt.Sprintf("upgrade to which tag? (service: %s, image: %s, tag: %s)", service.Name, image, tag)
msg := i18n.G("upgrade to which tag? (service: %s, image: %s, tag: %s)", service.Name, image, tag)
if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) || allTags {
tag := img.(reference.NamedTagged).Tag()
if !allTags {
log.Warn(fmt.Sprintf("unable to determine versioning semantics of %s, listing all tags", tag))
log.Warn(i18n.G("unable to determine versioning semantics of %s, listing all tags", tag))
}
msg = fmt.Sprintf("upgrade to which tag? (service: %s, tag: %s)", service.Name, tag)
msg = i18n.G("upgrade to which tag? (service: %s, tag: %s)", service.Name, tag)
compatibleStrings = []string{"skip"}
for _, regVersion := range regVersions {
compatibleStrings = append(compatibleStrings, regVersion)
@ -276,7 +277,7 @@ You may invoke this command in "wizard" mode and be prompted for input.`,
} else {
prompt := &survey.Select{
Message: msg,
Help: "enter / return to confirm, choose 'skip' to not upgrade this tag, vim mode is enabled",
Help: i18n.G("enter / return to confirm, choose 'skip' to not upgrade this tag, vim mode is enabled"),
VimMode: true,
Options: compatibleStrings,
}
@ -292,11 +293,11 @@ You may invoke this command in "wizard" mode and be prompted for input.`,
log.Fatal(err)
}
if ok {
log.Infof("tag upgraded from %s to %s for %s", tag.String(), upgradeTag, image)
log.Info(i18n.G("tag upgraded from %s to %s for %s", tag.String(), upgradeTag, image))
}
} else {
if !internal.NoInput {
log.Warnf("not upgrading %s, skipping as requested", image)
log.Warn(i18n.G("not upgrading %s, skipping as requested", image))
}
}
}
@ -314,7 +315,7 @@ You may invoke this command in "wizard" mode and be prompted for input.`,
}
for _, upgrade := range upgradeList {
log.Infof("can upgrade service: %s, image: %s, tag: %s ::", upgrade.Service, upgrade.Image, upgrade.Tag)
log.Info(i18n.G("can upgrade service: %s, image: %s, tag: %s ::", upgrade.Service, upgrade.Image, upgrade.Tag))
for _, utag := range upgrade.UpgradeTags {
log.Infof(" %s", utag)
}
@ -326,7 +327,7 @@ You may invoke this command in "wizard" mode and be prompted for input.`,
log.Fatal(err)
}
if !isClean {
log.Infof("%s currently has these unstaged changes 👇", recipe.Name)
log.Info(i18n.G("%s currently has these unstaged changes 👇", recipe.Name))
if err := gitPkg.DiffUnstaged(recipe.Dir); err != nil {
log.Fatal(err)
}
@ -341,41 +342,41 @@ var (
func init() {
RecipeUpgradeCommand.Flags().BoolVarP(
&internal.Major,
"major",
"x",
i18n.G("major"),
i18n.G("x"),
false,
"increase the major part of the version",
i18n.G("increase the major part of the version"),
)
RecipeUpgradeCommand.Flags().BoolVarP(
&internal.Minor,
"minor",
"y",
i18n.G("minor"),
i18n.G("y"),
false,
"increase the minor part of the version",
i18n.G("increase the minor part of the version"),
)
RecipeUpgradeCommand.Flags().BoolVarP(
&internal.Patch,
"patch",
"z",
i18n.G("patch"),
i18n.G("z"),
false,
"increase the patch part of the version",
i18n.G("increase the patch part of the version"),
)
RecipeUpgradeCommand.Flags().BoolVarP(
&internal.MachineReadable,
"machine",
"m",
i18n.G("machine"),
i18n.G("m"),
false,
"print machine-readable output",
i18n.G("print machine-readable output"),
)
RecipeUpgradeCommand.Flags().BoolVarP(
&allTags,
"all-tags",
"a",
i18n.G("all-tags"),
i18n.G("a"),
false,
"list all tags, not just upgrades",
i18n.G("list all tags, not just upgrades"),
)
}

View File

@ -7,15 +7,16 @@ import (
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/autocomplete"
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/i18n"
"coopcloud.tech/abra/pkg/log"
recipePkg "coopcloud.tech/abra/pkg/recipe"
"github.com/spf13/cobra"
)
var RecipeVersionCommand = &cobra.Command{
Use: "versions <recipe> [flags]",
Aliases: []string{"v"},
Short: "List recipe versions",
Use: i18n.G("versions <recipe> [flags]"),
Aliases: []string{i18n.G("v")},
Short: i18n.G("List recipe versions"),
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,
@ -35,7 +36,7 @@ var RecipeVersionCommand = &cobra.Command{
recipeMeta, ok := catl[recipe.Name]
if !ok {
warnMessages = append(warnMessages, "retrieved versions from local recipe repository")
warnMessages = append(warnMessages, i18n.G("retrieved versions from local recipe repository"))
recipeVersions, warnMsg, err := recipe.GetRecipeVersions()
if err != nil {
@ -49,7 +50,7 @@ var RecipeVersionCommand = &cobra.Command{
}
if len(recipeMeta.Versions) == 0 {
log.Fatalf("%s has no published versions?", recipe.Name)
log.Fatal(i18n.G("%s has no published versions?", recipe.Name))
}
for i := len(recipeMeta.Versions) - 1; i >= 0; i-- {
@ -58,7 +59,7 @@ var RecipeVersionCommand = &cobra.Command{
log.Fatal(err)
}
table.Headers("SERVICE", "IMAGE", "TAG", "VERSION")
table.Headers(i18n.G("SERVICE"), i18n.G("IMAGE"), i18n.G("TAG"), i18n.G("VERSION"))
for version, meta := range recipeMeta.Versions[i] {
var allRows [][]string
@ -99,10 +100,10 @@ var RecipeVersionCommand = &cobra.Command{
if internal.MachineReadable {
sort.Slice(allRows, sortServiceByName(allRows))
headers := []string{"VERSION", "SERVICE", "NAME", "TAG"}
headers := []string{i18n.G("VERSION"), i18n.G("SERVICE"), i18n.G("NAME"), i18n.G("TAG")}
out, err := formatter.ToJSON(headers, allRows)
if err != nil {
log.Fatal("unable to render to JSON: %s", err)
log.Fatal(i18n.G("unable to render to JSON: %s", err))
}
fmt.Println(out)
continue
@ -127,9 +128,9 @@ func sortServiceByName(versions [][]string) func(i, j int) bool {
func init() {
RecipeVersionCommand.Flags().BoolVarP(
&internal.MachineReadable,
"machine",
"m",
i18n.G("machine"),
i18n.G("m"),
false,
"print machine-readable output",
i18n.G("print machine-readable output"),
)
}