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