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