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  }