github.com/justinjmoses/evergreen@v0.0.0-20170530173719-1d50e381ff0d/cli/update.go (about) 1 package cli 2 3 import ( 4 //"bytes" 5 "fmt" 6 "io" 7 "io/ioutil" 8 "net/http" 9 "os" 10 "os/exec" 11 "path/filepath" 12 "runtime" 13 "strings" 14 "syscall" 15 16 "github.com/evergreen-ci/evergreen" 17 "github.com/kardianos/osext" 18 "github.com/pkg/errors" 19 ) 20 21 // GetUpdateCommand attempts to fetch the latest version of the client binary and install it over 22 // the current one. 23 type GetUpdateCommand struct { 24 GlobalOpts *Options `no-flag:"true"` 25 Install bool `long:"install" description:"after downloading update, overwrite old binary with new one"` 26 } 27 28 // VersionCommand prints the revision that the CLI binary was built with. 29 // Is used by auto-update to verify the new version. 30 type VersionCommand struct{} 31 32 func (vc *VersionCommand) Execute(_ []string) error { 33 fmt.Println(evergreen.ClientVersion) 34 return nil 35 } 36 37 // Searches a ClientConfig for a ClientBinary with a non-empty URL, whose architecture and OS 38 // match that of the current system. 39 func findClientUpdate(clients evergreen.ClientConfig) *evergreen.ClientBinary { 40 for _, c := range clients.ClientBinaries { 41 if c.Arch == runtime.GOARCH && c.OS == runtime.GOOS && len(c.URL) > 0 { 42 return &c 43 } 44 } 45 return nil 46 } 47 48 // prepareUpdate fetches the update at the given URL, writes it to a temporary file, and returns 49 // the path to the temporary file. 50 func prepareUpdate(url, newVersion string) (string, error) { 51 tempFile, err := ioutil.TempFile("", "") 52 if err != nil { 53 return "", err 54 } 55 56 response, err := http.Get(url) 57 if err != nil { 58 return "", err 59 } 60 61 if response == nil { 62 return "", errors.Errorf("empty response from URL: %v", url) 63 } 64 65 defer response.Body.Close() 66 _, err = io.Copy(tempFile, response.Body) 67 if err != nil { 68 return "", err 69 } 70 err = tempFile.Close() 71 if err != nil { 72 return "", err 73 } 74 75 tempPath, err := filepath.Abs(tempFile.Name()) 76 if err != nil { 77 return "", err 78 } 79 80 //chmod the binary so that it is executable 81 err = os.Chmod(tempPath, 0755) 82 if err != nil { 83 return "", err 84 } 85 86 fmt.Println("Upgraded binary downloaded to", tempPath, "- verifying") 87 88 // Run the new binary's "version" command to verify that it is in fact the correct upgraded 89 // version 90 cmd := exec.Command(tempPath, "version") 91 out, err := cmd.CombinedOutput() 92 if err != nil { 93 return "", errors.Errorf("Update failed - checking version of new binary returned error: %v", err) 94 } 95 96 updatedVersion := string(out) 97 updatedVersion = strings.TrimSpace(updatedVersion) 98 99 if updatedVersion != newVersion { 100 return "", errors.Errorf("Update failed - expected new binary to have version %v, but got %v instead", newVersion, updatedVersion) 101 } 102 103 return tempPath, nil 104 } 105 106 type updateStatus struct { 107 binary *evergreen.ClientBinary 108 needsUpdate bool 109 newVersion string 110 } 111 112 // checkUpdate checks if an update is available and logs its activity. If "silent" is true, logging 113 // is suppressed. 114 // Returns the info on the new binary to be downloaded (nil if none was found), a boolean 115 // indicating if the binary needs an update (version on client and server don't match), the new version if found, 116 // and an error if relevant. 117 func checkUpdate(ac *APIClient, silent bool) (updateStatus, error) { 118 var outLog io.Writer = os.Stdout 119 if silent { 120 outLog = ioutil.Discard 121 } 122 123 // This version of the cli has been built with a version, so we can compare it with what the 124 // server says is the latest 125 clients, err := ac.CheckUpdates() 126 if err != nil { 127 fmt.Fprintf(outLog, "Failed checking for updates: %v\n", err) 128 return updateStatus{nil, false, ""}, err 129 } 130 131 // No update needed 132 if clients.LatestRevision == evergreen.ClientVersion { 133 fmt.Fprintf(outLog, "Binary is already up to date at revision %v - not updating.\n", evergreen.ClientVersion) 134 return updateStatus{nil, false, clients.LatestRevision}, nil 135 } 136 137 binarySource := findClientUpdate(*clients) 138 if binarySource == nil { 139 // Client is out of date but no update available 140 fmt.Fprintf(outLog, "Client is out of date (version %v) but update is unavailable.\n", evergreen.ClientVersion) 141 return updateStatus{nil, true, clients.LatestRevision}, nil 142 } 143 144 fmt.Fprintf(outLog, "Update to version %v found at %v\n", clients.LatestRevision, binarySource.URL) 145 return updateStatus{binarySource, true, clients.LatestRevision}, nil 146 } 147 148 // Silently check if an update is available, and print a notification message if it is. 149 func notifyUserUpdate(ac *APIClient) { 150 update, err := checkUpdate(ac, true) 151 if update.needsUpdate && err == nil { 152 if runtime.GOOS == "windows" { 153 fmt.Printf("A new version is available. Run '%s get-update' to fetch it.\n", os.Args[0]) 154 } else { 155 fmt.Printf("A new version is available. Run '%s get-update --install' to download and install it.\n", os.Args[0]) 156 } 157 } 158 } 159 160 func (uc *GetUpdateCommand) Execute(_ []string) error { 161 ac, _, _, err := getAPIClients(uc.GlobalOpts) 162 if err != nil { 163 return err 164 } 165 166 update, err := checkUpdate(ac, false) 167 if err != nil { 168 return err 169 } 170 if !update.needsUpdate || update.binary == nil { 171 return nil 172 } 173 174 fmt.Println("Fetching update from", update.binary.URL) 175 updatedBin, err := prepareUpdate(update.binary.URL, update.newVersion) 176 if err != nil { 177 return err 178 } 179 180 if uc.Install { 181 fmt.Println("Upgraded binary successfully downloaded to temporary file:", updatedBin) 182 183 binaryDest, err := osext.Executable() 184 if err != nil { 185 return errors.Errorf("Failed to get installation path: %v", err) 186 } 187 188 fmt.Println("Unlinking existing binary at", binaryDest) 189 err = syscall.Unlink(binaryDest) 190 if err != nil { 191 return err 192 } 193 fmt.Println("Copying upgraded binary to: ", binaryDest) 194 err = copyFile(binaryDest, updatedBin) 195 if err != nil { 196 return err 197 } 198 199 fmt.Println("Setting binary permissions...") 200 err = os.Chmod(binaryDest, 0755) 201 if err != nil { 202 return err 203 } 204 fmt.Println("Upgrade complete!") 205 return nil 206 } 207 fmt.Println("New binary downloaded (but not installed) to path: ", updatedBin) 208 209 // Attempt to generate a command that the user can copy/paste to complete the install. 210 binaryDest, err := osext.Executable() 211 if err != nil { 212 // osext not working on this platform so we can't generate command, give up (but ignore err) 213 return nil 214 } 215 installCommand := fmt.Sprintf("\tmv %v %v", updatedBin, binaryDest) 216 if runtime.GOOS == "windows" { 217 installCommand = fmt.Sprintf("\tmove %v %v", updatedBin, binaryDest) 218 } 219 fmt.Printf("\nTo complete the install, run the following command:\n\n") 220 fmt.Println(installCommand) 221 222 return nil 223 } 224 225 func copyFile(dst, src string) error { 226 s, err := os.Open(src) 227 if err != nil { 228 return err 229 } 230 // no need to check errors on read only file, we already got everything 231 // we need from the filesystem, so nothing can go wrong now. 232 defer s.Close() 233 d, err := os.Create(dst) 234 if err != nil { 235 return err 236 } 237 if _, err := io.Copy(d, s); err != nil { 238 _ = d.Close() 239 return err 240 } 241 return d.Close() 242 }