refactor: break up recipe cli package
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
This commit is contained in:
parent
ec40d88134
commit
48bcc9cb36
|
@ -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
|
||||||
|
},
|
||||||
|
}
|
|
@ -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
|
||||||
|
},
|
||||||
|
}
|
|
@ -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
|
||||||
|
},
|
||||||
|
}
|
|
@ -1,444 +1,10 @@
|
||||||
package recipe
|
package recipe
|
||||||
|
|
||||||
import (
|
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"
|
"github.com/urfave/cli/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var recipeListCommand = &cli.Command{
|
// RecipeCommand defines all recipe related sub-commands.
|
||||||
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
|
|
||||||
var RecipeCommand = &cli.Command{
|
var RecipeCommand = &cli.Command{
|
||||||
Name: "recipe",
|
Name: "recipe",
|
||||||
Usage: "Manage recipes",
|
Usage: "Manage recipes",
|
||||||
|
|
|
@ -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
|
||||||
|
},
|
||||||
|
}
|
|
@ -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
|
||||||
|
},
|
||||||
|
}
|
|
@ -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
|
||||||
|
},
|
||||||
|
}
|
Loading…
Reference in New Issue