2021-09-05 20:33:07 +00:00
package recipe
import (
2021-11-01 10:32:47 +00:00
"bufio"
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"
"coopcloud.tech/abra/pkg/catalogue"
"coopcloud.tech/abra/pkg/client"
2021-11-01 10:32:47 +00:00
"coopcloud.tech/abra/pkg/config"
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/v2"
)
2021-11-01 10:32:47 +00:00
type imgPin struct {
image string
version tagcmp . Tag
}
2021-09-05 20:33:07 +00:00
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 .
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
` ,
ArgsUsage : "<recipe>" ,
2021-10-01 17:48:48 +00:00
Flags : [ ] cli . Flag {
2021-11-06 22:40:22 +00:00
internal . PatchFlag ,
internal . MinorFlag ,
internal . MajorFlag ,
2021-10-01 17:48:48 +00:00
} ,
2021-09-05 20:33:07 +00:00
Action : func ( c * cli . Context ) error {
2021-11-09 17:06:06 +00:00
recipe := internal . ValidateRecipeWithPrompt ( c )
2021-09-05 20:33:07 +00:00
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." )
}
}
2021-11-01 10:32:47 +00:00
// check for versions file and load pinned versions
versionsPresent := false
recipeDir := path . Join ( config . ABRA_DIR , "apps" , 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 )
}
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 )
}
image := reference . Path ( img )
regVersions , err := client . GetRegistryTags ( image )
if err != nil {
logrus . Fatal ( err )
}
2021-12-19 23:49:36 +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-12-19 23:15:55 +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-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 . 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-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
if len ( compatible ) == 0 && semverLikeTag {
2021-12-19 23:49:36 +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
}
2021-12-19 23:50:09 +00:00
catlVersions , err := catalogue . VersionsOfService ( recipe . Name , service . Name )
if err != nil {
logrus . Fatal ( err )
}
2021-09-05 20:33:07 +00:00
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-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 {
logrus . Infof ( "Upgrading service %s from %s to %s (pinned tag: %s)" , service . Name , tag . String ( ) , upgradeTag , pinnedTagString )
} 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 {
2021-11-06 22:40:22 +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 == "" {
2021-12-19 23:15:55 +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 {
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 ( )
2021-12-19 23:15:55 +00:00
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 )
compatibleStrings = [ ] string { }
for _ , regVersion := range regVersions {
compatibleStrings = append ( compatibleStrings , regVersion . Name )
}
2021-10-01 17:48:48 +00:00
}
2021-09-05 20:33:07 +00:00
2021-11-01 10:32:47 +00:00
prompt := & survey . Select {
Message : msg ,
Options : compatibleStrings ,
}
if err := survey . AskOne ( prompt , & upgradeTag ) ; err != nil {
logrus . Fatal ( err )
}
2021-10-01 17:48:48 +00:00
}
2021-09-05 20:33:07 +00:00
}
2021-10-01 18:33:24 +00:00
if err := recipe . UpdateTag ( image , upgradeTag ) ; err != nil {
logrus . Fatal ( err )
}
2021-12-19 23:15:55 +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
} ,
}