See coop-cloud/organising#258 This fixes also how we read the digest of the image. I think it was wrong before. Some registries restrict reading this info and we now just default to "unknown" for that case. This also appears to bring a wave of new dependencies due to the generic handling logic of containers/... package. The abra binary is now 1mb larger. The catalogue generation is now slower unfortunately. But it is more robust. The generic logic looks in ~/.docker/config.json for log in details, so you don't have to pass those in manually on the CLI anymore. We just read those defaults. You can "docker login" to get credentials setup in that file. Since most folks won't generate the catalogue, this seems fine for now.
266 lines
8.0 KiB
Go
266 lines
8.0 KiB
Go
package recipe
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"sort"
|
|
"strings"
|
|
|
|
"coopcloud.tech/abra/cli/internal"
|
|
"coopcloud.tech/abra/pkg/autocomplete"
|
|
"coopcloud.tech/abra/pkg/client"
|
|
"coopcloud.tech/abra/pkg/config"
|
|
"coopcloud.tech/abra/pkg/formatter"
|
|
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"
|
|
)
|
|
|
|
type imgPin struct {
|
|
image string
|
|
version tagcmp.Tag
|
|
}
|
|
|
|
var recipeUpgradeCommand = cli.Command{
|
|
Name: "upgrade",
|
|
Aliases: []string{"u"},
|
|
Usage: "Upgrade recipe image tags",
|
|
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.
|
|
|
|
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
|
|
|
|
`,
|
|
BashComplete: autocomplete.RecipeNameComplete,
|
|
ArgsUsage: "<recipe>",
|
|
Flags: []cli.Flag{
|
|
internal.DebugFlag,
|
|
internal.NoInputFlag,
|
|
internal.PatchFlag,
|
|
internal.MinorFlag,
|
|
internal.MajorFlag,
|
|
internal.AllTagsFlag,
|
|
},
|
|
Before: internal.SubCommandBefore,
|
|
Action: func(c *cli.Context) error {
|
|
recipe := internal.ValidateRecipeWithPrompt(c, true)
|
|
|
|
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)
|
|
}
|
|
|
|
regVersions, err := client.GetRegistryTags(img)
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
logrus.Debugf("parsed %s for %s", tag, service.Name)
|
|
|
|
var compatible []tagcmp.Tag
|
|
for _, regVersion := range regVersions {
|
|
other, err := tagcmp.Parse(regVersion)
|
|
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 && !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))
|
|
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)
|
|
}
|
|
|
|
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())
|
|
}
|
|
}
|
|
|
|
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()) || 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)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
if upgradeTag != "skip" {
|
|
ok, err := recipe.UpdateTag(image, upgradeTag)
|
|
if err != nil {
|
|
logrus.Fatal(err)
|
|
}
|
|
if ok {
|
|
logrus.Infof("tag upgraded from %s to %s for %s", tag.String(), upgradeTag, image)
|
|
}
|
|
} else {
|
|
logrus.Warnf("not upgrading %s, skipping as requested", image)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}
|