github.com/criteo/command-launcher@v0.0.0-20230407142452-fb616f546e98/internal/updater/self-updater.go (about)

     1  package updater
     2  
     3  import (
     4  	"fmt"
     5  	"net/http"
     6  	"net/url"
     7  	"path"
     8  	"runtime"
     9  	"time"
    10  
    11  	"github.com/criteo/command-launcher/internal/console"
    12  	"github.com/criteo/command-launcher/internal/helper"
    13  	"github.com/criteo/command-launcher/internal/user"
    14  	"github.com/inconshreveable/go-update"
    15  	log "github.com/sirupsen/logrus"
    16  	"gopkg.in/yaml.v2"
    17  )
    18  
    19  type LatestVersion struct {
    20  	Version        string `json:"version" yaml:"version"`
    21  	ReleaseNotes   string `json:"releaseNotes" yaml:"releaseNotes"`
    22  	StartPartition uint8  `json:"startPartition" yaml:"startPartition"`
    23  	EndPartition   uint8  `json:"endPartition" yaml:"endPartition"`
    24  }
    25  
    26  type SelfUpdater struct {
    27  	selfUpdateChan <-chan bool
    28  	latestVersion  LatestVersion
    29  
    30  	BinaryName        string
    31  	LatestVersionUrl  string
    32  	SelfUpdateRootUrl string
    33  	User              user.User
    34  	CurrentVersion    string
    35  	Timeout           time.Duration
    36  }
    37  
    38  func (u *SelfUpdater) CheckUpdateAsync() {
    39  	ch := make(chan bool, 1)
    40  	u.selfUpdateChan = ch
    41  	go func() {
    42  		select {
    43  		case value := <-u.checkSelfUpdate():
    44  			ch <- value
    45  		case <-time.After(u.Timeout):
    46  			ch <- false
    47  		}
    48  	}()
    49  }
    50  
    51  func (u *SelfUpdater) Update() error {
    52  	canBeSelfUpdated := <-u.selfUpdateChan || helper.LoadDebugFlags().ForceSelfUpdate
    53  	if !canBeSelfUpdated {
    54  		return nil
    55  	}
    56  
    57  	fmt.Println("\n-----------------------------------")
    58  	fmt.Printf("🚀 %s version %s \n", u.BinaryName, u.CurrentVersion)
    59  	fmt.Printf("\nan update of %s (%s) is available:\n\n", u.BinaryName, u.latestVersion.Version)
    60  	fmt.Println(u.latestVersion.ReleaseNotes)
    61  	fmt.Println()
    62  	console.Reminder("do you want to update it? [yN]")
    63  	var resp int
    64  	if _, err := fmt.Scanf("%c", &resp); err != nil || (resp != 'y' && resp != 'Y') {
    65  		fmt.Println("aborted by user")
    66  		return fmt.Errorf("Aborted by user")
    67  	}
    68  
    69  	fmt.Printf("update and install the latest version of %s (%s)\n", u.BinaryName, u.latestVersion.Version)
    70  	downloadUrl, err := u.downloadUrl(u.latestVersion.Version)
    71  	if err != nil {
    72  		console.Error("update failed: %s\n", err)
    73  		return err
    74  	}
    75  	if err = u.doSelfUpdate(downloadUrl); err != nil {
    76  		// fallback to legacy self update
    77  		if err = u.legacySelfUpdate(); err != nil {
    78  			console.Error("update failed: %s\n", err)
    79  			return err
    80  		}
    81  	}
    82  
    83  	return nil
    84  }
    85  
    86  func (u *SelfUpdater) checkSelfUpdate() <-chan bool {
    87  	ch := make(chan bool, 1)
    88  	go func() {
    89  		data, err := helper.LoadFile(u.LatestVersionUrl)
    90  		if err != nil {
    91  			log.Infof(err.Error())
    92  			ch <- false
    93  			return
    94  		}
    95  
    96  		u.latestVersion = LatestVersion{}
    97  		// YAML is a supper set of json, should work with JSON as well.
    98  		err = yaml.Unmarshal(data, &u.latestVersion)
    99  		if err != nil {
   100  			log.Errorf(err.Error())
   101  			ch <- false
   102  			return
   103  		}
   104  
   105  		ch <- u.latestVersion.Version != u.CurrentVersion &&
   106  			u.User.InPartition(u.latestVersion.StartPartition, u.latestVersion.EndPartition)
   107  	}()
   108  	return ch
   109  }
   110  
   111  func (u *SelfUpdater) doSelfUpdate(url string) error {
   112  	log.Debugf("Update %s version %s from %s", u.BinaryName, u.latestVersion.Version, url)
   113  	resp, err := helper.HttpGetWrapper(url)
   114  	if err != nil {
   115  		return fmt.Errorf("cannot download the new version from %s: %v", url, err)
   116  	}
   117  
   118  	if resp.StatusCode != http.StatusOK {
   119  		return fmt.Errorf("cannot download the new version from %s: code %d", url, resp.StatusCode)
   120  	}
   121  
   122  	defer resp.Body.Close()
   123  	err = update.Apply(resp.Body, update.Options{})
   124  	if err != nil {
   125  		if err = update.RollbackError(err); err != nil {
   126  			return fmt.Errorf("update failed, unfortunately, the rollback did not work neither: %v\nplease contact #build-services team", err)
   127  		}
   128  		console.Warn("update failed, rollback to previous version: %v\n", err)
   129  	}
   130  
   131  	return nil
   132  }
   133  
   134  func (u *SelfUpdater) downloadUrl(version string) (string, error) {
   135  	updateUrl, err := url.Parse(u.SelfUpdateRootUrl)
   136  	if err != nil {
   137  		return "", err
   138  	}
   139  
   140  	// the download url convention: [self_update_base_url]/[version]/[binaryName]_[OS]_[ARCH]_[version][extension]
   141  	// Example: https://github.com/criteo/command-launcher/releases/download/1.6.0/cdt_darwin_arm64_1.6.0"
   142  	updateUrl.Path = path.Join(updateUrl.Path, version, u.binaryFileName(version))
   143  	return updateUrl.String(), nil
   144  }
   145  
   146  func (u *SelfUpdater) binaryFileName(version string) string {
   147  	downloadFileName := fmt.Sprintf("%s_%s_%s_%s", u.BinaryName, runtime.GOOS, runtime.GOARCH, version)
   148  	if runtime.GOOS == "windows" {
   149  		return fmt.Sprintf("%s.exe", downloadFileName)
   150  	}
   151  	return downloadFileName
   152  }
   153  
   154  // deprecated. Keep it here for backward compatibility, will be removed in 1.8.0
   155  func (u *SelfUpdater) legacySelfUpdate() error {
   156  	legacyUrl, err := u.legacyLatestDownloadUrl()
   157  	if err != nil {
   158  		return err
   159  	}
   160  	if err := u.doSelfUpdate(legacyUrl); err != nil {
   161  		return err
   162  	}
   163  	return nil
   164  }
   165  
   166  func (u *SelfUpdater) legacyLatestDownloadUrl() (string, error) {
   167  	updateUrl, err := url.Parse(u.SelfUpdateRootUrl)
   168  	if err != nil {
   169  		return "", err
   170  	}
   171  
   172  	updateUrl.Path = path.Join(updateUrl.Path, "current", runtime.GOOS, runtime.GOARCH, u.binaryFileNameWithoutVersion())
   173  	return updateUrl.String(), nil
   174  }
   175  
   176  func (u *SelfUpdater) binaryFileNameWithoutVersion() string {
   177  	if runtime.GOOS == "windows" {
   178  		return fmt.Sprintf("%s.exe", u.BinaryName)
   179  	}
   180  	return u.BinaryName
   181  }