github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/cmd/juju/gui/upgradegui.go (about)

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package gui
     5  
     6  import (
     7  	"archive/tar"
     8  	"compress/bzip2"
     9  	"crypto/sha256"
    10  	"fmt"
    11  	"io"
    12  	"io/ioutil"
    13  	"os"
    14  	"path/filepath"
    15  	"strings"
    16  
    17  	"github.com/juju/cmd"
    18  	"github.com/juju/errors"
    19  	"github.com/juju/version"
    20  	"launchpad.net/gnuflag"
    21  
    22  	"github.com/juju/juju/api"
    23  	"github.com/juju/juju/apiserver/params"
    24  	"github.com/juju/juju/cmd/juju/common"
    25  	"github.com/juju/juju/cmd/modelcmd"
    26  	"github.com/juju/juju/environs/gui"
    27  )
    28  
    29  // NewUpgradeGUICommand creates and returns a new upgrade-gui command.
    30  func NewUpgradeGUICommand() cmd.Command {
    31  	return modelcmd.Wrap(&upgradeGUICommand{})
    32  }
    33  
    34  // upgradeGUICommand upgrades to a new Juju GUI version in the controller.
    35  type upgradeGUICommand struct {
    36  	modelcmd.ModelCommandBase
    37  
    38  	versOrPath string
    39  	list       bool
    40  }
    41  
    42  const upgradeGUIDoc = `
    43  Upgrade to the latest Juju GUI released version:
    44  
    45  	juju upgrade-gui
    46  
    47  Upgrade to a specific Juju GUI released version:
    48  
    49  	juju upgrade-gui 2.2.0
    50  
    51  Upgrade to a Juju GUI version present in a local tar.bz2 GUI release file:
    52  
    53  	juju upgrade-gui /path/to/jujugui-2.2.0.tar.bz2
    54  
    55  List available Juju GUI releases without upgrading:
    56  
    57  	juju upgrade-gui --list
    58  `
    59  
    60  // Info implements the cmd.Command interface.
    61  func (c *upgradeGUICommand) Info() *cmd.Info {
    62  	return &cmd.Info{
    63  		Name:    "upgrade-gui",
    64  		Purpose: "upgrade to a new Juju GUI version",
    65  		Doc:     upgradeGUIDoc,
    66  	}
    67  }
    68  
    69  // SetFlags implements the cmd.Command interface.
    70  func (c *upgradeGUICommand) SetFlags(f *gnuflag.FlagSet) {
    71  	f.BoolVar(&c.list, "list", false, "list available Juju GUI release versions without upgrading")
    72  }
    73  
    74  // Init implements the cmd.Command interface.
    75  func (c *upgradeGUICommand) Init(args []string) error {
    76  	if len(args) == 1 {
    77  		if c.list {
    78  			return errors.New("cannot provide arguments if --list is provided")
    79  		}
    80  		c.versOrPath = args[0]
    81  		return nil
    82  	}
    83  	return cmd.CheckEmpty(args)
    84  }
    85  
    86  // Run implements the cmd.Command interface.
    87  func (c *upgradeGUICommand) Run(ctx *cmd.Context) error {
    88  	if c.list {
    89  		// List available Juju GUI archive versions.
    90  		allMeta, err := remoteArchiveMetadata()
    91  		if err != nil {
    92  			return errors.Annotate(err, "cannot list Juju GUI release versions")
    93  		}
    94  		for _, metadata := range allMeta {
    95  			ctx.Infof(metadata.Version.String())
    96  		}
    97  		return nil
    98  	}
    99  	// Retrieve the GUI archive and its related info.
   100  	r, hash, size, vers, err := openArchive(c.versOrPath)
   101  	if err != nil {
   102  		return errors.Trace(err)
   103  	}
   104  	defer r.Close()
   105  
   106  	// Open the Juju API client.
   107  	client, err := c.NewAPIClient()
   108  	if err != nil {
   109  		return errors.Annotate(err, "cannot establish API connection")
   110  	}
   111  	defer client.Close()
   112  
   113  	// Check currently uploaded GUI version.
   114  	existingHash, isCurrent, err := existingVersionInfo(client, vers)
   115  	if err != nil {
   116  		return errors.Trace(err)
   117  	}
   118  
   119  	// Upload the release file if required.
   120  	if hash != existingHash {
   121  		ctx.Infof("fetching Juju GUI archive")
   122  		f, err := storeArchive(r)
   123  		if err != nil {
   124  			return errors.Trace(err)
   125  		}
   126  		defer f.Close()
   127  		ctx.Infof("uploading Juju GUI %s", vers)
   128  		isCurrent, err = clientUploadGUIArchive(client, f, hash, size, vers)
   129  		if err != nil {
   130  			return errors.Annotate(err, "cannot upload Juju GUI")
   131  		}
   132  		ctx.Infof("upload completed")
   133  	}
   134  	// Switch to the new version if not already at the desired one.
   135  	if isCurrent {
   136  		ctx.Infof("Juju GUI at version %s", vers)
   137  		return nil
   138  	}
   139  	if err = clientSelectGUIVersion(client, vers); err != nil {
   140  		return errors.Annotate(err, "cannot switch to new Juju GUI version")
   141  	}
   142  	ctx.Infof("Juju GUI switched to version %s", vers)
   143  	return nil
   144  }
   145  
   146  // openArchive opens a Juju GUI archive from the given version or file path.
   147  // The returned readSeekCloser must be closed by callers.
   148  func openArchive(versOrPath string) (r io.ReadCloser, hash string, size int64, vers version.Number, err error) {
   149  	if versOrPath == "" {
   150  		// Return the most recent Juju GUI from simplestreams.
   151  		allMeta, err := remoteArchiveMetadata()
   152  		if err != nil {
   153  			return nil, "", 0, vers, errors.Annotate(err, "cannot upgrade to most recent release")
   154  		}
   155  		// The most recent Juju GUI release is the first on the list.
   156  		metadata := allMeta[0]
   157  		r, _, err := metadata.Source.Fetch(metadata.Path)
   158  		if err != nil {
   159  			return nil, "", 0, vers, errors.Annotatef(err, "cannot open Juju GUI archive at %q", metadata.FullPath)
   160  		}
   161  		return r, metadata.SHA256, metadata.Size, metadata.Version, nil
   162  	}
   163  	f, err := os.Open(versOrPath)
   164  	if err != nil {
   165  		if !os.IsNotExist(err) {
   166  			return nil, "", 0, vers, errors.Annotate(err, "cannot open GUI archive")
   167  		}
   168  		vers, err = version.Parse(versOrPath)
   169  		if err != nil {
   170  			return nil, "", 0, vers, errors.Errorf("invalid GUI release version or local path %q", versOrPath)
   171  		}
   172  		// Return a specific release version from simplestreams.
   173  		allMeta, err := remoteArchiveMetadata()
   174  		if err != nil {
   175  			return nil, "", 0, vers, errors.Annotatef(err, "cannot upgrade to release %s", vers)
   176  		}
   177  		metadata, err := findMetadataVersion(allMeta, vers)
   178  		if err != nil {
   179  			return nil, "", 0, vers, errors.Trace(err)
   180  		}
   181  		r, _, err := metadata.Source.Fetch(metadata.Path)
   182  		if err != nil {
   183  			return nil, "", 0, vers, errors.Annotatef(err, "cannot open Juju GUI archive at %q", metadata.FullPath)
   184  		}
   185  		return r, metadata.SHA256, metadata.Size, metadata.Version, nil
   186  	}
   187  	// This is a local Juju GUI release.
   188  	defer func() {
   189  		if err != nil {
   190  			f.Close()
   191  		}
   192  	}()
   193  	vers, err = archiveVersion(f)
   194  	if err != nil {
   195  		return nil, "", 0, vers, errors.Annotatef(err, "cannot upgrade Juju GUI using %q", versOrPath)
   196  	}
   197  	if _, err := f.Seek(0, 0); err != nil {
   198  		return nil, "", 0, version.Number{}, errors.Annotate(err, "cannot seek archive")
   199  	}
   200  	hash, size, err = hashAndSize(f)
   201  	if err != nil {
   202  		return nil, "", 0, version.Number{}, errors.Annotatef(err, "cannot upgrade Juju GUI using %q", versOrPath)
   203  	}
   204  	if _, err := f.Seek(0, 0); err != nil {
   205  		return nil, "", 0, version.Number{}, errors.Annotate(err, "cannot seek archive")
   206  	}
   207  	return f, hash, size, vers, nil
   208  }
   209  
   210  // remoteArchiveMetadata returns Juju GUI archive metadata from simplestreams.
   211  func remoteArchiveMetadata() ([]*gui.Metadata, error) {
   212  	source := gui.NewDataSource(common.GUIDataSourceBaseURL())
   213  	allMeta, err := guiFetchMetadata(gui.ReleasedStream, source)
   214  	if err != nil {
   215  		return nil, errors.Annotate(err, "cannot retrieve Juju GUI archive info")
   216  	}
   217  	if len(allMeta) == 0 {
   218  		return nil, errors.New("no available Juju GUI archives found")
   219  	}
   220  	return allMeta, nil
   221  }
   222  
   223  // findMetadataVersion returns the metadata in allMeta with the given version.
   224  func findMetadataVersion(allMeta []*gui.Metadata, vers version.Number) (*gui.Metadata, error) {
   225  	for _, metadata := range allMeta {
   226  		if metadata.Version == vers {
   227  			return metadata, nil
   228  		}
   229  	}
   230  	return nil, errors.NotFoundf("Juju GUI release version %s", vers)
   231  }
   232  
   233  // archiveVersion retrieves the GUI version from the juju-gui-* directory
   234  // included in the given tar.bz2 archive reader.
   235  func archiveVersion(r io.Reader) (version.Number, error) {
   236  	var vers version.Number
   237  	prefix := "jujugui-"
   238  	tr := tar.NewReader(bzip2.NewReader(r))
   239  	for {
   240  		hdr, err := tr.Next()
   241  		if err == io.EOF {
   242  			break
   243  		}
   244  		if err != nil {
   245  			return vers, errors.New("cannot read Juju GUI archive")
   246  		}
   247  		info := hdr.FileInfo()
   248  		if !info.IsDir() || !strings.HasPrefix(hdr.Name, prefix) {
   249  			continue
   250  		}
   251  		n := filepath.Dir(hdr.Name)[len(prefix):]
   252  		vers, err = version.Parse(n)
   253  		if err != nil {
   254  			return vers, errors.Errorf("invalid version %q in archive", n)
   255  		}
   256  		return vers, nil
   257  	}
   258  	return vers, errors.New("cannot find Juju GUI version in archive")
   259  }
   260  
   261  // hashAndSize returns the SHA256 hash and size of the data included in r.
   262  func hashAndSize(r io.Reader) (hash string, size int64, err error) {
   263  	h := sha256.New()
   264  	size, err = io.Copy(h, r)
   265  	if err != nil {
   266  		return "", 0, errors.Annotate(err, "cannot calculate archive hash")
   267  	}
   268  	return fmt.Sprintf("%x", h.Sum(nil)), size, nil
   269  }
   270  
   271  // existingVersionInfo returns the hash of the existing GUI archive at the
   272  // given version and reports whether that's the current version served by the
   273  // controller. If the given version is not present in the server, an empty
   274  // hash is returned.
   275  func existingVersionInfo(client *api.Client, vers version.Number) (hash string, current bool, err error) {
   276  	versions, err := clientGUIArchives(client)
   277  	if err != nil {
   278  		return "", false, errors.Annotate(err, "cannot retrieve GUI versions from the controller")
   279  	}
   280  	for _, v := range versions {
   281  		if v.Version == vers {
   282  			return v.SHA256, v.Current, nil
   283  		}
   284  	}
   285  	return "", false, nil
   286  }
   287  
   288  // storeArchive saves the Juju GUI archive in the given reader in a temporary
   289  // file. The resulting returned readSeekCloser is deleted when closed.
   290  func storeArchive(r io.Reader) (readSeekCloser, error) {
   291  	f, err := ioutil.TempFile("", "gui-archive")
   292  	if err != nil {
   293  		return nil, errors.Annotate(err, "cannot create a temporary file to save the Juju GUI archive")
   294  	}
   295  	if _, err = io.Copy(f, r); err != nil {
   296  		return nil, errors.Annotate(err, "cannot retrieve Juju GUI archive")
   297  	}
   298  	if _, err = f.Seek(0, 0); err != nil {
   299  		return nil, errors.Annotate(err, "cannot seek temporary archive file")
   300  	}
   301  	return deleteOnCloseFile{f}, nil
   302  }
   303  
   304  // readSeekCloser combines the io read, seek and close methods.
   305  type readSeekCloser interface {
   306  	io.ReadCloser
   307  	io.Seeker
   308  }
   309  
   310  // deleteOnCloseFile is a file that gets deleted when closed.
   311  type deleteOnCloseFile struct {
   312  	*os.File
   313  }
   314  
   315  // Close closes the file.
   316  func (f deleteOnCloseFile) Close() error {
   317  	f.File.Close()
   318  	os.Remove(f.Name())
   319  	return nil
   320  }
   321  
   322  // clientGUIArchives is defined for testing purposes.
   323  var clientGUIArchives = func(client *api.Client) ([]params.GUIArchiveVersion, error) {
   324  	return client.GUIArchives()
   325  }
   326  
   327  // clientSelectGUIVersion is defined for testing purposes.
   328  var clientSelectGUIVersion = func(client *api.Client, vers version.Number) error {
   329  	return client.SelectGUIVersion(vers)
   330  }
   331  
   332  // clientUploadGUIArchive is defined for testing purposes.
   333  var clientUploadGUIArchive = func(client *api.Client, r io.ReadSeeker, hash string, size int64, vers version.Number) (bool, error) {
   334  	return client.UploadGUIArchive(r, hash, size, vers)
   335  }
   336  
   337  // guiFetchMetadata is defined for testing purposes.
   338  var guiFetchMetadata = gui.FetchMetadata