abra/cli/recipe/upgrade.go

356 lines
10 KiB
Go
Raw Normal View History

2021-09-05 20:33:07 +00:00
package recipe
import (
"bufio"
"encoding/json"
2021-09-05 20:33:07 +00:00
"fmt"
"os"
"path"
2021-09-05 20:33:07 +00:00
"sort"
"strings"
"coopcloud.tech/abra/cli/internal"
2021-12-27 18:56:27 +00:00
"coopcloud.tech/abra/pkg/autocomplete"
2021-09-05 20:33:07 +00:00
"coopcloud.tech/abra/pkg/client"
"coopcloud.tech/abra/pkg/config"
"coopcloud.tech/abra/pkg/formatter"
gitPkg "coopcloud.tech/abra/pkg/git"
recipePkg "coopcloud.tech/abra/pkg/recipe"
2021-09-05 20:33:07 +00:00
"coopcloud.tech/tagcmp"
"github.com/AlecAivazis/survey/v2"
"github.com/docker/distribution/reference"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
2021-09-05 20:33:07 +00:00
)
type imgPin struct {
image string
version tagcmp.Tag
}
// anUpgrade represents a single service upgrade (as within a recipe), and the list of tags that it can be upgraded to,
// for serialization purposes.
type anUpgrade struct {
Service string `json:"service"`
Image string `json:"image"`
Tag string `json:"tag"`
UpgradeTags []string `json:"upgrades"`
}
var recipeUpgradeCommand = cli.Command{
2021-09-05 20:33:07 +00:00
Name: "upgrade",
Aliases: []string{"u"},
Usage: "Upgrade recipe image tags",
2021-09-05 20:33:07 +00:00
Description: `
2022-05-13 14:44:49 +00:00
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.
2021-09-05 20:33:07 +00:00
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.
2022-01-02 14:46:35 +00:00
The command is interactive and will show a select input which allows you to
make a seclection. Use the "?" key to see more help on navigating this
interface.
You may invoke this command in "wizard" mode and be prompted for input:
abra recipe upgrade
2021-09-05 20:33:07 +00:00
`,
2023-09-07 16:50:25 +00:00
ArgsUsage: "<recipe>",
Flags: []cli.Flag{
internal.DebugFlag,
internal.NoInputFlag,
internal.PatchFlag,
internal.MinorFlag,
internal.MajorFlag,
internal.MachineReadableFlag,
internal.AllTagsFlag,
},
2023-09-07 16:50:25 +00:00
Before: internal.SubCommandBefore,
BashComplete: autocomplete.RecipeNameComplete,
2021-09-05 20:33:07 +00:00
Action: func(c *cli.Context) error {
2023-09-07 16:50:25 +00:00
recipe := internal.ValidateRecipe(c)
if err := recipePkg.EnsureIsClean(recipe.Name); err != nil {
logrus.Fatal(err)
}
if err := recipePkg.EnsureExists(recipe.Name); err != nil {
logrus.Fatal(err)
}
2023-09-07 16:50:25 +00:00
if err := recipePkg.EnsureUpToDate(recipe.Name); err != nil {
logrus.Fatal(err)
}
2021-09-05 20:33:07 +00:00
2023-09-07 16:50:25 +00:00
if err := recipePkg.EnsureLatest(recipe.Name); err != nil {
logrus.Fatal(err)
}
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.")
}
}
2023-04-13 16:32:09 +00:00
if internal.MachineReadable {
// -m implies -n in this case
internal.NoInput = true
}
upgradeList := make(map[string]anUpgrade)
// check for versions file and load pinned versions
versionsPresent := false
2021-12-25 13:04:07 +00:00
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 {
2021-09-05 20:33:07 +00:00
img, err := reference.ParseNormalizedNamed(service.Image)
if err != nil {
logrus.Fatal(err)
}
regVersions, err := client.GetRegistryTags(img)
2021-09-05 20:33:07 +00:00
if err != nil {
logrus.Fatal(err)
}
image := reference.Path(img)
logrus.Debugf("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()) {
logrus.Debugf("%s not considered semver-like", img.(reference.NamedTagged).Tag())
}
default:
logrus.Warnf("unable to read tag for image %s, is it missing? skipping upgrade for %s", image, service.Name)
continue
2021-09-05 20:33:07 +00:00
}
tag, err := tagcmp.Parse(img.(reference.NamedTagged).Tag())
if err != nil {
logrus.Warnf("unable to parse %s, error was: %s, skipping upgrade for %s", image, err.Error(), service.Name)
continue
2021-09-05 20:33:07 +00:00
}
2021-12-19 23:15:55 +00:00
logrus.Debugf("parsed %s for %s", tag, service.Name)
2021-09-05 20:33:07 +00:00
var compatible []tagcmp.Tag
for _, regVersion := range regVersions {
other, err := tagcmp.Parse(regVersion)
2021-09-05 20:33:07 +00:00
if err != nil {
continue // skip tags that cannot be parsed
}
if tag.IsCompatible(other) && tag.IsLessThan(other) && !tag.Equals(other) {
compatible = append(compatible, other)
}
}
2021-12-19 23:15:55 +00:00
logrus.Debugf("detected potential upgradable tags %s for %s", compatible, service.Name)
sort.Sort(tagcmp.ByTagDesc(compatible))
2021-09-05 20:33:07 +00:00
if len(compatible) == 0 && !internal.AllTags {
logrus.Info(fmt.Sprintf("no new versions available for %s, assuming %s is the latest (use -a/--all-tags to see all anyway)", image, tag))
2021-09-05 20:33:07 +00:00
continue // skip on to the next tag and don't update any compose files
}
2023-09-07 16:50:25 +00:00
catlVersions, err := recipePkg.VersionsOfService(recipe.Name, service.Name, internal.Offline)
2021-12-19 23:50:09 +00:00
if err != nil {
logrus.Fatal(err)
}
2022-01-02 14:46:35 +00:00
compatibleStrings := []string{"skip"}
2021-09-05 20:33:07 +00:00
for _, compat := range compatible {
skip := false
for _, catlVersion := range catlVersions {
if compat.String() == catlVersion {
skip = true
}
}
if !skip {
compatibleStrings = append(compatibleStrings, compat.String())
}
}
2021-12-19 23:15:55 +00:00
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 {
2021-12-23 00:56:09 +00:00
msg := fmt.Sprintf("upgrade to which tag? (service: %s, image: %s, tag: %s)", service.Name, image, tag)
if !tagcmp.IsParsable(img.(reference.NamedTagged).Tag()) || internal.AllTags {
tag := img.(reference.NamedTagged).Tag()
if !internal.AllTags {
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{"skip"}
for _, regVersion := range regVersions {
compatibleStrings = append(compatibleStrings, regVersion)
}
}
// there is always at least the item "skip" in compatibleStrings (a list of
// possible upgradable tags) and at least one other tag.
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: "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 {
logrus.Fatal(err)
}
}
}
2021-09-05 20:33:07 +00:00
}
2022-01-02 14:46:35 +00:00
if upgradeTag != "skip" {
ok, err := recipe.UpdateTag(image, upgradeTag)
if err != nil {
2022-01-02 14:46:35 +00:00
logrus.Fatal(err)
}
if ok {
logrus.Infof("tag upgraded from %s to %s for %s", tag.String(), upgradeTag, image)
}
2022-01-02 14:46:35 +00:00
} else {
if !internal.NoInput {
logrus.Warnf("not upgrading %s, skipping as requested", image)
}
}
2021-09-05 20:33:07 +00:00
}
if internal.NoInput {
if internal.MachineReadable {
jsonstring, err := json.Marshal(upgradeList)
if err != nil {
logrus.Fatal(err)
}
fmt.Println(string(jsonstring))
return nil
}
for _, upgrade := range upgradeList {
logrus.Infof("can upgrade service: %s, image: %s, tag: %s ::\n", upgrade.Service, upgrade.Image, upgrade.Tag)
for _, utag := range upgrade.UpgradeTags {
logrus.Infof(" %s\n", utag)
}
}
}
isClean, err := gitPkg.IsClean(recipeDir)
if err != nil {
logrus.Fatal(err)
}
if !isClean {
logrus.Infof("%s currently has these unstaged changes 👇", recipe.Name)
if err := gitPkg.DiffUnstaged(recipeDir); err != nil {
logrus.Fatal(err)
}
}
2021-09-05 20:33:07 +00:00
return nil
},
}