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 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 ". `, ArgsUsage: "", 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:, // postgres:, 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 }, }