Catalogue package had to be merged into the recipe package due to too many circular import errors. Also, use https url for cloning, assume folks don't have ssh setup by default (the whole reason for the refactor).
241 lines
7.2 KiB
Go
241 lines
7.2 KiB
Go
package recipe
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"sort"
|
|
"strings"
|
|
|
|
"coopcloud.tech/abra/cli/internal"
|
|
"coopcloud.tech/abra/pkg/client"
|
|
"coopcloud.tech/abra/pkg/config"
|
|
recipePkg "coopcloud.tech/abra/pkg/recipe"
|
|
"coopcloud.tech/tagcmp"
|
|
"github.com/AlecAivazis/survey/v2"
|
|
"github.com/docker/distribution/reference"
|
|
"github.com/sirupsen/logrus"
|
|
"github.com/urfave/cli/v2"
|
|
)
|
|
|
|
type imgPin struct {
|
|
image string
|
|
version tagcmp.Tag
|
|
}
|
|
|
|
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.
|
|
|
|
You may invoke this command in "wizard" mode and be prompted for input:
|
|
|
|
abra recipe upgrade
|
|
|
|
`,
|
|
ArgsUsage: "<recipe>",
|
|
Flags: []cli.Flag{
|
|
internal.PatchFlag,
|
|
internal.MinorFlag,
|
|
internal.MajorFlag,
|
|
},
|
|
Action: func(c *cli.Context) error {
|
|
recipe := internal.ValidateRecipeWithPrompt(c)
|
|
|
|
bumpType := btoi(internal.Major)*4 + btoi(internal.Minor)*2 + btoi(internal.Patch)
|
|
if bumpType != 0 {
|
|
// a bitwise check if the number is a power of 2
|
|
if (bumpType & (bumpType - 1)) != 0 {
|
|
logrus.Fatal("you can only use one of: --major, --minor, --patch.")
|
|
}
|
|
}
|
|
|
|
// check for versions file and load pinned versions
|
|
versionsPresent := false
|
|
recipeDir := path.Join(config.RECIPES_DIR, recipe.Name)
|
|
versionsPath := path.Join(recipeDir, "versions")
|
|
var servicePins = make(map[string]imgPin)
|
|
if _, err := os.Stat(versionsPath); err == nil {
|
|
logrus.Debugf("found versions file for %s", recipe.Name)
|
|
file, err := os.Open(versionsPath)
|
|
if err != nil {
|
|
logrus.Fatal(err)
|
|
}
|
|
scanner := bufio.NewScanner(file)
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
splitLine := strings.Split(line, " ")
|
|
if splitLine[0] != "pin" || len(splitLine) != 3 {
|
|
logrus.Fatalf("malformed version pin specification: %s", line)
|
|
}
|
|
pinSlice := strings.Split(splitLine[2], ":")
|
|
pinTag, err := tagcmp.Parse(pinSlice[1])
|
|
if err != nil {
|
|
logrus.Fatal(err)
|
|
}
|
|
pin := imgPin{
|
|
image: pinSlice[0],
|
|
version: pinTag,
|
|
}
|
|
servicePins[splitLine[1]] = pin
|
|
}
|
|
if err := scanner.Err(); err != nil {
|
|
logrus.Error(err)
|
|
}
|
|
versionsPresent = true
|
|
} else {
|
|
logrus.Debugf("did not find versions file for %s", recipe.Name)
|
|
}
|
|
|
|
for _, service := range recipe.Config.Services {
|
|
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)
|
|
}
|
|
logrus.Debugf("retrieved %s from remote registry for %s", regVersions, image)
|
|
|
|
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()) {
|
|
logrus.Debugf("%s not considered semver-like", img.(reference.NamedTagged).Tag())
|
|
semverLikeTag = false
|
|
}
|
|
|
|
tag, err := tagcmp.Parse(img.(reference.NamedTagged).Tag())
|
|
if err != nil && semverLikeTag {
|
|
logrus.Fatal(err)
|
|
}
|
|
logrus.Debugf("parsed %s for %s", tag, service.Name)
|
|
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)
|
|
}
|
|
}
|
|
|
|
logrus.Debugf("detected potential upgradable tags %s for %s", compatible, service.Name)
|
|
|
|
sort.Sort(tagcmp.ByTagDesc(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
|
|
}
|
|
|
|
catlVersions, err := recipePkg.VersionsOfService(recipe.Name, service.Name)
|
|
if err != nil {
|
|
logrus.Fatal(err)
|
|
}
|
|
|
|
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())
|
|
}
|
|
}
|
|
|
|
logrus.Debugf("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 {
|
|
logrus.Infof("Upgrading service %s from %s to %s (pinned tag: %s)", service.Name, tag.String(), upgradeTag, pinnedTagString)
|
|
} else {
|
|
logrus.Infof("service %s, image %s pinned to %s, no compatible upgrade found", service.Name, servicePins[service.Name].image, pinnedTagString)
|
|
continue
|
|
}
|
|
} else {
|
|
logrus.Fatalf("Service %s is at version %s, but pinned to %s, please correct your compose.yml file manually!", service.Name, tag.String(), pinnedTag.String())
|
|
continue
|
|
}
|
|
} else {
|
|
if bumpType != 0 {
|
|
for _, upTag := range compatible {
|
|
upElement, err := tag.UpgradeDelta(upTag)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
delta := upElement.UpgradeType()
|
|
if delta <= bumpType {
|
|
upgradeTag = upTag.String()
|
|
break
|
|
}
|
|
}
|
|
if upgradeTag == "" {
|
|
logrus.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)
|
|
continue
|
|
}
|
|
} else {
|
|
msg := fmt.Sprintf("upgrade to which tag? (service: %s, image: %s, tag: %s)", service.Name, image, 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("upgrade to which tag? (service: %s, tag: %s)", service.Name, tag)
|
|
compatibleStrings = []string{}
|
|
for _, regVersion := range regVersions {
|
|
compatibleStrings = append(compatibleStrings, regVersion.Name)
|
|
}
|
|
}
|
|
|
|
prompt := &survey.Select{
|
|
Message: msg,
|
|
Options: compatibleStrings,
|
|
}
|
|
if err := survey.AskOne(prompt, &upgradeTag); err != nil {
|
|
logrus.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
if err := recipe.UpdateTag(image, upgradeTag); err != nil {
|
|
logrus.Fatal(err)
|
|
}
|
|
logrus.Infof("tag upgraded from %s to %s for %s", tag.String(), upgradeTag, image)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}
|