2021-09-05 20:33:07 +00:00
package recipe
import (
2021-11-01 10:32:47 +00:00
"bufio"
2023-04-13 16:28:34 +00:00
"encoding/json"
2021-09-05 20:33:07 +00:00
"fmt"
2021-11-01 10:32:47 +00:00
"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"
2021-11-01 10:32:47 +00:00
"coopcloud.tech/abra/pkg/config"
2022-01-19 09:40:14 +00:00
"coopcloud.tech/abra/pkg/formatter"
2023-10-15 11:39:04 +00:00
gitPkg "coopcloud.tech/abra/pkg/git"
2021-12-27 15:40:59 +00:00
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"
2022-01-18 13:13:20 +00:00
"github.com/urfave/cli"
2021-09-05 20:33:07 +00:00
)
2021-11-01 10:32:47 +00:00
type imgPin struct {
image string
version tagcmp . Tag
}
2023-04-27 16:43:47 +00:00
// anUpgrade represents a single service upgrade (as within a recipe), and the list of tags that it can be upgraded to,
// for serialization purposes.
2023-04-13 16:28:34 +00:00
type anUpgrade struct {
Service string ` json:"service" `
Image string ` json:"image" `
Tag string ` json:"tag" `
UpgradeTags [ ] string ` json:"upgrades" `
2023-04-12 22:25:48 +00:00
}
2022-01-18 13:13:20 +00:00
var recipeUpgradeCommand = cli . Command {
2021-09-05 20:33:07 +00:00
Name : "upgrade" ,
Aliases : [ ] string { "u" } ,
2022-01-18 13:13:20 +00:00
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 .
2021-11-09 17:06:06 +00:00
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 .
2021-11-09 17:06:06 +00:00
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>" ,
2021-10-01 17:48:48 +00:00
Flags : [ ] cli . Flag {
2022-01-18 13:13:20 +00:00
internal . DebugFlag ,
internal . NoInputFlag ,
2021-11-06 22:40:22 +00:00
internal . PatchFlag ,
internal . MinorFlag ,
internal . MajorFlag ,
2023-04-13 16:28:34 +00:00
internal . MachineReadableFlag ,
2022-01-17 20:59:31 +00:00
internal . AllTagsFlag ,
2021-10-01 17:48:48 +00:00
} ,
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 )
2023-09-21 08:36:53 +00:00
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 {
2022-04-20 10:31:21 +00:00
logrus . Fatal ( err )
}
2021-11-06 22:40:22 +00:00
bumpType := btoi ( internal . Major ) * 4 + btoi ( internal . Minor ) * 2 + btoi ( internal . Patch )
2021-10-01 17:48:48 +00:00
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
}
2023-04-13 16:28:34 +00:00
upgradeList := make ( map [ string ] anUpgrade )
2023-04-12 22:25:48 +00:00
2021-11-01 10:32:47 +00:00
// 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 )
2021-11-01 10:32:47 +00:00
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 )
}
2021-09-05 23:34:28 +00:00
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 )
}
2022-02-20 13:38:44 +00:00
regVersions , err := client . GetRegistryTags ( img )
2021-09-05 20:33:07 +00:00
if err != nil {
logrus . Fatal ( err )
}
2022-02-20 13:38:44 +00:00
image := reference . Path ( img )
logrus . Debugf ( "retrieved %s from remote registry for %s" , regVersions , image )
2022-01-19 09:40:14 +00:00
image = formatter . StripTagMeta ( image )
2022-01-05 16:32:58 +00:00
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 ( ) )
2022-01-05 16:32:58 +00:00
if err != nil {
2022-01-05 16:57:11 +00:00
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
}
2022-01-05 16:57:11 +00:00
2021-12-19 23:15:55 +00:00
logrus . Debugf ( "parsed %s for %s" , tag , service . Name )
2022-01-05 16:57:11 +00:00
2021-09-05 20:33:07 +00:00
var compatible [ ] tagcmp . Tag
for _ , regVersion := range regVersions {
2022-02-20 13:38:44 +00:00
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 )
2021-09-10 22:54:02 +00:00
2021-09-06 10:22:45 +00:00
sort . Sort ( tagcmp . ByTagDesc ( compatible ) )
2021-09-05 20:33:07 +00:00
2022-01-17 20:59:31 +00:00
if len ( compatible ) == 0 && ! internal . AllTags {
2022-01-19 09:40:37 +00:00
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 )
2021-11-01 10:32:47 +00:00
2021-10-01 18:33:24 +00:00
var upgradeTag string
2021-11-01 10:32:47 +00:00
_ , 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
}
2021-10-01 18:33:24 +00:00
}
2021-11-01 10:32:47 +00:00
if contains {
2022-01-04 21:49:23 +00:00
logrus . Infof ( "upgrading service %s from %s to %s (pinned tag: %s)" , service . Name , tag . String ( ) , upgradeTag , pinnedTagString )
2021-11-01 10:32:47 +00:00
} else {
2021-11-06 22:40:22 +00:00
logrus . Infof ( "service %s, image %s pinned to %s, no compatible upgrade found" , service . Name , servicePins [ service . Name ] . image , pinnedTagString )
2021-11-01 10:32:47 +00:00
continue
2021-10-01 18:33:24 +00:00
}
2021-11-01 10:32:47 +00:00
} else {
2022-01-04 21:49:23 +00:00
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 ( ) )
2021-10-05 09:39:05 +00:00
continue
}
2021-10-01 17:48:48 +00:00
} else {
2021-11-01 10:32:47 +00:00
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 == "" {
2022-01-04 21:49:23 +00:00
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 )
2021-11-01 10:32:47 +00:00
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 )
2022-01-17 20:59:31 +00:00
if ! tagcmp . IsParsable ( img . ( reference . NamedTagged ) . Tag ( ) ) || internal . AllTags {
2021-11-01 10:32:47 +00:00
tag := img . ( reference . NamedTagged ) . Tag ( )
2022-01-17 20:59:31 +00:00
if ! internal . AllTags {
logrus . Warning ( fmt . Sprintf ( "unable to determine versioning semantics of %s, listing all tags" , tag ) )
}
2021-11-01 10:32:47 +00:00
msg = fmt . Sprintf ( "upgrade to which tag? (service: %s, tag: %s)" , service . Name , tag )
2022-01-04 21:49:36 +00:00
compatibleStrings = [ ] string { "skip" }
2021-11-01 10:32:47 +00:00
for _ , regVersion := range regVersions {
2022-02-20 13:38:44 +00:00
compatibleStrings = append ( compatibleStrings , regVersion )
2021-11-01 10:32:47 +00:00
}
2021-10-01 17:48:48 +00:00
}
2023-04-12 22:25:48 +00:00
2023-04-27 16:43:47 +00:00
// 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 : ]
2023-04-13 16:28:34 +00:00
upgrade := anUpgrade {
Service : service . Name ,
Image : image ,
Tag : tag . String ( ) ,
2023-04-27 16:43:47 +00:00
UpgradeTags : make ( [ ] string , len ( upgradableTags ) ) ,
2023-04-12 22:25:48 +00:00
}
2023-04-27 16:43:47 +00:00
for n , s := range upgradableTags {
2023-04-12 22:25:48 +00:00
var sb strings . Builder
if _ , err := sb . WriteString ( s ) ; err != nil {
2023-04-12 21:58:21 +00:00
}
2023-04-13 16:28:34 +00:00
upgrade . UpgradeTags [ n ] = sb . String ( )
2023-04-12 22:25:48 +00:00
}
2023-04-13 16:28:34 +00:00
upgradeList [ upgrade . Service ] = upgrade
2023-04-12 22:25:48 +00:00
if internal . NoInput {
upgradeTag = "skip"
2023-04-12 21:58:21 +00:00
} 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-11-01 10:32:47 +00:00
}
2021-10-01 17:48:48 +00:00
}
2021-09-05 20:33:07 +00:00
}
2022-01-02 14:46:35 +00:00
if upgradeTag != "skip" {
2022-01-05 16:57:48 +00:00
ok , err := recipe . UpdateTag ( image , upgradeTag )
if err != nil {
2022-01-02 14:46:35 +00:00
logrus . Fatal ( err )
}
2022-01-05 16:57:48 +00:00
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 {
2023-04-12 22:25:48 +00:00
if ! internal . NoInput {
logrus . Warnf ( "not upgrading %s, skipping as requested" , image )
}
2021-10-01 18:33:24 +00:00
}
2021-09-05 20:33:07 +00:00
}
2023-04-12 22:25:48 +00:00
if internal . NoInput {
2023-04-13 16:28:34 +00:00
if internal . MachineReadable {
jsonstring , err := json . Marshal ( upgradeList )
if err != nil {
logrus . Fatal ( err )
}
2023-04-27 16:43:47 +00:00
fmt . Println ( string ( jsonstring ) )
2023-10-15 11:39:04 +00:00
2023-04-27 16:43:47 +00:00
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 )
2023-04-12 22:25:48 +00:00
}
}
}
2023-10-15 11:39:04 +00:00
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
} ,
}