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