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 }