Documentation
¶
Index ¶
Constants ¶
This section is empty.
Variables ¶
View Source
var RecipeCommand = &cobra.Command{ Use: i18n.G("recipe [cmd] [args] [flags]"), Aliases: strings.Split(recipeAliases, ","), 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 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.`), }
RecipeCommand defines all recipe related sub-commands.
View Source
var RecipeDiffCommand = &cobra.Command{ Use: i18n.G("diff <recipe> [flags]"), Aliases: strings.Split(recipeDiffAliases, ","), 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, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return autocomplete.RecipeNameComplete() }, Run: func(cmd *cobra.Command, args []string) { r := internal.ValidateRecipe(args, cmd.Name()) if err := gitPkg.DiffUnstaged(r.Dir); err != nil { log.Fatal(err) } }, }
View Source
var RecipeFetchCommand = &cobra.Command{ Use: i18n.G("fetch [recipe | --all] [flags]"), Aliases: strings.Split(recipeFetchAliases, ","), 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: 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`), ValidArgsFunction: func( cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return autocomplete.RecipeNameComplete() }, Run: func(cmd *cobra.Command, args []string) { var recipeName string if len(args) > 0 { recipeName = args[0] } if recipeName == "" && !fetchAllRecipes { log.Fatal(i18n.G("missing [recipe] or --all/-a")) } if recipeName != "" && fetchAllRecipes { 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.Warn(i18n.G("%s is already fetched", r.Name)) return } } r = internal.ValidateRecipe(args, cmd.Name()) if sshRemote { if r.SSHURL == "" { log.Warn(i18n.G("unable to discover SSH remote for %s", r.Name)) return } repo, err := git.PlainOpen(r.Dir) if err != nil { log.Fatal(i18n.G("unable to open %s: %s", r.Dir, err)) } if err = repo.DeleteRemote("origin"); err != nil { 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.Fatal(i18n.G("unable to set SSH remote in %s: %s", r.Dir, err)) } } return } catalogue, err := recipe.ReadRecipeCatalogue(internal.Offline) if err != nil { log.Fatal(err) } catlBar := formatter.CreateProgressbar(len(catalogue), i18n.G("fetching latest recipes...")) ensureCtx := internal.GetEnsureContext() for recipeName := range catalogue { r := recipe.Get(recipeName) if err := r.Ensure(ensureCtx); err != nil { log.Error(err) } catlBar.Add(1) } }, }
View Source
var RecipeLintCommand = &cobra.Command{ Use: i18n.G("lint <recipe> [flags]"), Short: i18n.G("Lint a recipe"), Aliases: strings.Split(recipeLintAliases, ","), Args: cobra.MinimumNArgs(1), ValidArgsFunction: func( cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return autocomplete.RecipeNameComplete() }, Run: func(cmd *cobra.Command, args []string) { recipe := internal.ValidateRecipe(args, cmd.Name()) if err := recipe.Ensure(internal.GetEnsureContext()); err != nil { log.Fatal(err) } headers := []string{ i18n.G("ref"), i18n.G("rule"), i18n.G("severity"), i18n.G("satisfied"), i18n.G("skipped"), i18n.G("resolve"), } table, err := formatter.CreateTable() if err != nil { log.Fatal(err) } table.Headers(headers...) hasError := false var rows [][]string var warnMessages []string for level := range lint.LintRules { for _, rule := range lint.LintRules[level] { if onlyError && rule.Level != "error" { log.Debug(i18n.G("skipping %s, does not have level \"error\"", rule.Ref)) continue } skipped := false if rule.Skip(recipe) { skipped = true } skippedOutput := "-" if skipped { skippedOutput = "✅" } satisfied := false if !skipped { ok, err := rule.Function(recipe) if err != nil { warnMessages = append(warnMessages, err.Error()) } if !ok && rule.Level == i18n.G("error") { hasError = true } if ok { satisfied = true } } satisfiedOutput := "✅" if !satisfied { satisfiedOutput = "❌" if skipped { satisfiedOutput = "-" } } row := []string{ rule.Ref, rule.Description, rule.Level, satisfiedOutput, skippedOutput, rule.HowToResolve, } rows = append(rows, row) table.Row(row...) } } if len(rows) > 0 { if err := formatter.PrintTable(table); err != nil { log.Fatal(err) } for _, warnMsg := range warnMessages { log.Warn(warnMsg) } if hasError { log.Warn(i18n.G("critical errors present in %s config", recipe.Name)) } } }, }
View Source
var RecipeListCommand = &cobra.Command{ Use: i18n.G("list"), Short: i18n.G("List recipes"), Aliases: strings.Split(recipeListAliases, ","), Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { catl, err := recipe.ReadRecipeCatalogue(internal.Offline) if err != nil { log.Fatal(err) } recipes := catl.Flatten() sort.Sort(recipe.ByRecipeName(recipes)) table, err := formatter.CreateTable() if err != nil { log.Fatal(err) } headers := []string{ 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...) var rows [][]string for _, recipe := range recipes { row := []string{ recipe.Name, recipe.Category, strconv.Itoa(recipe.Features.Status), recipe.Features.Healthcheck, recipe.Features.Backups, recipe.Features.Email, recipe.Features.Tests, recipe.Features.SSO, } if pattern != "" { if strings.Contains(recipe.Name, pattern) { table.Row(row...) rows = append(rows, row) } } else { table.Row(row...) rows = append(rows, row) } } if len(rows) > 0 { if internal.MachineReadable { out, err := formatter.ToJSON(headers, rows) if err != nil { log.Fatal(i18n.G("unable to render to JSON: %s", err)) } fmt.Println(out) return } if err := formatter.PrintTable(table); err != nil { log.Fatal(err) } } }, }
View Source
var RecipeNewCommand = &cobra.Command{ Use: i18n.G("new <recipe> [flags]"), Aliases: strings.Split(recipeNewAliases, ","), 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, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return autocomplete.RecipeNameComplete() }, Run: func(cmd *cobra.Command, args []string) { recipeName := args[0] r := recipe.Get(recipeName) if _, err := os.Stat(r.Dir); !os.IsNotExist(err) { log.Fatal(i18n.G("%s recipe directory already exists?", r.Dir)) } url := i18n.G("%s/example.git", config.REPOS_BASE_URL) if err := git.Clone(r.Dir, url); err != nil { log.Fatal(err) } gitRepo := path.Join(r.Dir, ".git") if err := os.RemoveAll(gitRepo); err != nil { log.Fatal(err) } log.Debug(i18n.G("removed .git repo in %s", gitRepo)) meta := newRecipeMeta(recipeName) for _, path := range []string{r.ReadmePath, r.SampleEnvPath} { tpl, err := template.ParseFiles(path) if err != nil { log.Fatal(err) } var templated bytes.Buffer if err := tpl.Execute(&templated, meta); err != nil { log.Fatal(err) } if err := os.WriteFile(path, templated.Bytes(), 0o644); err != nil { log.Fatal(err) } } if err := git.Init(r.Dir, true, gitName, gitEmail); err != nil { log.Fatal(err) } log.Info(i18n.G("new recipe '%s' created: %s", recipeName, path.Join(r.Dir))) log.Info(i18n.G("happy hacking 🎉")) }, }
View Source
var RecipeReleaseCommand = &cobra.Command{ Use: i18n.G("release <recipe> [version] [flags]"), Aliases: strings.Split(recipeReleaseAliases, ","), 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: a.b.c+x.y.z Where the "a.b.c" part is a semantic version determined by the maintainer. The "x.y.z" part is the image tag of the recipe "app" service (the main container which contains the software to be used, by naming convention). We maintain a semantic versioning scheme ("a.b.c") alongside the recipe versioning scheme ("x.y.z") in order to maximise the chances that the nature of recipe updates are properly communicated. I.e. developers of an app might publish a minor version but that might lead to changes in the recipe which are major and therefore require intervention while doing the upgrade work. This command will publish your new release to git.coopcloud.tech. This requires that you have permission to git push to these repositories and have your SSH keys configured on your account. Enable ssh-agent and make sure to add your private key and enter your passphrase beforehand. eval ` + "`ssh-agent`" + ` ssh-add ~/.ssh/<my-ssh-private-key-for-git-coopcloud-tech>`), Example: ` # publish release eval ` + "`ssh-agent`" + ` ssh-add ~/.ssh/id_ed25519 abra recipe release gitea`, Args: cobra.RangeArgs(1, 2), ValidArgsFunction: func( cmd *cobra.Command, args []string, toComplete string, ) ([]string, cobra.ShellCompDirective) { switch l := len(args); l { case 0: return autocomplete.RecipeNameComplete() case 1: return autocomplete.RecipeVersionComplete(args[0]) default: return nil, cobra.ShellCompDirectiveDefault } }, Run: func(cmd *cobra.Command, args []string) { recipe := internal.ValidateRecipe(args, cmd.Name()) imagesTmp, err := GetImageVersions(recipe) if err != nil { log.Fatal(err) } mainApp, err := internal.GetMainAppImage(recipe) if err != nil { log.Fatal(err) } mainAppVersion := imagesTmp[mainApp] if mainAppVersion == "" { log.Fatal(i18n.G("main app service version for %s is empty?", recipe.Name)) } repo, err := git.PlainOpen(recipe.Dir) if err != nil { log.Fatal(err) } preCommitHead, err := repo.Head() if err != nil { log.Fatal(err) } isClean, err := gitPkg.IsClean(recipe.Dir) if err != nil { log.Fatal(err) } if !isClean { log.Fatal(i18n.G("working directory not clean in %s, aborting", recipe.Dir)) } tags, err := recipe.Tags() if err != nil { log.Fatal(err) } var tagString string if len(args) == 2 { tagString = args[1] } if (internal.Major || internal.Minor || internal.Patch) && tagString != "" { log.Fatal(i18n.G("cannot specify tag and bump type at the same time")) } if len(tags) == 0 && tagString == "" { log.Warn(i18n.G("no git tags found for %s", recipe.Name)) if internal.NoInput { log.Fatal(i18n.G("unable to continue, input required for initial version")) } 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 0.1.0: development release, still hacking. when you make a major upgrade you increment the "y" part (i.e. 0.1.0 -> 0.2.0) and only move to using the "x" part when things are stable. 1.0.0: public release, assumed to be working. you already have a stable and reliable deployment of this app and feel relatively confident about it. If you want people to be able alpha test your current config for %s but don't think it is quite reliable, go with 0.1.0 and people will know that things are likely to change. `, recipe.Name, recipe.Name)) var chosenVersion string edPrompt := &survey.Select{ Message: i18n.G("which version do you want to begin with?"), Options: []string{"0.1.0", "1.0.0"}, } if err := survey.AskOne(edPrompt, &chosenVersion); err != nil { log.Fatal(err) } tagString = fmt.Sprintf("%s+%s", chosenVersion, mainAppVersion) } if tagString == "" && (!internal.Major && !internal.Minor && !internal.Patch) { catl, err := recipePkg.ReadRecipeCatalogue(false) if err != nil { log.Fatal(err) } changesTable, err := formatter.CreateTable() if err != nil { log.Fatal(err) } latestRelease := tags[len(tags)-1] latestRecipeVersion, err := getLatestVersion(recipe, catl) if err != nil && err != errEmptyVersionsInCatalogue { log.Fatal(err) } changesTable.Headers(i18n.G("SERVICE"), latestRelease, i18n.G("PROPOSED CHANGES")) allRecipeVersions := catl[recipe.Name].Versions for _, recipeVersion := range allRecipeVersions { if serviceVersions, ok := recipeVersion[latestRecipeVersion]; ok { for serviceName := range serviceVersions { serviceMeta := serviceVersions[serviceName] existingImageTag := fmt.Sprintf("%s:%s", serviceMeta.Image, serviceMeta.Tag) newImageTag := fmt.Sprintf("%s:%s", serviceMeta.Image, imagesTmp[serviceMeta.Image]) if existingImageTag == newImageTag { continue } changesTable.Row([]string{serviceName, existingImageTag, newImageTag}...) } } } changeOverview := changesTable.Render() if err := internal.PromptBumpType("", latestRelease, changeOverview); err != nil { log.Fatal(err) } } if tagString == "" { var lastGitTag tagcmp.Tag iter, err := repo.Tags() if err != nil { log.Fatal(err) } if err := iter.ForEach(func(ref *plumbing.Reference) error { obj, err := repo.TagObject(ref.Hash()) if err != nil { log.Fatal(i18n.G("tag at commit %s is unannotated or otherwise broken", ref.Hash())) 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 { log.Fatal(err) } bumpType := btoi(internal.Major)*4 + btoi(internal.Minor)*2 + btoi(internal.Patch) if bumpType != 0 { if (bumpType & (bumpType - 1)) != 0 { log.Fatal(i18n.G("you can only use one version flag: --major, --minor or --patch")) } } newTag := lastGitTag if bumpType > 0 { if internal.Patch { now, err := strconv.Atoi(newTag.Patch) if err != nil { log.Fatal(err) } newTag.Patch = strconv.Itoa(now + 1) } else if internal.Minor { now, err := strconv.Atoi(newTag.Minor) if err != nil { log.Fatal(err) } newTag.Patch = "0" newTag.Minor = strconv.Itoa(now + 1) } else if internal.Major { now, err := strconv.Atoi(newTag.Major) if err != nil { log.Fatal(err) } newTag.Patch = "0" newTag.Minor = "0" newTag.Major = strconv.Itoa(now + 1) } } newTag.Metadata = mainAppVersion log.Debug(i18n.G("choosing %s as new version for %s", newTag.String(), recipe.Name)) tagString = newTag.String() } if _, err := tagcmp.Parse(tagString); err != nil { log.Fatal(i18n.G("invalid version %s specified", tagString)) } for _, tag := range tags { previousTagLeftHand := strings.Split(tag, "+")[0] newTagStringLeftHand := strings.Split(tagString, "+")[0] if previousTagLeftHand == newTagStringLeftHand { log.Fatal(i18n.G("%s+... conflicts with a previous release: %s", newTagStringLeftHand, tag)) } } if err := createReleaseFromTag(recipe, tagString, mainAppVersion); err != nil { if cleanErr := cleanTag(recipe, tagString); cleanErr != nil { log.Fatal(i18n.G("unable to clean up tag after failed release attempt: %s", cleanErr)) } if resetErr := resetCommit(recipe, preCommitHead); resetErr != nil { log.Fatal(i18n.G("unable to reset commit after failed release attempt: %s", resetErr)) } log.Error(err) log.Fatal(i18n.G("release failed. any changes made have been reverted")) } }, }
View Source
var RecipeResetCommand = &cobra.Command{ Use: i18n.G("reset <recipe> [flags]"), Aliases: strings.Split(recipeResetAliases, ","), 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, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return autocomplete.RecipeNameComplete() }, Run: func(cmd *cobra.Command, args []string) { r := internal.ValidateRecipe(args, cmd.Name()) repo, err := git.PlainOpen(r.Dir) if err != nil { log.Fatal(err) } ref, err := repo.Head() if err != nil { log.Fatal(err) } worktree, err := repo.Worktree() if err != nil { log.Fatal(err) } opts := &git.ResetOptions{Commit: ref.Hash(), Mode: git.HardReset} if err := worktree.Reset(opts); err != nil { log.Fatal(err) } }, }
View Source
var RecipeUpgradeCommand = &cobra.Command{ Use: i18n.G("upgrade <recipe> [flags]"), Aliases: strings.Split(recipeUpgradeAliases, ","), 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. Some image tags cannot be parsed because they do not follow some sort of semver-like convention. In this case, all possible tags will be listed and it is up to the end-user to decide. 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.`), Args: cobra.RangeArgs(0, 1), ValidArgsFunction: func( cmd *cobra.Command, args []string, toComplete string, ) ([]string, cobra.ShellCompDirective) { return autocomplete.RecipeNameComplete() }, Run: func(cmd *cobra.Command, args []string) { recipe := internal.ValidateRecipe(args, cmd.Name()) if err := recipe.Ensure(internal.GetEnsureContext()); err != nil { log.Fatal(err) } bumpType := btoi(internal.Major)*4 + btoi(internal.Minor)*2 + btoi(internal.Patch) if bumpType != 0 { if (bumpType & (bumpType - 1)) != 0 { log.Fatal(i18n.G("you can only use one of: --major, --minor, --patch.")) } } if internal.MachineReadable { internal.NoInput = true } upgradeList := make(map[string]anUpgrade) versionsPresent := false versionsPath := path.Join(recipe.Dir, "versions") servicePins := make(map[string]imgPin) if _, err := os.Stat(versionsPath); err == nil { log.Debug(i18n.G("found versions file for %s", recipe.Name)) file, err := os.Open(versionsPath) if err != nil { log.Fatal(err) } scanner := bufio.NewScanner(file) for scanner.Scan() { line := scanner.Text() splitLine := strings.Split(line, " ") if splitLine[0] != "pin" || len(splitLine) != 3 { log.Fatal(i18n.G("malformed version pin specification: %s", line)) } pinSlice := strings.Split(splitLine[2], ":") pinTag, err := tagcmp.Parse(pinSlice[1]) if err != nil { log.Fatal(err) } pin := imgPin{ image: pinSlice[0], version: pinTag, } servicePins[splitLine[1]] = pin } if err := scanner.Err(); err != nil { log.Error(err) } versionsPresent = true } else { log.Debug(i18n.G("did not find versions file for %s", recipe.Name)) } config, err := recipe.GetComposeConfig(nil) if err != nil { log.Fatal(err) } for _, service := range config.Services { img, err := reference.ParseNormalizedNamed(service.Image) if err != nil { log.Fatal(err) } regVersions, err := client.GetRegistryTags(img) if err != nil { log.Fatal(err) } image := reference.Path(img) 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.Debug(i18n.G("%s not considered semver-like", img.(reference.NamedTagged).Tag())) } default: 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.Warn(i18n.G("unable to parse %s, error was: %s, skipping upgrade for %s", image, err.Error(), service.Name)) continue } log.Debug(i18n.G("parsed %s for %s", tag, service.Name)) var compatible []tagcmp.Tag for _, regVersion := range regVersions { other, err := tagcmp.Parse(regVersion) if err != nil { continue } if tag.IsCompatible(other) && tag.IsLessThan(other) && !tag.Equals(other) { compatible = append(compatible, other) } } 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(i18n.G("no new versions available for %s, assuming %s is the latest (use -a/--all-tags to see all anyway)", image, tag)) continue } catlVersions, err := recipePkg.VersionsOfService(recipe.Name, service.Name, internal.Offline) if err != nil { log.Fatal(err) } compatibleStrings := []string{"skip"} for _, compat := range compatible { skip := false for _, catlVersion := range catlVersions { if compat.String() == catlVersion { skip = true } } if !skip { compatibleStrings = append(compatibleStrings, compat.String()) } } log.Debug(i18n.G("detected compatible upgradable tags %s for %s", compatibleStrings, service.Name)) var upgradeTag string _, ok := servicePins[service.Name] if versionsPresent && ok { pinnedTag := servicePins[service.Name].version if tag.IsLessThan(pinnedTag) { pinnedTagString := pinnedTag.String() contains := false for _, v := range compatible { if pinnedTag.IsUpgradeCompatible(v) { contains = true upgradeTag = v.String() break } } if contains { log.Info(i18n.G("upgrading service %s from %s to %s (pinned tag: %s)", service.Name, tag.String(), upgradeTag, pinnedTagString)) } else { 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.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 { if bumpType != 0 { for _, upTag := range compatible { upElement, err := tag.UpgradeDelta(upTag) if err != nil { return } delta := upElement.UpgradeType() if delta <= bumpType { upgradeTag = upTag.String() break } } if upgradeTag == "" { 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 := 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(i18n.G("unable to determine versioning semantics of %s, listing all tags", 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) } } upgradableTags := compatibleStrings[1:] upgrade := anUpgrade{ Service: service.Name, Image: image, Tag: tag.String(), UpgradeTags: make([]string, len(upgradableTags)), } for n, s := range upgradableTags { var sb strings.Builder if _, err := sb.WriteString(s); err != nil { } upgrade.UpgradeTags[n] = sb.String() } upgradeList[upgrade.Service] = upgrade if internal.NoInput { upgradeTag = "skip" } else { prompt := &survey.Select{ Message: msg, Help: i18n.G("enter / return to confirm, choose 'skip' to not upgrade this tag, vim mode is enabled"), VimMode: true, Options: compatibleStrings, } if err := survey.AskOne(prompt, &upgradeTag); err != nil { log.Fatal(err) } } } } if upgradeTag != "skip" { ok, err := recipe.UpdateTag(image, upgradeTag) if err != nil { log.Fatal(err) } if ok { log.Info(i18n.G("tag upgraded from %s to %s for %s", tag.String(), upgradeTag, image)) } } else { if !internal.NoInput { log.Warn(i18n.G("not upgrading %s, skipping as requested", image)) } } } if internal.NoInput { if internal.MachineReadable { jsonstring, err := json.Marshal(upgradeList) if err != nil { log.Fatal(err) } fmt.Println(string(jsonstring)) return } for _, upgrade := range upgradeList { 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) } } } isClean, err := gitPkg.IsClean(recipe.Dir) if err != nil { log.Fatal(err) } if !isClean { log.Info(i18n.G("%s currently has these unstaged changes 👇", recipe.Name)) if err := gitPkg.DiffUnstaged(recipe.Dir); err != nil { log.Fatal(err) } if !internal.NoInput && !createCommit { prompt := &survey.Confirm{ Message: i18n.G("commit changes?"), Default: true, } if err := survey.AskOne(prompt, &createCommit); err != nil { log.Fatal(err) } } if createCommit { msg := i18n.G("chore: update image tags") if err := gitPkg.Commit(recipe.Dir, msg, internal.Dry); err != nil { log.Fatal(err) } log.Info(i18n.G("committed changes as '%s'", msg)) } } else { if createCommit { log.Warn(i18n.G("no changes, skip creating commit")) } } }, }
View Source
var RecipeVersionCommand = &cobra.Command{ Use: i18n.G("versions <recipe> [flags]"), Aliases: strings.Split(recipeVersionsAliases, ","), Short: i18n.G("List recipe versions"), Args: cobra.ExactArgs(1), ValidArgsFunction: func( cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return autocomplete.RecipeNameComplete() }, Run: func(cmd *cobra.Command, args []string) { var warnMessages []string recipe := internal.ValidateRecipe(args, cmd.Name()) catl, err := recipePkg.ReadRecipeCatalogue(internal.Offline) if err != nil { log.Fatal(err) } recipeMeta, ok := catl[recipe.Name] if !ok { warnMessages = append(warnMessages, i18n.G("retrieved versions from local recipe repository")) recipeVersions, warnMsg, err := recipe.GetRecipeVersions() if err != nil { warnMessages = append(warnMessages, err.Error()) } if len(warnMsg) > 0 { warnMessages = append(warnMessages, warnMsg...) } recipeMeta = recipePkg.RecipeMeta{Versions: recipeVersions} } if len(recipeMeta.Versions) == 0 { log.Fatal(i18n.G("%s has no published versions?", recipe.Name)) } for i := len(recipeMeta.Versions) - 1; i >= 0; i-- { table, err := formatter.CreateTable() if err != nil { log.Fatal(err) } table.Headers(i18n.G("SERVICE"), i18n.G("IMAGE"), i18n.G("TAG"), i18n.G("VERSION")) for version, meta := range recipeMeta.Versions[i] { var allRows [][]string var rows [][]string for service, serviceMeta := range meta { recipeVersion := version if service != "app" { recipeVersion = "" } rows = append(rows, []string{ service, serviceMeta.Image, serviceMeta.Tag, recipeVersion, }) allRows = append(allRows, []string{ version, service, serviceMeta.Image, serviceMeta.Tag, recipeVersion, }) } sort.Slice(rows, sortServiceByName(rows)) table.Rows(rows...) if !internal.MachineReadable { if err := formatter.PrintTable(table); err != nil { log.Fatal(err) } continue } if internal.MachineReadable { sort.Slice(allRows, sortServiceByName(allRows)) 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(i18n.G("unable to render to JSON: %s", err)) } fmt.Println(out) continue } } } if !internal.MachineReadable { for _, warnMsg := range warnMessages { log.Warn(warnMsg) } } }, }
Functions ¶
Types ¶
This section is empty.
Click to show internal directories.
Click to hide internal directories.