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"
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"
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
}
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 : `
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
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
` ,
2021-12-27 18:56:27 +00:00
BashComplete : autocomplete . RecipeNameComplete ,
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 ,
2022-01-17 20:59:31 +00:00
internal . AllTagsFlag ,
2021-10-01 17:48:48 +00:00
} ,
2022-01-18 13:13:20 +00:00
Before : internal . SubCommandBefore ,
2021-09-05 20:33:07 +00:00
Action : func ( c * cli . Context ) error {
2022-01-29 12:35:42 +00:00
recipe := internal . ValidateRecipeWithPrompt ( c , true )
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
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 )
}
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
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 {
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
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
}
2021-12-27 15:40:59 +00:00
catlVersions , err := recipePkg . VersionsOfService ( recipe . Name , service . Name )
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 {
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 ,
2022-01-02 14:46:35 +00:00
Help : "enter / return to confirm, choose 'skip' to not upgrade this tag, vim mode is enabled" ,
VimMode : true ,
2021-11-01 10:32:47 +00:00
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
}
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 {
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
}
return nil
} ,
}