2021-09-05 20:33:07 +00:00
package recipe
import (
"fmt"
"sort"
"strings"
"coopcloud.tech/abra/cli/internal"
"coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/client"
"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
< 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 .
` ,
ArgsUsage : "<recipe>" ,
2021-10-01 17:48:48 +00:00
Flags : [ ] cli . Flag {
PatchFlag ,
MinorFlag ,
MajorFlag ,
} ,
2021-09-05 20:33:07 +00:00
Action : func ( c * cli . Context ) error {
2021-09-05 23:41:16 +00:00
recipe := internal . ValidateRecipe ( c )
2021-09-05 20:33:07 +00:00
2021-10-01 17:48:48 +00:00
bumpType := btoi ( Major ) * 4 + btoi ( Minor ) * 2 + btoi ( 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." )
}
}
2021-09-05 23:34:28 +00:00
for _ , service := range recipe . Config . Services {
2021-09-05 23:41:16 +00:00
catlVersions , err := catalogue . VersionsOfService ( recipe . Name , service . Name )
2021-09-05 20:33:07 +00:00
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 )
}
2021-09-10 22:54:02 +00:00
logrus . Debugf ( "retrieved '%s' from remote registry for '%s'" , regVersions , image )
2021-09-05 20:33:07 +00:00
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 ( ) ) {
2021-09-10 22:54:02 +00:00
logrus . Debugf ( "'%s' not considered semver-like" , img . ( reference . NamedTagged ) . Tag ( ) )
2021-09-05 20:33:07 +00:00
semverLikeTag = false
}
tag , err := tagcmp . Parse ( img . ( reference . NamedTagged ) . Tag ( ) )
if err != nil && semverLikeTag {
logrus . Fatal ( err )
}
2021-09-10 22:54:02 +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 . 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 )
}
}
2021-09-10 22:54:02 +00:00
logrus . Debugf ( "detected potential upgradable tags '%s' for '%s'" , compatible , service . Name )
2021-09-06 10:22:45 +00:00
sort . Sort ( tagcmp . ByTagDesc ( compatible ) )
2021-09-05 20:33:07 +00:00
if len ( compatible ) == 0 && semverLikeTag {
2021-09-10 22:54:02 +00:00
logrus . Info ( fmt . Sprintf ( "no new versions available for '%s', '%s' is the latest" , image , tag ) )
2021-09-05 20:33:07 +00:00
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 ( ) )
}
}
2021-09-10 22:54:02 +00:00
logrus . Debugf ( "detected compatible upgradable tags '%s' for '%s'" , compatibleStrings , service . Name )
2021-10-01 18:33:24 +00:00
var upgradeTag string
2021-10-01 17:48:48 +00:00
if bumpType != 0 {
2021-10-01 18:33:24 +00:00
for _ , upTag := range compatible {
2021-10-11 14:39:25 +00:00
upElement , err := tag . UpgradeDelta ( upTag )
2021-10-01 18:33:24 +00:00
if err != nil {
return err
}
2021-10-11 14:39:25 +00:00
delta := upElement . UpgradeType ( )
2021-10-04 11:40:24 +00:00
if delta <= bumpType {
2021-10-01 18:33:24 +00:00
upgradeTag = upTag . String ( )
break
}
2021-10-01 17:48:48 +00:00
}
2021-10-05 09:39:05 +00:00
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
}
2021-10-01 17:48:48 +00:00
} else {
msg := fmt . Sprintf ( "upgrade to which tag? (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 ( "upgrade to which tag? (service: %s, tag: %s)" , service . Name , tag )
compatibleStrings = [ ] string { }
for _ , regVersion := range regVersions {
compatibleStrings = append ( compatibleStrings , regVersion . Name )
}
2021-09-05 20:33:07 +00:00
}
2021-10-01 17:48:48 +00:00
prompt := & survey . Select {
Message : msg ,
Options : compatibleStrings ,
}
if err := survey . AskOne ( prompt , & upgradeTag ) ; err != nil {
logrus . Fatal ( err )
}
2021-09-05 20:33:07 +00:00
}
2021-10-04 11:40:24 +00:00
2021-10-01 18:33:24 +00:00
if err := recipe . UpdateTag ( image , upgradeTag ) ; err != nil {
logrus . Fatal ( err )
}
2021-10-05 09:39:05 +00:00
logrus . Infof ( "tag upgraded from '%s' to '%s' for '%s'" , tag . String ( ) , upgradeTag , image )
2021-09-05 20:33:07 +00:00
}
return nil
} ,
}