refactor: break up recipe cli package
continuous-integration/drone/push Build is passing Details

This commit is contained in:
decentral1se 2021-09-05 22:33:07 +02:00
parent ec40d88134
commit 48bcc9cb36
No known key found for this signature in database
GPG Key ID: 5E2EF5A63E3718CC
7 changed files with 496 additions and 435 deletions

84
cli/recipe/create.go Normal file
View File

@ -0,0 +1,84 @@
package recipe
import (
"errors"
"fmt"
"os"
"path"
"text/template"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/config"
"github.com/go-git/go-git/v5"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var recipeCreateCommand = &cli.Command{
Name: "create",
Usage: "Create a new recipe",
Aliases: []string{"c"},
ArgsUsage: "<recipe>",
Action: func(c *cli.Context) error {
recipe := c.Args().First()
if recipe == "" {
internal.ShowSubcommandHelpAndError(c, errors.New("no recipe provided"))
}
directory := path.Join(config.APPS_DIR, recipe)
if _, err := os.Stat(directory); !os.IsNotExist(err) {
logrus.Fatalf("'%s' recipe directory already exists?", directory)
return nil
}
url := fmt.Sprintf("%s/example.git", config.REPOS_BASE_URL)
_, err := git.PlainClone(directory, false, &git.CloneOptions{URL: url, Tags: git.AllTags})
if err != nil {
logrus.Fatal(err)
return nil
}
gitRepo := path.Join(config.APPS_DIR, recipe, ".git")
if err := os.RemoveAll(gitRepo); err != nil {
logrus.Fatal(err)
return nil
}
toParse := []string{
path.Join(config.APPS_DIR, recipe, "README.md"),
path.Join(config.APPS_DIR, recipe, ".env.sample"),
path.Join(config.APPS_DIR, recipe, ".drone.yml"),
}
for _, path := range toParse {
file, err := os.OpenFile(path, os.O_RDWR, 0755)
if err != nil {
logrus.Fatal(err)
return nil
}
tpl, err := template.ParseFiles(path)
if err != nil {
logrus.Fatal(err)
return nil
}
// TODO: ask for description and probably other things so that the
// template repository is more "ready" to go than the current best-guess
// mode of templating
if err := tpl.Execute(file, struct {
Name string
Description string
}{recipe, "TODO"}); err != nil {
logrus.Fatal(err)
return nil
}
}
logrus.Infof(
"New recipe '%s' created in %s, happy hacking!\n",
recipe, path.Join(config.APPS_DIR, recipe),
)
return nil
},
}

108
cli/recipe/lint.go Normal file
View File

@ -0,0 +1,108 @@
package recipe
import (
"fmt"
"os"
"path/filepath"
"strconv"
"coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/client/stack"
loader "coopcloud.tech/abra/pkg/client/stack"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/tagcmp"
"github.com/docker/distribution/reference"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var recipeLintCommand = &cli.Command{
Name: "lint",
Usage: "Lint a recipe",
Aliases: []string{"l"},
ArgsUsage: "<recipe>",
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipeArg(c)
pattern := fmt.Sprintf("%s/%s/compose**yml", config.APPS_DIR, recipe)
composeFiles, err := filepath.Glob(pattern)
if err != nil {
logrus.Fatal(err)
}
opts := stack.Deploy{Composefiles: composeFiles}
compose, err := loader.LoadComposefile(opts, make(map[string]string))
if err != nil {
logrus.Fatal(err)
}
expectedVersion := false
if compose.Version == "3.8" {
expectedVersion = true
}
envSampleProvided := false
envSample := fmt.Sprintf("%s/%s/.env.sample", config.APPS_DIR, recipe)
if _, err := os.Stat(envSample); !os.IsNotExist(err) {
envSampleProvided = true
}
if err != nil {
logrus.Fatal(err)
}
serviceNamedApp := false
traefikEnabled := false
healthChecksForAllServices := true
allImagesTagged := true
noUnstableTags := true
semverLikeTags := true
for _, service := range compose.Services {
if service.Name == "app" {
serviceNamedApp = true
}
for label := range service.Deploy.Labels {
if label == "traefik.enable" {
if service.Deploy.Labels[label] == "true" {
traefikEnabled = true
}
}
}
img, err := reference.ParseNormalizedNamed(service.Image)
if err != nil {
logrus.Fatal(err)
}
if reference.IsNameOnly(img) {
allImagesTagged = false
}
tag := img.(reference.NamedTagged).Tag()
if tag == "latest" {
noUnstableTags = false
}
if !tagcmp.IsParsable(tag) {
semverLikeTags = false
}
if service.HealthCheck == nil {
healthChecksForAllServices = false
}
}
tableCol := []string{"Rule", "Satisfied"}
table := formatter.CreateTable(tableCol)
table.Append([]string{"Compose files have the expected version", strconv.FormatBool(expectedVersion)})
table.Append([]string{"Environment configuration is provided", strconv.FormatBool(envSampleProvided)})
table.Append([]string{"Recipe contains a service named 'app'", strconv.FormatBool(serviceNamedApp)})
table.Append([]string{"Traefik routing enabled on at least one service", strconv.FormatBool(traefikEnabled)})
table.Append([]string{"All services have a healthcheck enabled", strconv.FormatBool(healthChecksForAllServices)})
table.Append([]string{"All images are using a tag", strconv.FormatBool(allImagesTagged)})
table.Append([]string{"No usage of unstable 'latest' tags", strconv.FormatBool(noUnstableTags)})
table.Append([]string{"All tags are using a semver-like format", strconv.FormatBool(semverLikeTags)})
table.Render()
return nil
},
}

34
cli/recipe/list.go Normal file
View File

@ -0,0 +1,34 @@
package recipe
import (
"fmt"
"sort"
"coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/pkg/catalogue"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var recipeListCommand = &cli.Command{
Name: "list",
Usage: "List available recipes",
Aliases: []string{"ls"},
Action: func(c *cli.Context) error {
catl, err := catalogue.ReadRecipeCatalogue()
if err != nil {
logrus.Fatal(err.Error())
}
recipes := catl.Flatten()
sort.Sort(catalogue.ByRecipeName(recipes))
tableCol := []string{"Name", "Category", "Status"}
table := formatter.CreateTable(tableCol)
for _, recipe := range recipes {
status := fmt.Sprintf("%v", recipe.Features.Status)
tableRow := []string{recipe.Name, recipe.Category, status}
table.Append(tableRow)
}
table.Render()
return nil
},
}

View File

@ -1,444 +1,10 @@
package recipe
import (
"errors"
"fmt"
"os"
"path"
"path/filepath"
"sort"
"strconv"
"strings"
"text/template"
"coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/client/stack"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/tagcmp"
loader "coopcloud.tech/abra/pkg/client/stack"
"github.com/AlecAivazis/survey/v2"
"github.com/docker/distribution/reference"
"github.com/go-git/go-git/v5"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var recipeListCommand = &cli.Command{
Name: "list",
Usage: "List available recipes",
Aliases: []string{"ls"},
Action: func(c *cli.Context) error {
catl, err := catalogue.ReadRecipeCatalogue()
if err != nil {
logrus.Fatal(err.Error())
}
recipes := catl.Flatten()
sort.Sort(catalogue.ByRecipeName(recipes))
tableCol := []string{"Name", "Category", "Status"}
table := formatter.CreateTable(tableCol)
for _, recipe := range recipes {
status := fmt.Sprintf("%v", recipe.Features.Status)
tableRow := []string{recipe.Name, recipe.Category, status}
table.Append(tableRow)
}
table.Render()
return nil
},
}
var recipeVersionCommand = &cli.Command{
Name: "versions",
Usage: "List recipe versions",
Aliases: []string{"v"},
ArgsUsage: "<recipe>",
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipeArg(c)
catalogue, err := catalogue.ReadRecipeCatalogue()
if err != nil {
logrus.Fatal(err)
return nil
}
if recipe, ok := catalogue[recipe]; ok {
tableCol := []string{"Version", "Service", "Image", "Digest"}
table := formatter.CreateTable(tableCol)
for version := range recipe.Versions {
for service := range recipe.Versions[version] {
meta := recipe.Versions[version][service]
table.Append([]string{version, service, meta.Image, meta.Digest})
}
}
table.SetAutoMergeCells(true)
table.Render()
return nil
}
logrus.Fatalf("'%s' recipe doesn't exist?", recipe)
return nil
},
}
var recipeCreateCommand = &cli.Command{
Name: "create",
Usage: "Create a new recipe",
Aliases: []string{"c"},
ArgsUsage: "<recipe>",
Action: func(c *cli.Context) error {
recipe := c.Args().First()
if recipe == "" {
internal.ShowSubcommandHelpAndError(c, errors.New("no recipe provided"))
}
directory := path.Join(config.APPS_DIR, recipe)
if _, err := os.Stat(directory); !os.IsNotExist(err) {
logrus.Fatalf("'%s' recipe directory already exists?", directory)
return nil
}
url := fmt.Sprintf("%s/example.git", config.REPOS_BASE_URL)
_, err := git.PlainClone(directory, false, &git.CloneOptions{URL: url, Tags: git.AllTags})
if err != nil {
logrus.Fatal(err)
return nil
}
gitRepo := path.Join(config.APPS_DIR, recipe, ".git")
if err := os.RemoveAll(gitRepo); err != nil {
logrus.Fatal(err)
return nil
}
toParse := []string{
path.Join(config.APPS_DIR, recipe, "README.md"),
path.Join(config.APPS_DIR, recipe, ".env.sample"),
path.Join(config.APPS_DIR, recipe, ".drone.yml"),
}
for _, path := range toParse {
file, err := os.OpenFile(path, os.O_RDWR, 0755)
if err != nil {
logrus.Fatal(err)
return nil
}
tpl, err := template.ParseFiles(path)
if err != nil {
logrus.Fatal(err)
return nil
}
// TODO: ask for description and probably other things so that the
// template repository is more "ready" to go than the current best-guess
// mode of templating
if err := tpl.Execute(file, struct {
Name string
Description string
}{recipe, "TODO"}); err != nil {
logrus.Fatal(err)
return nil
}
}
logrus.Infof(
"New recipe '%s' created in %s, happy hacking!\n",
recipe, path.Join(config.APPS_DIR, recipe),
)
return nil
},
}
var recipeUpgradeCommand = &cli.Command{
Name: "upgrade",
Usage: "Upgrade recipe image tags",
Aliases: []string{"u"},
Description: `
This command reads and attempts to parse all image tags within the given
<recipe> configuration and prompt with more recent tags to upgrade to. 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.
This is step 1 of upgrading a recipe. Step 2 is running "abra recipe sync
<recipe>".
`,
ArgsUsage: "<recipe>",
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipeArg(c)
appFiles, err := config.LoadAppFiles("")
if err != nil {
logrus.Fatal(err)
}
appEnv, err := config.GetApp(appFiles, recipe)
if err != nil {
logrus.Fatal(err)
}
compose, err := config.GetAppComposeConfig(recipe, stack.Deploy{}, appEnv.Env)
if err != nil {
logrus.Fatal(err)
}
for _, service := range compose.Services {
catlVersions, err := catalogue.VersionsOfService(recipe, service.Name)
if err != nil {
logrus.Fatal(err)
}
img, err := reference.ParseNormalizedNamed(service.Image)
if err != nil {
logrus.Fatal(err)
}
image := reference.Path(img)
regVersions, err := client.GetRegistryTags(image)
if err != nil {
logrus.Fatal(err)
}
if strings.Contains(image, "library") {
// ParseNormalizedNamed prepends 'library' to images like nginx:<tag>,
// postgres:<tag>, i.e. images which do not have a username in the
// first position of the string
image = strings.Split(image, "/")[1]
}
semverLikeTag := true
if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) {
semverLikeTag = false
}
tag, err := tagcmp.Parse(img.(reference.NamedTagged).Tag())
if err != nil && semverLikeTag {
logrus.Fatal(err)
}
var compatible []tagcmp.Tag
for _, regVersion := range regVersions {
other, err := tagcmp.Parse(regVersion.Name)
if err != nil {
continue // skip tags that cannot be parsed
}
if tag.IsCompatible(other) && tag.IsLessThan(other) && !tag.Equals(other) {
compatible = append(compatible, other)
}
}
sort.Sort(tagcmp.ByTag(compatible))
if len(compatible) == 0 && semverLikeTag {
logrus.Info(fmt.Sprintf("No new versions available for '%s', '%s' is the latest", image, tag))
continue // skip on to the next tag and don't update any compose files
}
var compatibleStrings []string
for _, compat := range compatible {
skip := false
for _, catlVersion := range catlVersions {
if compat.String() == catlVersion {
skip = true
}
}
if !skip {
compatibleStrings = append(compatibleStrings, compat.String())
}
}
msg := fmt.Sprintf("Which tag would you like to upgrade to? (service: %s, tag: %s)", service.Name, tag)
if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) {
tag := img.(reference.NamedTagged).Tag()
logrus.Warning(fmt.Sprintf("Unable to determine versioning semantics of '%s', listing all tags...", tag))
msg = fmt.Sprintf("Which tag would you like to upgrade to? (service: %s, tag: %s)", service.Name, tag)
compatibleStrings = []string{}
for _, regVersion := range regVersions {
compatibleStrings = append(compatibleStrings, regVersion.Name)
}
}
var upgradeTag string
prompt := &survey.Select{
Message: msg,
Options: compatibleStrings,
}
if err := survey.AskOne(prompt, &upgradeTag); err != nil {
logrus.Fatal(err)
}
if err := config.UpdateAppComposeTag(recipe, image, upgradeTag, appEnv.Env); err != nil {
logrus.Fatal(err)
}
}
return nil
},
}
var recipeSyncCommand = &cli.Command{
Name: "sync",
Usage: "Generate new recipe labels",
Aliases: []string{"s"},
Description: `
This command will generate labels for each service which correspond to the
following format:
coop-cloud.${STACK_NAME}.${SERVICE_NAME}.version=${IMAGE_TAG}-${IMAGE_DIGEST}
The <recipe> configuration will be updated on the local file system. These
labels are consumed by abra in other command invocations and used to determine
the versioning metadata of up-and-running containers are.
`,
ArgsUsage: "<recipe>",
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipeArg(c)
appFiles, err := config.LoadAppFiles("")
if err != nil {
logrus.Fatal(err)
}
appEnv, err := config.GetApp(appFiles, recipe)
if err != nil {
logrus.Fatal(err)
}
compose, err := config.GetAppComposeConfig(recipe, stack.Deploy{}, appEnv.Env)
if err != nil {
logrus.Fatal(err)
}
hasAppService := false
for _, service := range compose.Services {
if service.Name == "app" {
hasAppService = true
}
}
if !hasAppService {
logrus.Fatal(fmt.Sprintf("No 'app' service defined in '%s', cannot proceed", recipe))
}
for _, service := range compose.Services {
img, _ := reference.ParseNormalizedNamed(service.Image)
if err != nil {
logrus.Fatal(err)
}
digest, err := client.GetTagDigest(img)
if err != nil {
logrus.Fatal(err)
}
tag := img.(reference.NamedTagged).Tag()
label := fmt.Sprintf("coop-cloud.${STACK_NAME}.%s.version=%s-%s", service.Name, tag, digest)
if err := config.UpdateAppComposeLabel(recipe, service.Name, label, appEnv.Env); err != nil {
logrus.Fatal(err)
}
}
return nil
},
}
var recipeLintCommand = &cli.Command{
Name: "lint",
Usage: "Lint a recipe",
Aliases: []string{"l"},
ArgsUsage: "<recipe>",
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipeArg(c)
pattern := fmt.Sprintf("%s/%s/compose**yml", config.APPS_DIR, recipe)
composeFiles, err := filepath.Glob(pattern)
if err != nil {
logrus.Fatal(err)
}
opts := stack.Deploy{Composefiles: composeFiles}
compose, err := loader.LoadComposefile(opts, make(map[string]string))
if err != nil {
logrus.Fatal(err)
}
expectedVersion := false
if compose.Version == "3.8" {
expectedVersion = true
}
envSampleProvided := false
envSample := fmt.Sprintf("%s/%s/.env.sample", config.APPS_DIR, recipe)
if _, err := os.Stat(envSample); !os.IsNotExist(err) {
envSampleProvided = true
}
if err != nil {
logrus.Fatal(err)
}
serviceNamedApp := false
traefikEnabled := false
healthChecksForAllServices := true
allImagesTagged := true
noUnstableTags := true
semverLikeTags := true
for _, service := range compose.Services {
if service.Name == "app" {
serviceNamedApp = true
}
for label := range service.Deploy.Labels {
if label == "traefik.enable" {
if service.Deploy.Labels[label] == "true" {
traefikEnabled = true
}
}
}
img, err := reference.ParseNormalizedNamed(service.Image)
if err != nil {
logrus.Fatal(err)
}
if reference.IsNameOnly(img) {
allImagesTagged = false
}
tag := img.(reference.NamedTagged).Tag()
if tag == "latest" {
noUnstableTags = false
}
if !tagcmp.IsParsable(tag) {
semverLikeTags = false
}
if service.HealthCheck == nil {
healthChecksForAllServices = false
}
}
tableCol := []string{"Rule", "Satisfied"}
table := formatter.CreateTable(tableCol)
table.Append([]string{"Compose files have the expected version", strconv.FormatBool(expectedVersion)})
table.Append([]string{"Environment configuration is provided", strconv.FormatBool(envSampleProvided)})
table.Append([]string{"Recipe contains a service named 'app'", strconv.FormatBool(serviceNamedApp)})
table.Append([]string{"Traefik routing enabled on at least one service", strconv.FormatBool(traefikEnabled)})
table.Append([]string{"All services have a healthcheck enabled", strconv.FormatBool(healthChecksForAllServices)})
table.Append([]string{"All images are using a tag", strconv.FormatBool(allImagesTagged)})
table.Append([]string{"No usage of unstable 'latest' tags", strconv.FormatBool(noUnstableTags)})
table.Append([]string{"All tags are using a semver-like format", strconv.FormatBool(semverLikeTags)})
table.Render()
return nil
},
}
// RecipeCommand defines the `abra recipe` command and ets subcommands
// RecipeCommand defines all recipe related sub-commands.
var RecipeCommand = &cli.Command{
Name: "recipe",
Usage: "Manage recipes",

79
cli/recipe/sync.go Normal file
View File

@ -0,0 +1,79 @@
package recipe
import (
"fmt"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/client/stack"
"coopcloud.tech/abra/pkg/config"
"github.com/docker/distribution/reference"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var recipeSyncCommand = &cli.Command{
Name: "sync",
Usage: "Generate new recipe labels",
Aliases: []string{"s"},
Description: `
This command will generate labels for each service which correspond to the
following format:
coop-cloud.${STACK_NAME}.${SERVICE_NAME}.version=${IMAGE_TAG}-${IMAGE_DIGEST}
The <recipe> configuration will be updated on the local file system. These
labels are consumed by abra in other command invocations and used to determine
the versioning metadata of up-and-running containers are.
`,
ArgsUsage: "<recipe>",
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipeArg(c)
appFiles, err := config.LoadAppFiles("")
if err != nil {
logrus.Fatal(err)
}
appEnv, err := config.GetApp(appFiles, recipe)
if err != nil {
logrus.Fatal(err)
}
compose, err := config.GetAppComposeConfig(recipe, stack.Deploy{}, appEnv.Env)
if err != nil {
logrus.Fatal(err)
}
hasAppService := false
for _, service := range compose.Services {
if service.Name == "app" {
hasAppService = true
}
}
if !hasAppService {
logrus.Fatal(fmt.Sprintf("No 'app' service defined in '%s', cannot proceed", recipe))
}
for _, service := range compose.Services {
img, _ := reference.ParseNormalizedNamed(service.Image)
if err != nil {
logrus.Fatal(err)
}
digest, err := client.GetTagDigest(img)
if err != nil {
logrus.Fatal(err)
}
tag := img.(reference.NamedTagged).Tag()
label := fmt.Sprintf("coop-cloud.${STACK_NAME}.%s.version=%s-%s", service.Name, tag, digest)
if err := config.UpdateAppComposeLabel(recipe, service.Name, label, appEnv.Env); err != nil {
logrus.Fatal(err)
}
}
return nil
},
}

148
cli/recipe/upgrade.go Normal file
View File

@ -0,0 +1,148 @@
package recipe
import (
"fmt"
"sort"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/client/stack"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2"
"github.com/docker/distribution/reference"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var recipeUpgradeCommand = &cli.Command{
Name: "upgrade",
Usage: "Upgrade recipe image tags",
Aliases: []string{"u"},
Description: `
This command reads and attempts to parse all image tags within the given
<recipe> configuration and prompt with more recent tags to upgrade to. 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.
This is step 1 of upgrading a recipe. Step 2 is running "abra recipe sync
<recipe>".
`,
ArgsUsage: "<recipe>",
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipeArg(c)
appFiles, err := config.LoadAppFiles("")
if err != nil {
logrus.Fatal(err)
}
appEnv, err := config.GetApp(appFiles, recipe)
if err != nil {
logrus.Fatal(err)
}
compose, err := config.GetAppComposeConfig(recipe, stack.Deploy{}, appEnv.Env)
if err != nil {
logrus.Fatal(err)
}
for _, service := range compose.Services {
catlVersions, err := catalogue.VersionsOfService(recipe, service.Name)
if err != nil {
logrus.Fatal(err)
}
img, err := reference.ParseNormalizedNamed(service.Image)
if err != nil {
logrus.Fatal(err)
}
image := reference.Path(img)
regVersions, err := client.GetRegistryTags(image)
if err != nil {
logrus.Fatal(err)
}
if strings.Contains(image, "library") {
// ParseNormalizedNamed prepends 'library' to images like nginx:<tag>,
// postgres:<tag>, i.e. images which do not have a username in the
// first position of the string
image = strings.Split(image, "/")[1]
}
semverLikeTag := true
if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) {
semverLikeTag = false
}
tag, err := tagcmp.Parse(img.(reference.NamedTagged).Tag())
if err != nil && semverLikeTag {
logrus.Fatal(err)
}
var compatible []tagcmp.Tag
for _, regVersion := range regVersions {
other, err := tagcmp.Parse(regVersion.Name)
if err != nil {
continue // skip tags that cannot be parsed
}
if tag.IsCompatible(other) && tag.IsLessThan(other) && !tag.Equals(other) {
compatible = append(compatible, other)
}
}
sort.Sort(tagcmp.ByTag(compatible))
if len(compatible) == 0 && semverLikeTag {
logrus.Info(fmt.Sprintf("No new versions available for '%s', '%s' is the latest", image, tag))
continue // skip on to the next tag and don't update any compose files
}
var compatibleStrings []string
for _, compat := range compatible {
skip := false
for _, catlVersion := range catlVersions {
if compat.String() == catlVersion {
skip = true
}
}
if !skip {
compatibleStrings = append(compatibleStrings, compat.String())
}
}
msg := fmt.Sprintf("Which tag would you like to upgrade to? (service: %s, tag: %s)", service.Name, tag)
if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) {
tag := img.(reference.NamedTagged).Tag()
logrus.Warning(fmt.Sprintf("Unable to determine versioning semantics of '%s', listing all tags...", tag))
msg = fmt.Sprintf("Which tag would you like to upgrade to? (service: %s, tag: %s)", service.Name, tag)
compatibleStrings = []string{}
for _, regVersion := range regVersions {
compatibleStrings = append(compatibleStrings, regVersion.Name)
}
}
var upgradeTag string
prompt := &survey.Select{
Message: msg,
Options: compatibleStrings,
}
if err := survey.AskOne(prompt, &upgradeTag); err != nil {
logrus.Fatal(err)
}
if err := config.UpdateAppComposeTag(recipe, image, upgradeTag, appEnv.Env); err != nil {
logrus.Fatal(err)
}
}
return nil
},
}

42
cli/recipe/version.go Normal file
View File

@ -0,0 +1,42 @@
package recipe
import (
"coopcloud.tech/abra/cli/formatter"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/catalogue"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
var recipeVersionCommand = &cli.Command{
Name: "versions",
Usage: "List recipe versions",
Aliases: []string{"v"},
ArgsUsage: "<recipe>",
Action: func(c *cli.Context) error {
recipe := internal.ValidateRecipeArg(c)
catalogue, err := catalogue.ReadRecipeCatalogue()
if err != nil {
logrus.Fatal(err)
return nil
}
if recipe, ok := catalogue[recipe]; ok {
tableCol := []string{"Version", "Service", "Image", "Digest"}
table := formatter.CreateTable(tableCol)
for version := range recipe.Versions {
for service := range recipe.Versions[version] {
meta := recipe.Versions[version][service]
table.Append([]string{version, service, meta.Image, meta.Digest})
}
}
table.SetAutoMergeCells(true)
table.Render()
return nil
}
logrus.Fatalf("'%s' recipe doesn't exist?", recipe)
return nil
},
}