1
// Copyright 2016 Canonical Ltd.
2
// Licensed under the AGPLv3, see LICENCE file for details.
20
jc "github.com/juju/testing/checkers"
21
"github.com/juju/version"
22
gc "gopkg.in/check.v1"
24
"github.com/juju/juju/api"
25
"github.com/juju/juju/apiserver/params"
26
"github.com/juju/juju/cmd/juju/gui"
27
envgui "github.com/juju/juju/environs/gui"
28
"github.com/juju/juju/environs/simplestreams"
29
jujutesting "github.com/juju/juju/juju/testing"
30
coretesting "github.com/juju/juju/testing"
33
type upgradeGUISuite struct {
34
jujutesting.JujuConnSuite
37
var _ = gc.Suite(&upgradeGUISuite{})
39
// run executes the upgrade-gui command passing the given args.
40
func (s *upgradeGUISuite) run(c *gc.C, args ...string) (string, error) {
41
ctx, err := coretesting.RunCommand(c, gui.NewUpgradeGUICommand(), args...)
42
return strings.Trim(coretesting.Stderr(ctx), "\n"), err
45
// calledFunc is returned by the patch* methods below, and when called reports
46
// whether the corresponding patched function has been called.
47
type calledFunc func() bool
49
func (s *upgradeGUISuite) patchClientGUIArchives(c *gc.C, returnedVersions []params.GUIArchiveVersion, returnedErr error) calledFunc {
51
f := func(client *api.Client) ([]params.GUIArchiveVersion, error) {
53
return returnedVersions, returnedErr
55
s.PatchValue(gui.ClientGUIArchives, f)
61
func (s *upgradeGUISuite) patchClientSelectGUIVersion(c *gc.C, expectedVers string, returnedErr error) calledFunc {
63
f := func(client *api.Client, vers version.Number) error {
65
c.Assert(vers.String(), gc.Equals, expectedVers)
68
s.PatchValue(gui.ClientSelectGUIVersion, f)
74
func (s *upgradeGUISuite) patchClientUploadGUIArchive(c *gc.C, expectedHash string, expectedSize int64, expectedVers string, returnedIsCurrent bool, returnedErr error) calledFunc {
76
f := func(client *api.Client, r io.ReadSeeker, hash string, size int64, vers version.Number) (bool, error) {
78
c.Assert(hash, gc.Equals, expectedHash)
79
c.Assert(size, gc.Equals, expectedSize)
80
c.Assert(vers.String(), gc.Equals, expectedVers)
81
return returnedIsCurrent, returnedErr
83
s.PatchValue(gui.ClientUploadGUIArchive, f)
89
func (s *upgradeGUISuite) patchGUIFetchMetadata(c *gc.C, returnedMetadata []*envgui.Metadata, returnedErr error) calledFunc {
91
f := func(stream string, sources ...simplestreams.DataSource) ([]*envgui.Metadata, error) {
93
c.Assert(stream, gc.Equals, envgui.ReleasedStream)
94
c.Assert(sources[0].Description(), gc.Equals, "gui simplestreams")
95
return returnedMetadata, returnedErr
97
s.PatchValue(gui.GUIFetchMetadata, f)
103
var upgradeGUIInputErrorsTests = []struct {
108
about: "too many arguments",
109
args: []string{"bad", "wolf"},
110
expectedError: `unrecognized args: \["bad" "wolf"\]`,
112
about: "listing and upgrading",
113
args: []string{"bad", "--list"},
114
expectedError: "cannot provide arguments if --list is provided",
116
about: "archive path not found",
117
args: []string{"no-such-file"},
118
expectedError: `invalid GUI release version or local path "no-such-file"`,
121
func (s *upgradeGUISuite) TestUpgradeGUIInputErrors(c *gc.C) {
122
for i, test := range upgradeGUIInputErrorsTests {
123
c.Logf("\n%d: %s", i, test.about)
124
_, err := s.run(c, test.args...)
125
c.Assert(err, gc.ErrorMatches, test.expectedError)
129
func (s *upgradeGUISuite) TestUpgradeGUIListSuccess(c *gc.C) {
130
s.patchGUIFetchMetadata(c, []*envgui.Metadata{{
131
Version: version.MustParse("2.2.0"),
133
Version: version.MustParse("2.1.1"),
135
Version: version.MustParse("2.1.0"),
137
uploadCalled := s.patchClientUploadGUIArchive(c, "", 0, "", false, nil)
138
selectCalled := s.patchClientSelectGUIVersion(c, "", nil)
140
// Run the command to list available Juju GUI archive versions.
141
out, err := s.run(c, "--list")
142
c.Assert(err, jc.ErrorIsNil)
143
c.Assert(out, gc.Equals, "2.2.0\n2.1.1\n2.1.0")
145
// No uploads or switches are preformed.
146
c.Assert(uploadCalled(), jc.IsFalse)
147
c.Assert(selectCalled(), jc.IsFalse)
150
func (s *upgradeGUISuite) TestUpgradeGUIListNoReleases(c *gc.C) {
151
s.patchGUIFetchMetadata(c, nil, nil)
152
out, err := s.run(c, "--list")
153
c.Assert(err, gc.ErrorMatches, "cannot list Juju GUI release versions: no available Juju GUI archives found")
154
c.Assert(out, gc.Equals, "")
157
func (s *upgradeGUISuite) TestUpgradeGUIListError(c *gc.C) {
158
s.patchGUIFetchMetadata(c, nil, errors.New("bad wolf"))
159
out, err := s.run(c, "--list")
160
c.Assert(err, gc.ErrorMatches, "cannot list Juju GUI release versions: cannot retrieve Juju GUI archive info: bad wolf")
161
c.Assert(out, gc.Equals, "")
164
func (s *upgradeGUISuite) TestUpgradeGUIFileError(c *gc.C) {
165
path, _, _ := saveGUIArchive(c, "2.0.0")
166
err := os.Chmod(path, 0000)
167
c.Assert(err, jc.ErrorIsNil)
168
defer os.Chmod(path, 0600)
169
out, err := s.run(c, path)
170
c.Assert(err, gc.ErrorMatches, "cannot open GUI archive: .*")
171
c.Assert(out, gc.Equals, "")
174
func (s *upgradeGUISuite) TestUpgradeGUIArchiveVersionNotValid(c *gc.C) {
175
path, _, _ := saveGUIArchive(c, "bad-wolf")
176
out, err := s.run(c, path)
177
c.Assert(err, gc.ErrorMatches, `cannot upgrade Juju GUI using ".*": invalid version "bad-wolf" in archive`)
178
c.Assert(out, gc.Equals, "")
181
func (s *upgradeGUISuite) TestUpgradeGUIArchiveVersionNotFound(c *gc.C) {
182
path, _, _ := saveGUIArchive(c, "")
183
out, err := s.run(c, path)
184
c.Assert(err, gc.ErrorMatches, `cannot upgrade Juju GUI using ".*": cannot find Juju GUI version in archive`)
185
c.Assert(out, gc.Equals, "")
188
func (s *upgradeGUISuite) TestUpgradeGUIGUIArchivesError(c *gc.C) {
189
path, _, _ := saveGUIArchive(c, "2.1.0")
190
s.patchClientGUIArchives(c, nil, errors.New("bad wolf"))
191
out, err := s.run(c, path)
192
c.Assert(err, gc.ErrorMatches, "cannot retrieve GUI versions from the controller: bad wolf")
193
c.Assert(out, gc.Equals, "")
196
func (s *upgradeGUISuite) TestUpgradeGUIUploadGUIArchiveError(c *gc.C) {
197
path, hash, size := saveGUIArchive(c, "2.2.0")
198
s.patchClientGUIArchives(c, nil, nil)
199
s.patchClientUploadGUIArchive(c, hash, size, "2.2.0", false, errors.New("bad wolf"))
200
out, err := s.run(c, path)
201
c.Assert(err, gc.ErrorMatches, "cannot upload Juju GUI: bad wolf")
202
c.Assert(out, gc.Equals, "using local Juju GUI archive\nuploading Juju GUI 2.2.0")
205
func (s *upgradeGUISuite) TestUpgradeGUISelectGUIVersionError(c *gc.C) {
206
path, hash, size := saveGUIArchive(c, "2.3.0")
207
s.patchClientGUIArchives(c, nil, nil)
208
s.patchClientUploadGUIArchive(c, hash, size, "2.3.0", false, nil)
209
s.patchClientSelectGUIVersion(c, "2.3.0", errors.New("bad wolf"))
210
out, err := s.run(c, path)
211
c.Assert(err, gc.ErrorMatches, "cannot switch to new Juju GUI version: bad wolf")
212
c.Assert(out, gc.Equals, "using local Juju GUI archive\nuploading Juju GUI 2.3.0\nupload completed")
215
func (s *upgradeGUISuite) TestUpgradeGUIFromSimplestreamsReleaseErrors(c *gc.C) {
219
returnedMetadata []*envgui.Metadata
223
about: "last release: no releases found",
224
expectedErr: "cannot upgrade to most recent release: no available Juju GUI archives found",
226
about: "specific release: no releases found",
228
expectedErr: "cannot upgrade to release 2.0.42: no available Juju GUI archives found",
230
about: "last release: error while fetching releases list",
231
returnedErr: errors.New("bad wolf"),
232
expectedErr: "cannot upgrade to most recent release: cannot retrieve Juju GUI archive info: bad wolf",
234
about: "specific release: error while fetching releases list",
236
returnedErr: errors.New("bad wolf"),
237
expectedErr: "cannot upgrade to release 2.0.47: cannot retrieve Juju GUI archive info: bad wolf",
239
about: "last release: error while opening the remote release resource",
240
returnedMetadata: []*envgui.Metadata{
241
makeGUIMetadata(c, "2.2.0", "exterminate"),
242
makeGUIMetadata(c, "2.1.0", ""),
244
expectedErr: `cannot open Juju GUI archive at "https://1.2.3.4/path/to/gui/2.2.0": exterminate`,
246
about: "specific release: error while opening the remote release resource",
248
returnedMetadata: []*envgui.Metadata{
249
makeGUIMetadata(c, "2.2.0", ""),
250
makeGUIMetadata(c, "2.1.0", "boo"),
251
makeGUIMetadata(c, "2.0.0", ""),
253
expectedErr: `cannot open Juju GUI archive at "https://1.2.3.4/path/to/gui/2.1.0": boo`,
255
about: "specific release: not found in available releases",
257
returnedMetadata: []*envgui.Metadata{
258
makeGUIMetadata(c, "2.2.0", ""),
259
makeGUIMetadata(c, "2.0.0", ""),
261
expectedErr: "Juju GUI release version 2.1.0 not found",
264
for i, test := range tests {
265
c.Logf("\n%d: %s", i, test.about)
267
s.patchGUIFetchMetadata(c, test.returnedMetadata, test.returnedErr)
268
out, err := s.run(c, test.arg)
269
c.Assert(err, gc.ErrorMatches, test.expectedErr)
270
c.Assert(out, gc.Equals, "")
274
func (s *upgradeGUISuite) TestUpgradeGUISuccess(c *gc.C) {
276
// about describes the test.
278
// returnedMetadata holds metadata information returned by simplestreams.
279
returnedMetadata *envgui.Metadata
280
// archiveVersion is the version of the archive to be uploaded.
281
archiveVersion string
282
// existingVersions is a function returning a list of GUI archive versions
283
// already included in the controller.
284
existingVersions func(hash string) []params.GUIArchiveVersion
285
// opened holds whether Juju GUI metadata information in simplestreams
288
// uploaded holds whether the archive has been actually uploaded. If an
289
// archive with the same hash and version is already present in the
290
// controller, the upload is not performed again.
292
// selected holds whether a new GUI version must be selected. If the upload
293
// upgraded the currently served version there is no need to perform
294
// the API call to switch GUI version.
296
// expectedOutput holds the expected upgrade-gui command output.
297
expectedOutput string
299
about: "archive: first archive",
300
archiveVersion: "2.0.0",
301
expectedOutput: "using local Juju GUI archive\nuploading Juju GUI 2.0.0\nupload completed\nJuju GUI switched to version 2.0.0",
305
about: "archive: new archive",
306
archiveVersion: "2.1.0",
307
existingVersions: func(hash string) []params.GUIArchiveVersion {
308
return []params.GUIArchiveVersion{{
309
Version: version.MustParse("1.0.0"),
316
expectedOutput: "using local Juju GUI archive\nuploading Juju GUI 2.1.0\nupload completed\nJuju GUI switched to version 2.1.0",
318
about: "archive: new archive, existing non-current version",
319
archiveVersion: "2.0.42",
320
existingVersions: func(hash string) []params.GUIArchiveVersion {
321
return []params.GUIArchiveVersion{{
322
Version: version.MustParse("2.0.42"),
326
Version: version.MustParse("2.0.47"),
333
expectedOutput: "using local Juju GUI archive\nuploading Juju GUI 2.0.42\nupload completed\nJuju GUI switched to version 2.0.42",
335
about: "archive: new archive, existing current version",
336
archiveVersion: "2.0.47",
337
existingVersions: func(hash string) []params.GUIArchiveVersion {
338
return []params.GUIArchiveVersion{{
339
Version: version.MustParse("2.0.47"),
345
expectedOutput: "using local Juju GUI archive\nuploading Juju GUI 2.0.47\nupload completed\nJuju GUI at version 2.0.47",
347
about: "archive: existing archive, existing non-current version",
348
archiveVersion: "2.0.42",
349
existingVersions: func(hash string) []params.GUIArchiveVersion {
350
return []params.GUIArchiveVersion{{
351
Version: version.MustParse("2.0.42"),
355
Version: version.MustParse("2.0.47"),
361
expectedOutput: "Juju GUI switched to version 2.0.42",
363
about: "archive: existing archive, existing current version",
364
archiveVersion: "1.47.0",
365
existingVersions: func(hash string) []params.GUIArchiveVersion {
366
return []params.GUIArchiveVersion{{
367
Version: version.MustParse("1.47.0"),
372
expectedOutput: "Juju GUI at version 1.47.0",
374
about: "archive: existing archive, different existing version",
375
archiveVersion: "2.0.42",
376
existingVersions: func(hash string) []params.GUIArchiveVersion {
377
return []params.GUIArchiveVersion{{
378
Version: version.MustParse("2.0.42"),
382
Version: version.MustParse("2.0.47"),
389
expectedOutput: "using local Juju GUI archive\nuploading Juju GUI 2.0.42\nupload completed\nJuju GUI switched to version 2.0.42",
391
about: "stream: first archive",
392
archiveVersion: "2.0.0",
393
returnedMetadata: makeGUIMetadata(c, "2.0.0", ""),
394
expectedOutput: "fetching Juju GUI archive\nuploading Juju GUI 2.0.0\nupload completed\nJuju GUI switched to version 2.0.0",
399
about: "stream: new archive",
400
archiveVersion: "2.1.0",
401
returnedMetadata: makeGUIMetadata(c, "2.1.0", ""),
402
existingVersions: func(hash string) []params.GUIArchiveVersion {
403
return []params.GUIArchiveVersion{{
404
Version: version.MustParse("1.0.0"),
412
expectedOutput: "fetching Juju GUI archive\nuploading Juju GUI 2.1.0\nupload completed\nJuju GUI switched to version 2.1.0",
414
about: "stream: new archive, existing non-current version",
415
archiveVersion: "2.0.42",
416
returnedMetadata: makeGUIMetadata(c, "2.0.42", ""),
417
existingVersions: func(hash string) []params.GUIArchiveVersion {
418
return []params.GUIArchiveVersion{{
419
Version: version.MustParse("2.0.42"),
423
Version: version.MustParse("2.0.47"),
431
expectedOutput: "fetching Juju GUI archive\nuploading Juju GUI 2.0.42\nupload completed\nJuju GUI switched to version 2.0.42",
433
about: "stream: new archive, existing current version",
434
archiveVersion: "2.0.47",
435
returnedMetadata: makeGUIMetadata(c, "2.0.47", ""),
436
existingVersions: func(hash string) []params.GUIArchiveVersion {
437
return []params.GUIArchiveVersion{{
438
Version: version.MustParse("2.0.47"),
445
expectedOutput: "fetching Juju GUI archive\nuploading Juju GUI 2.0.47\nupload completed\nJuju GUI at version 2.0.47",
447
about: "stream: existing archive, existing non-current version",
448
archiveVersion: "2.0.42",
449
returnedMetadata: makeGUIMetadata(c, "2.0.42", ""),
450
existingVersions: func(hash string) []params.GUIArchiveVersion {
451
return []params.GUIArchiveVersion{{
452
Version: version.MustParse("2.0.42"),
456
Version: version.MustParse("2.0.47"),
463
expectedOutput: "Juju GUI switched to version 2.0.42",
465
about: "stream: existing archive, existing current version",
466
archiveVersion: "1.47.0",
467
returnedMetadata: makeGUIMetadata(c, "1.47.0", ""),
468
existingVersions: func(hash string) []params.GUIArchiveVersion {
469
return []params.GUIArchiveVersion{{
470
Version: version.MustParse("1.47.0"),
476
expectedOutput: "Juju GUI at version 1.47.0",
478
about: "stream: existing archive, different existing version",
479
archiveVersion: "2.0.42",
480
returnedMetadata: makeGUIMetadata(c, "2.0.42", ""),
481
existingVersions: func(hash string) []params.GUIArchiveVersion {
482
return []params.GUIArchiveVersion{{
483
Version: version.MustParse("2.0.42"),
487
Version: version.MustParse("2.0.47"),
495
expectedOutput: "fetching Juju GUI archive\nuploading Juju GUI 2.0.42\nupload completed\nJuju GUI switched to version 2.0.42",
498
for i, test := range tests {
499
c.Logf("\n%d: %s", i, test.about)
505
if test.returnedMetadata == nil {
506
// Create an fake Juju GUI local archive.
507
arg, hash, size = saveGUIArchive(c, test.archiveVersion)
509
// Use the remote metadata information.
510
arg = test.returnedMetadata.Version.String()
511
hash = test.returnedMetadata.SHA256
512
size = test.returnedMetadata.Size
515
// Patch the call to get simplestreams metadata information.
516
fetchMetadataCalled := s.patchGUIFetchMetadata(c, []*envgui.Metadata{test.returnedMetadata}, nil)
518
// Patch the call to get existing archive versions.
519
var existingVersions []params.GUIArchiveVersion
520
if test.existingVersions != nil {
521
existingVersions = test.existingVersions(hash)
523
guiArchivesCalled := s.patchClientGUIArchives(c, existingVersions, nil)
525
// Patch the other calls to the controller.
526
uploadGUIArchiveCalled := s.patchClientUploadGUIArchive(c, hash, size, test.archiveVersion, !test.selected, nil)
527
selectGUIVersionCalled := s.patchClientSelectGUIVersion(c, test.archiveVersion, nil)
530
out, err := s.run(c, arg)
531
c.Assert(err, jc.ErrorIsNil)
532
c.Assert(out, gc.Equals, test.expectedOutput)
533
c.Assert(guiArchivesCalled(), jc.IsTrue)
534
c.Assert(fetchMetadataCalled(), gc.Equals, test.opened)
535
c.Assert(uploadGUIArchiveCalled(), gc.Equals, test.uploaded)
536
c.Assert(selectGUIVersionCalled(), gc.Equals, test.selected)
540
func (s *upgradeGUISuite) TestUpgradeGUIIntegration(c *gc.C) {
541
// Prepare a GUI archive.
542
path, hash, size := saveGUIArchive(c, "2.42.0")
544
// Upload the archive from command line.
545
out, err := s.run(c, path)
546
c.Assert(err, jc.ErrorIsNil)
547
c.Assert(out, gc.Equals, "using local Juju GUI archive\nuploading Juju GUI 2.42.0\nupload completed\nJuju GUI switched to version 2.42.0")
549
// Check that the archive is present in the GUI storage server side.
550
storage, err := s.State.GUIStorage()
551
c.Assert(err, jc.ErrorIsNil)
552
defer storage.Close()
553
metadata, err := storage.Metadata("2.42.0")
554
c.Assert(err, jc.ErrorIsNil)
555
c.Assert(metadata.SHA256, gc.Equals, hash)
556
c.Assert(metadata.Size, gc.Equals, size)
558
// Check that the uploaded version has been set as the current one.
559
vers, err := s.State.GUIVersion()
560
c.Assert(err, jc.ErrorIsNil)
561
c.Assert(vers.String(), gc.Equals, "2.42.0")
564
// makeGUIArchive creates a Juju GUI tar.bz2 archive in memory, and returns a
565
// reader for the archive, its SHA256 hash and size.
566
func makeGUIArchive(c *gc.C, vers string) (r io.Reader, hash string, size int64) {
567
if runtime.GOOS == "windows" {
568
c.Skip("bzip2 command not available")
570
cmd := exec.Command("bzip2", "--compress", "--stdout", "--fast")
572
stdin, err := cmd.StdinPipe()
573
c.Assert(err, jc.ErrorIsNil)
574
stdout, err := cmd.StdoutPipe()
575
c.Assert(err, jc.ErrorIsNil)
578
c.Assert(err, jc.ErrorIsNil)
580
tw := tar.NewWriter(stdin)
582
err = tw.WriteHeader(&tar.Header{
583
Name: filepath.Join("jujugui-"+vers, "jujugui"),
585
Typeflag: tar.TypeDir,
587
c.Assert(err, jc.ErrorIsNil)
590
c.Assert(err, jc.ErrorIsNil)
592
c.Assert(err, jc.ErrorIsNil)
595
r = io.TeeReader(stdout, h)
596
b, err := ioutil.ReadAll(r)
597
c.Assert(err, jc.ErrorIsNil)
600
c.Assert(err, jc.ErrorIsNil)
602
return bytes.NewReader(b), fmt.Sprintf("%x", h.Sum(nil)), int64(len(b))
605
// saveGUIArchive creates a Juju GUI tar.bz2 archive with the given version on
606
// disk, and return its path, SHA256 hash and size.
607
func saveGUIArchive(c *gc.C, vers string) (path, hash string, size int64) {
608
r, hash, size := makeGUIArchive(c, vers)
609
path = filepath.Join(c.MkDir(), "gui.tar.bz2")
610
data, err := ioutil.ReadAll(r)
611
c.Assert(err, jc.ErrorIsNil)
612
err = ioutil.WriteFile(path, data, 0600)
613
c.Assert(err, jc.ErrorIsNil)
614
return path, hash, size
617
// makeGUIMetadata creates and return a Juju GUI archive metadata with the
618
// given version. If fetchError is not empty, trying to fetch the corresponding
619
// archive will return the given error.
620
func makeGUIMetadata(c *gc.C, vers, fetchError string) *envgui.Metadata {
621
path, hash, size := saveGUIArchive(c, vers)
622
metaPath := "/path/to/gui/" + vers
623
return &envgui.Metadata{
624
Version: version.MustParse(vers),
628
FullPath: "https://1.2.3.4" + metaPath,
630
DataSource: envgui.NewDataSource("htpps://1.2.3.4"),
633
fetchError: fetchError,
639
// datasource implements simplestreams.DataSource and overrides the Fetch
640
// method for testing purposes.
641
type dataSource struct {
642
simplestreams.DataSource
650
// Fetch implements simplestreams.DataSource.
651
func (ds *dataSource) Fetch(path string) (io.ReadCloser, string, error) {
652
ds.c.Assert(path, gc.Equals, ds.metaPath)
653
if ds.fetchError != "" {
654
return nil, "", errors.New(ds.fetchError)
656
f, err := os.Open(ds.path)
657
ds.c.Assert(err, jc.ErrorIsNil)