github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cmd/juju/application/bundlediff_test.go (about)

     1  // Copyright 2018 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package application_test
     5  
     6  import (
     7  	"fmt"
     8  	"io/ioutil"
     9  	"path/filepath"
    10  	"reflect"
    11  	"strings"
    12  
    13  	"github.com/juju/cmd"
    14  	"github.com/juju/cmd/cmdtesting"
    15  	"github.com/juju/errors"
    16  	jujutesting "github.com/juju/testing"
    17  	jc "github.com/juju/testing/checkers"
    18  	gc "gopkg.in/check.v1"
    19  	"gopkg.in/juju/charm.v6"
    20  	csparams "gopkg.in/juju/charmrepo.v3/csclient/params"
    21  
    22  	"github.com/juju/juju/api/base"
    23  	"github.com/juju/juju/apiserver/params"
    24  	"github.com/juju/juju/cmd/juju/application"
    25  	"github.com/juju/juju/core/constraints"
    26  	"github.com/juju/juju/core/model"
    27  	"github.com/juju/juju/jujuclient"
    28  	"github.com/juju/juju/jujuclient/jujuclienttesting"
    29  	"github.com/juju/juju/testing"
    30  )
    31  
    32  type diffSuite struct {
    33  	jujutesting.IsolationSuite
    34  	apiRoot    *mockAPIRoot
    35  	charmStore *mockCharmStore
    36  	dir        string
    37  }
    38  
    39  var _ = gc.Suite(&diffSuite{})
    40  
    41  func (s *diffSuite) SetUpTest(c *gc.C) {
    42  	s.IsolationSuite.SetUpTest(c)
    43  	s.apiRoot = &mockAPIRoot{responses: makeAPIResponses()}
    44  	s.charmStore = &mockCharmStore{}
    45  	s.dir = c.MkDir()
    46  }
    47  
    48  func (s *diffSuite) runDiffBundle(c *gc.C, args ...string) (*cmd.Context, error) {
    49  	store := jujuclienttesting.MinimalStore()
    50  	store.Models["enz"] = &jujuclient.ControllerModels{
    51  		CurrentModel: "golden/horse",
    52  		Models: map[string]jujuclient.ModelDetails{"golden/horse": {
    53  			ModelType: model.IAAS,
    54  		}},
    55  	}
    56  	command := application.NewBundleDiffCommandForTest(s.apiRoot, s.charmStore, store)
    57  	return cmdtesting.RunCommandInDir(c, command, args, s.dir)
    58  }
    59  
    60  func (s *diffSuite) TestNoArgs(c *gc.C) {
    61  	_, err := s.runDiffBundle(c)
    62  	c.Assert(err, gc.ErrorMatches, "no bundle specified")
    63  }
    64  
    65  func (s *diffSuite) TestTooManyArgs(c *gc.C) {
    66  	_, err := s.runDiffBundle(c, "bundle", "somethingelse")
    67  	c.Assert(err, gc.ErrorMatches, `unrecognized args: \["somethingelse"\]`)
    68  }
    69  
    70  func (s *diffSuite) TestVerifiesBundle(c *gc.C) {
    71  	_, err := s.runDiffBundle(c, s.writeLocalBundle(c, invalidBundle))
    72  	c.Assert(err, gc.ErrorMatches, "(?s)the provided bundle has the following errors:.*")
    73  }
    74  
    75  func (s *diffSuite) TestNotABundle(c *gc.C) {
    76  	s.charmStore.url = &charm.URL{
    77  		Schema:   "cs",
    78  		Name:     "prometheus",
    79  		Revision: 23,
    80  		Series:   "xenial",
    81  	}
    82  	s.apiRoot.responses["ModelConfig.ModelGet"] = params.ModelConfigResults{
    83  		Config: map[string]params.ConfigValue{
    84  			"uuid":           {Value: testing.ModelTag.Id()},
    85  			"type":           {Value: "iaas"},
    86  			"name":           {Value: "horse"},
    87  			"default-series": {Value: "xenial"},
    88  		},
    89  	}
    90  	_, err := s.runDiffBundle(c, "prometheus")
    91  	c.Logf(errors.ErrorStack(err))
    92  	// Fails because the series that comes back from the charm store
    93  	// is xenial rather than "bundle" (and there's no local bundle).
    94  	c.Assert(err, gc.ErrorMatches, `couldn't interpret "prometheus" as a local or charmstore bundle`)
    95  }
    96  
    97  func (s *diffSuite) TestLocalBundle(c *gc.C) {
    98  	ctx, err := s.runDiffBundle(c, s.writeLocalBundle(c, testBundle))
    99  	c.Assert(err, jc.ErrorIsNil)
   100  	c.Assert(cmdtesting.Stdout(ctx), gc.Equals, `
   101  applications:
   102    grafana:
   103      missing: bundle
   104    prometheus:
   105      options:
   106        ontology:
   107          bundle: anselm
   108          model: kant
   109      constraints:
   110        bundle: cores=4
   111        model: cores=3
   112  machines:
   113    "1":
   114      missing: bundle
   115  `[1:])
   116  }
   117  
   118  func (s *diffSuite) TestIncludeAnnotations(c *gc.C) {
   119  	ctx, err := s.runDiffBundle(c, "--annotations", s.writeLocalBundle(c, testBundle))
   120  	c.Assert(err, jc.ErrorIsNil)
   121  	c.Assert(cmdtesting.Stdout(ctx), gc.Equals, `
   122  applications:
   123    grafana:
   124      missing: bundle
   125    prometheus:
   126      options:
   127        ontology:
   128          bundle: anselm
   129          model: kant
   130      annotations:
   131        aspect:
   132          bundle: west
   133          model: north
   134      constraints:
   135        bundle: cores=4
   136        model: cores=3
   137  machines:
   138    "1":
   139      missing: bundle
   140  `[1:])
   141  }
   142  
   143  func (s *diffSuite) TestHandlesIncludes(c *gc.C) {
   144  	s.writeFile(c, "include.yaml", "hume")
   145  	ctx, err := s.runDiffBundle(c, s.writeLocalBundle(c, withInclude))
   146  	c.Assert(err, jc.ErrorIsNil)
   147  	c.Assert(cmdtesting.Stdout(ctx), gc.Equals, `
   148  applications:
   149    grafana:
   150      missing: bundle
   151    prometheus:
   152      options:
   153        ontology:
   154          bundle: hume
   155          model: kant
   156      constraints:
   157        bundle: cores=4
   158        model: cores=3
   159  machines:
   160    "1":
   161      missing: bundle
   162  `[1:])
   163  }
   164  
   165  func (s *diffSuite) TestHandlesOverlays(c *gc.C) {
   166  	path1 := s.writeFile(c, "overlay1.yaml", overlay1)
   167  	path2 := s.writeFile(c, "overlay2.yaml", overlay2)
   168  	ctx, err := s.runDiffBundle(c,
   169  		"--overlay", path1,
   170  		"--overlay", path2,
   171  		s.writeLocalBundle(c, testBundle))
   172  	c.Assert(err, jc.ErrorIsNil)
   173  	c.Assert(cmdtesting.Stdout(ctx), gc.Equals, `
   174  applications:
   175    grafana:
   176      missing: bundle
   177    prometheus:
   178      options:
   179        admin-user:
   180          bundle: lovecraft
   181          model: null
   182        ontology:
   183          bundle: anselm
   184          model: kant
   185      constraints:
   186        bundle: cores=4
   187        model: cores=3
   188    telegraf:
   189      missing: model
   190  machines:
   191    "1":
   192      missing: bundle
   193  relations:
   194    bundle-additions:
   195    - - prometheus:juju-info
   196      - telegraf:info
   197  `[1:])
   198  }
   199  
   200  func (s *diffSuite) TestCharmStoreBundle(c *gc.C) {
   201  	bundleData, err := charm.ReadBundleData(strings.NewReader(testBundle))
   202  	c.Assert(err, jc.ErrorIsNil)
   203  	s.charmStore.url = &charm.URL{
   204  		Schema: "cs",
   205  		Name:   "my-bundle",
   206  		Series: "bundle",
   207  	}
   208  	s.charmStore.bundle = &mockBundle{data: bundleData}
   209  
   210  	ctx, err := s.runDiffBundle(c, "my-bundle")
   211  	c.Assert(err, jc.ErrorIsNil)
   212  
   213  	c.Assert(cmdtesting.Stdout(ctx), gc.Equals, `
   214  applications:
   215    grafana:
   216      missing: bundle
   217    prometheus:
   218      options:
   219        ontology:
   220          bundle: anselm
   221          model: kant
   222      constraints:
   223        bundle: cores=4
   224        model: cores=3
   225  machines:
   226    "1":
   227      missing: bundle
   228  `[1:])
   229  }
   230  
   231  func (s *diffSuite) TestBundleNotFound(c *gc.C) {
   232  	s.charmStore.stub.SetErrors(errors.NotFoundf(`cannot resolve URL "cs:my-bundle": charm or bundle`))
   233  	_, err := s.runDiffBundle(c, "cs:my-bundle")
   234  	c.Assert(err, gc.ErrorMatches, `cannot resolve URL "cs:my-bundle": charm or bundle not found`)
   235  }
   236  
   237  func (s *diffSuite) TestMachineMap(c *gc.C) {
   238  	ctx, err := s.runDiffBundle(c,
   239  		"--map-machines", "0=1",
   240  		s.writeLocalBundle(c, testBundle))
   241  	c.Assert(err, jc.ErrorIsNil)
   242  	c.Assert(cmdtesting.Stdout(ctx), gc.Equals, `
   243  applications:
   244    grafana:
   245      missing: bundle
   246    prometheus:
   247      options:
   248        ontology:
   249          bundle: anselm
   250          model: kant
   251      constraints:
   252        bundle: cores=4
   253        model: cores=3
   254  machines:
   255    "0":
   256      missing: bundle
   257    "1":
   258      series:
   259        bundle: xenial
   260        model: bionic
   261  `[1:])
   262  }
   263  
   264  func (s *diffSuite) writeLocalBundle(c *gc.C, content string) string {
   265  	return s.writeFile(c, "bundle.yaml", content)
   266  }
   267  
   268  func (s *diffSuite) writeFile(c *gc.C, name, content string) string {
   269  	path := filepath.Join(s.dir, name)
   270  	err := ioutil.WriteFile(path, []byte(content), 0666)
   271  	c.Assert(err, jc.ErrorIsNil)
   272  	return path
   273  }
   274  
   275  func makeAPIResponses() map[string]interface{} {
   276  	var cores uint64 = 3
   277  	return map[string]interface{}{
   278  		"ModelConfig.ModelGet": params.ModelConfigResults{
   279  			Config: map[string]params.ConfigValue{
   280  				"uuid":           {Value: testing.ModelTag.Id()},
   281  				"type":           {Value: "iaas"},
   282  				"name":           {Value: "horse"},
   283  				"default-series": {Value: "xenial"},
   284  			},
   285  		},
   286  		"Client.FullStatus": params.FullStatus{
   287  			Applications: map[string]params.ApplicationStatus{
   288  				"prometheus": {
   289  					Charm:  "cs:prometheus2-7",
   290  					Series: "xenial",
   291  					Life:   "alive",
   292  					Units: map[string]params.UnitStatus{
   293  						"prometheus/0": {Machine: "0"},
   294  					},
   295  				},
   296  				"grafana": {
   297  					Charm:  "cs:grafana-19",
   298  					Series: "bionic",
   299  					Life:   "alive",
   300  					Units: map[string]params.UnitStatus{
   301  						"grafana/0": {Machine: "1"},
   302  					},
   303  				},
   304  			},
   305  			Machines: map[string]params.MachineStatus{
   306  				"0": {Series: "xenial"},
   307  				"1": {Series: "bionic"},
   308  			},
   309  		},
   310  		"Annotations.Get": params.AnnotationsGetResults{
   311  			Results: []params.AnnotationsGetResult{{
   312  				EntityTag: "application-prometheus",
   313  				Annotations: map[string]string{
   314  					"aspect": "north",
   315  				},
   316  			}},
   317  		},
   318  		"ModelConfig.Sequences": params.ModelSequencesResult{},
   319  		"Application.CharmConfig": params.ApplicationGetConfigResults{
   320  			// Included twice since we can't predict which app will be
   321  			// requested first.
   322  			Results: []params.ConfigResult{{
   323  				Config: map[string]interface{}{"ontology": map[string]interface{}{
   324  					"value":  "kant",
   325  					"source": "user",
   326  				}},
   327  			}, {
   328  				Config: map[string]interface{}{"ontology": map[string]interface{}{
   329  					"value":  "kant",
   330  					"source": "user",
   331  				}},
   332  			}},
   333  		},
   334  		"Application.GetConstraints": params.ApplicationGetConstraintsResults{
   335  			Results: []params.ApplicationConstraint{{
   336  				Constraints: constraints.Value{CpuCores: &cores},
   337  			}, {
   338  				Constraints: constraints.Value{CpuCores: &cores},
   339  			}},
   340  		},
   341  	}
   342  }
   343  
   344  type mockCharmStore struct {
   345  	stub    jujutesting.Stub
   346  	url     *charm.URL
   347  	channel csparams.Channel
   348  	series  []string
   349  	bundle  *mockBundle
   350  }
   351  
   352  func (s *mockCharmStore) ResolveWithChannel(url *charm.URL) (*charm.URL, csparams.Channel, []string, error) {
   353  	s.stub.AddCall("ResolveWithChannel", url)
   354  	return s.url, s.channel, s.series, s.stub.NextErr()
   355  }
   356  
   357  func (s *mockCharmStore) GetBundle(url *charm.URL) (charm.Bundle, error) {
   358  	s.stub.AddCall("GetBundle", url)
   359  	return s.bundle, s.stub.NextErr()
   360  }
   361  
   362  type mockBundle struct {
   363  	data *charm.BundleData
   364  }
   365  
   366  func (b *mockBundle) Data() *charm.BundleData { return b.data }
   367  func (b *mockBundle) ReadMe() string          { return "" }
   368  
   369  type mockAPIRoot struct {
   370  	base.APICallCloser
   371  
   372  	stub      jujutesting.Stub
   373  	responses map[string]interface{}
   374  }
   375  
   376  func (r *mockAPIRoot) BestFacadeVersion(name string) int {
   377  	r.stub.AddCall("BestFacadeVersion", name)
   378  	return 42
   379  }
   380  
   381  func (r *mockAPIRoot) APICall(objType string, version int, id, request string, params, response interface{}) error {
   382  	call := objType + "." + request
   383  	r.stub.AddCall(call, version, params)
   384  	value := r.responses[call]
   385  	rv := reflect.ValueOf(response)
   386  	if value == nil {
   387  		panic(fmt.Sprintf("nil response for %s call", call))
   388  	}
   389  	if reflect.TypeOf(value).AssignableTo(rv.Type().Elem()) {
   390  		rv.Elem().Set(reflect.ValueOf(value))
   391  	} else {
   392  		panic(fmt.Sprintf("%s: can't assign value %v to %T", call, value, response))
   393  	}
   394  	return r.stub.NextErr()
   395  }
   396  
   397  func (r *mockAPIRoot) Close() error {
   398  	r.stub.AddCall("Close")
   399  	return r.stub.NextErr()
   400  }
   401  
   402  const (
   403  	testBundle = `
   404  applications:
   405    prometheus:
   406      charm: 'cs:prometheus2-7'
   407      num_units: 1
   408      series: xenial
   409      options:
   410        ontology: anselm
   411      annotations:
   412        aspect: west
   413      constraints: 'cores=4'
   414      to:
   415        - 0
   416  machines:
   417    '0':
   418      series: xenial
   419  `
   420  	withInclude = `
   421  applications:
   422    prometheus:
   423      charm: 'cs:prometheus2-7'
   424      num_units: 1
   425      series: xenial
   426      options:
   427        ontology: include-file://include.yaml
   428      annotations:
   429        aspect: west
   430      constraints: 'cores=4'
   431      to:
   432        - 0
   433  machines:
   434    '0':
   435      series: xenial
   436  `
   437  	invalidBundle = `
   438  machines:
   439    0:
   440  `
   441  	overlay1 = `
   442  applications:
   443    prometheus:
   444      options:
   445        admin-user: lovecraft
   446  `
   447  
   448  	overlay2 = `
   449  applications:
   450    telegraf:
   451      charm: 'cs:telegraf-3'
   452  relations:
   453  - - telegraf:info
   454    - prometheus:juju-info
   455  `
   456  )