gopkg.in/juju/charm.v6-unstable@v6.0.0-20171026192109-50d0c219b496/bundledata_test.go (about)

     1  // Copyright 2014 Canonical Ltd.
     2  // Licensed under the LGPLv3, see LICENCE file for details.
     3  
     4  package charm_test
     5  
     6  import (
     7  	"fmt"
     8  	"os"
     9  	"path/filepath"
    10  	"sort"
    11  	"strings"
    12  
    13  	"github.com/juju/testing"
    14  	jc "github.com/juju/testing/checkers"
    15  	gc "gopkg.in/check.v1"
    16  	"gopkg.in/mgo.v2/bson"
    17  
    18  	"gopkg.in/juju/charm.v6-unstable"
    19  )
    20  
    21  type bundleDataSuite struct {
    22  	testing.IsolationSuite
    23  }
    24  
    25  var _ = gc.Suite(&bundleDataSuite{})
    26  
    27  const mediawikiBundle = `
    28  series: precise
    29  applications:
    30      mediawiki:
    31          charm: "cs:precise/mediawiki-10"
    32          num_units: 1
    33          expose: true
    34          options:
    35              debug: false
    36              name: Please set name of wiki
    37              skin: vector
    38          annotations:
    39              "gui-x": 609
    40              "gui-y": -15
    41          storage:
    42              valid-store: 10G
    43          bindings:
    44              db: db
    45              website: public
    46          resources:
    47              data: 3
    48      mysql:
    49          charm: "cs:precise/mysql-28"
    50          num_units: 2
    51          to: [0, mediawiki/0]
    52          options:
    53              "binlog-format": MIXED
    54              "block-size": 5.3
    55              "dataset-size": "80%"
    56              flavor: distro
    57              "ha-bindiface": eth0
    58              "ha-mcastport": 5411.1
    59          annotations:
    60              "gui-x": 610
    61              "gui-y": 255
    62          constraints: "mem=8g"
    63          bindings:
    64              db: db
    65          resources:
    66              data: "resources/data.tar"
    67  relations:
    68      - ["mediawiki:db", "mysql:db"]
    69      - ["mysql:foo", "mediawiki:bar"]
    70  machines:
    71      0:
    72           constraints: 'arch=amd64 mem=4g'
    73           annotations:
    74               foo: bar
    75  tags:
    76      - super
    77      - awesome
    78  description: |
    79      Everything is awesome. Everything is cool when we work as a team.
    80      Lovely day.
    81  `
    82  
    83  var parseTests = []struct {
    84  	about                         string
    85  	data                          string
    86  	expectedBD                    *charm.BundleData
    87  	expectedErr                   string
    88  	expectUnmarshaledWithServices bool
    89  }{{
    90  	about: "mediawiki",
    91  	data:  mediawikiBundle,
    92  	expectedBD: &charm.BundleData{
    93  		Series: "precise",
    94  		Applications: map[string]*charm.ApplicationSpec{
    95  			"mediawiki": {
    96  				Charm:    "cs:precise/mediawiki-10",
    97  				NumUnits: 1,
    98  				Expose:   true,
    99  				Options: map[string]interface{}{
   100  					"debug": false,
   101  					"name":  "Please set name of wiki",
   102  					"skin":  "vector",
   103  				},
   104  				Annotations: map[string]string{
   105  					"gui-x": "609",
   106  					"gui-y": "-15",
   107  				},
   108  				Storage: map[string]string{
   109  					"valid-store": "10G",
   110  				},
   111  				EndpointBindings: map[string]string{
   112  					"db":      "db",
   113  					"website": "public",
   114  				},
   115  				Resources: map[string]interface{}{
   116  					"data": 3,
   117  				},
   118  			},
   119  			"mysql": {
   120  				Charm:    "cs:precise/mysql-28",
   121  				NumUnits: 2,
   122  				To:       []string{"0", "mediawiki/0"},
   123  				Options: map[string]interface{}{
   124  					"binlog-format": "MIXED",
   125  					"block-size":    5.3,
   126  					"dataset-size":  "80%",
   127  					"flavor":        "distro",
   128  					"ha-bindiface":  "eth0",
   129  					"ha-mcastport":  5411.1,
   130  				},
   131  				Annotations: map[string]string{
   132  					"gui-x": "610",
   133  					"gui-y": "255",
   134  				},
   135  				Constraints: "mem=8g",
   136  				EndpointBindings: map[string]string{
   137  					"db": "db",
   138  				},
   139  				Resources: map[string]interface{}{"data": "resources/data.tar"},
   140  			},
   141  		},
   142  		Machines: map[string]*charm.MachineSpec{
   143  			"0": {
   144  				Constraints: "arch=amd64 mem=4g",
   145  				Annotations: map[string]string{
   146  					"foo": "bar",
   147  				},
   148  			},
   149  		},
   150  		Relations: [][]string{
   151  			{"mediawiki:db", "mysql:db"},
   152  			{"mysql:foo", "mediawiki:bar"},
   153  		},
   154  		Tags: []string{"super", "awesome"},
   155  		Description: `Everything is awesome. Everything is cool when we work as a team.
   156  Lovely day.
   157  `,
   158  	},
   159  }, {
   160  	about: "relations specified with hyphens",
   161  	data: `
   162  relations:
   163      - - "mediawiki:db"
   164        - "mysql:db"
   165      - - "mysql:foo"
   166        - "mediawiki:bar"
   167  `,
   168  	expectedBD: &charm.BundleData{
   169  		Relations: [][]string{
   170  			{"mediawiki:db", "mysql:db"},
   171  			{"mysql:foo", "mediawiki:bar"},
   172  		},
   173  	},
   174  }, {
   175  	about: "legacy bundle with services instead of applications",
   176  	data: `
   177  services:
   178      wordpress:
   179          charm: wordpress
   180      mysql:
   181          charm: mysql
   182          num_units: 1
   183  relations:
   184      - ["wordpress:db", "mysql:db"]
   185  `,
   186  	expectedBD: &charm.BundleData{
   187  		Applications: map[string]*charm.ApplicationSpec{
   188  			"wordpress": {
   189  				Charm: "wordpress",
   190  			},
   191  			"mysql": {
   192  				Charm:    "mysql",
   193  				NumUnits: 1,
   194  			},
   195  		},
   196  		Relations: [][]string{
   197  			{"wordpress:db", "mysql:db"},
   198  		},
   199  	},
   200  	expectUnmarshaledWithServices: true,
   201  }, {
   202  	about: "bundle with services and applications",
   203  	data: `
   204  applications:
   205      wordpress:
   206          charm: wordpress
   207  services:
   208      wordpress:
   209          charm: wordpress
   210      mysql:
   211          charm: mysql
   212          num_units: 1
   213  relations:
   214      - ["wordpress:db", "mysql:db"]
   215  `,
   216  	expectedErr: ".*cannot specify both applications and services",
   217  }}
   218  
   219  func (*bundleDataSuite) TestParse(c *gc.C) {
   220  	for i, test := range parseTests {
   221  		c.Logf("test %d: %s", i, test.about)
   222  		bd, err := charm.ReadBundleData(strings.NewReader(test.data))
   223  		if test.expectedErr != "" {
   224  			c.Assert(err, gc.ErrorMatches, test.expectedErr)
   225  			continue
   226  		}
   227  		c.Assert(err, gc.IsNil)
   228  		c.Assert(bd.UnmarshaledWithServices(), gc.Equals, test.expectUnmarshaledWithServices)
   229  		bd.ClearUnmarshaledWithServices()
   230  		c.Assert(bd, jc.DeepEquals, test.expectedBD)
   231  	}
   232  }
   233  
   234  func (*bundleDataSuite) TestCodecRoundTrip(c *gc.C) {
   235  	for _, test := range parseTests {
   236  		if test.expectedErr != "" {
   237  			continue
   238  		}
   239  		// Check that for all the known codecs, we can
   240  		// round-trip the bundle data through them.
   241  		for _, codec := range codecs {
   242  			data, err := codec.Marshal(test.expectedBD)
   243  			c.Assert(err, gc.IsNil)
   244  			var bd charm.BundleData
   245  			err = codec.Unmarshal(data, &bd)
   246  			c.Assert(err, gc.IsNil)
   247  
   248  			for _, app := range bd.Applications {
   249  				for resName, res := range app.Resources {
   250  					if val, ok := res.(float64); ok {
   251  						app.Resources[resName] = int(val)
   252  					}
   253  				}
   254  			}
   255  
   256  			c.Assert(&bd, jc.DeepEquals, test.expectedBD)
   257  		}
   258  	}
   259  }
   260  
   261  func (*bundleDataSuite) TestParseLocalWithSeries(c *gc.C) {
   262  	path := "internal/test-charm-repo/quanta/riak"
   263  	data := fmt.Sprintf(`
   264          applications:
   265              dummy:
   266                  charm: %s
   267                  series: xenial
   268                  num_units: 1
   269      `, path)
   270  	bd, err := charm.ReadBundleData(strings.NewReader(data))
   271  	c.Assert(err, gc.IsNil)
   272  	c.Assert(bd, jc.DeepEquals, &charm.BundleData{
   273  		Applications: map[string]*charm.ApplicationSpec{
   274  			"dummy": {
   275  				Charm:    path,
   276  				Series:   "xenial",
   277  				NumUnits: 1,
   278  			},
   279  		}})
   280  }
   281  
   282  func (s *bundleDataSuite) TestUnmarshalWithServices(c *gc.C) {
   283  	obj := map[string]interface{}{
   284  		"services": map[string]interface{}{
   285  			"wordpress": map[string]interface{}{
   286  				"charm": "wordpress",
   287  			},
   288  		},
   289  	}
   290  	for i, codec := range codecs {
   291  		c.Logf("codec %d: %v", i, codec.Name)
   292  		data, err := codec.Marshal(obj)
   293  		c.Assert(err, gc.IsNil)
   294  		var bd charm.BundleData
   295  		err = codec.Unmarshal(data, &bd)
   296  		c.Assert(err, gc.IsNil)
   297  		c.Assert(bd.UnmarshaledWithServices(), gc.Equals, true)
   298  		bd.ClearUnmarshaledWithServices()
   299  		c.Assert(bd, jc.DeepEquals, charm.BundleData{
   300  			Applications: map[string]*charm.ApplicationSpec{"wordpress": {Charm: "wordpress"}}},
   301  		)
   302  	}
   303  }
   304  
   305  func (s *bundleDataSuite) TestBSONNilData(c *gc.C) {
   306  	bd := map[string]*charm.BundleData{
   307  		"test": nil,
   308  	}
   309  	data, err := bson.Marshal(bd)
   310  	c.Assert(err, jc.ErrorIsNil)
   311  	var result map[string]*charm.BundleData
   312  	err = bson.Unmarshal(data, &result)
   313  	c.Assert(err, gc.IsNil)
   314  	c.Assert(result["test"], gc.IsNil)
   315  }
   316  
   317  var verifyErrorsTests = []struct {
   318  	about  string
   319  	data   string
   320  	errors []string
   321  }{{
   322  	about: "as many errors as possible",
   323  	data: `
   324  series: "9wrong"
   325  
   326  machines:
   327      0:
   328           constraints: 'bad constraints'
   329           annotations:
   330               foo: bar
   331           series: 'bad series'
   332      bogus:
   333      3:
   334  applications:
   335      mediawiki:
   336          charm: "bogus:precise/mediawiki-10"
   337          num_units: -4
   338          options:
   339              debug: false
   340              name: Please set name of wiki
   341              skin: vector
   342          annotations:
   343              "gui-x": 609
   344              "gui-y": -15
   345          resources:
   346              "": 42
   347      riak:
   348          charm: "./somepath"
   349      mysql:
   350          charm: "cs:precise/mysql-28"
   351          num_units: 2
   352          to: [0, mediawiki/0, nowhere/3, 2, "bad placement"]
   353          options:
   354              "binlog-format": MIXED
   355              "block-size": 5
   356              "dataset-size": "80%"
   357              flavor: distro
   358              "ha-bindiface": eth0
   359              "ha-mcastport": 5411
   360          annotations:
   361              "gui-x": 610
   362              "gui-y": 255
   363          constraints: "bad constraints"
   364      wordpress:
   365            charm: wordpress
   366      postgres:
   367          charm: "cs:xenial/postgres"
   368          series: trusty
   369      terracotta:
   370          charm: "cs:xenial/terracotta"
   371          series: xenial
   372      ceph:
   373            charm: ceph
   374            storage:
   375                valid-storage: 3,10G
   376                no_underscores: 123
   377      ceph-osd:
   378            charm: ceph-osd
   379            storage:
   380                invalid-storage: "bad storage constraints"
   381  relations:
   382      - ["mediawiki:db", "mysql:db"]
   383      - ["mysql:foo", "mediawiki:bar"]
   384      - ["arble:bar"]
   385      - ["arble:bar", "mediawiki:db"]
   386      - ["mysql:foo", "mysql:bar"]
   387      - ["mysql:db", "mediawiki:db"]
   388      - ["mediawiki/db", "mysql:db"]
   389      - ["wordpress", "mysql"]
   390  `,
   391  	errors: []string{
   392  		`bundle declares an invalid series "9wrong"`,
   393  		`invalid storage name "no_underscores" in application "ceph"`,
   394  		`invalid storage "invalid-storage" in application "ceph-osd": bad storage constraint`,
   395  		`machine "3" is not referred to by a placement directive`,
   396  		`machine "bogus" is not referred to by a placement directive`,
   397  		`invalid machine id "bogus" found in machines`,
   398  		`invalid constraints "bad constraints" in machine "0": bad constraint`,
   399  		`invalid charm URL in application "mediawiki": cannot parse URL "bogus:precise/mediawiki-10": schema "bogus" not valid`,
   400  		`charm path in application "riak" does not exist: internal/test-charm-repo/bundle/somepath`,
   401  		`invalid constraints "bad constraints" in application "mysql": bad constraint`,
   402  		`negative number of units specified on application "mediawiki"`,
   403  		`missing resource name on application "mediawiki"`,
   404  		`the charm URL for application "postgres" has a series which does not match, please remove the series from the URL`,
   405  		`too many units specified in unit placement for application "mysql"`,
   406  		`placement "nowhere/3" refers to an application not defined in this bundle`,
   407  		`placement "mediawiki/0" specifies a unit greater than the -4 unit(s) started by the target application`,
   408  		`placement "2" refers to a machine not defined in this bundle`,
   409  		`relation ["arble:bar"] has 1 endpoint(s), not 2`,
   410  		`relation ["arble:bar" "mediawiki:db"] refers to application "arble" not defined in this bundle`,
   411  		`relation ["mysql:foo" "mysql:bar"] relates an application to itself`,
   412  		`relation ["mysql:db" "mediawiki:db"] is defined more than once`,
   413  		`invalid placement syntax "bad placement"`,
   414  		`invalid relation syntax "mediawiki/db"`,
   415  		`invalid series bad series for machine "0"`,
   416  	},
   417  }, {
   418  	about: "mediawiki should be ok",
   419  	data:  mediawikiBundle,
   420  }}
   421  
   422  func (*bundleDataSuite) TestVerifyErrors(c *gc.C) {
   423  	for i, test := range verifyErrorsTests {
   424  		c.Logf("test %d: %s", i, test.about)
   425  		assertVerifyErrors(c, test.data, nil, test.errors)
   426  	}
   427  }
   428  
   429  func assertVerifyErrors(c *gc.C, bundleData string, charms map[string]charm.Charm, expectErrors []string) {
   430  	bd, err := charm.ReadBundleData(strings.NewReader(bundleData))
   431  	c.Assert(err, gc.IsNil)
   432  
   433  	validateConstraints := func(c string) error {
   434  		if c == "bad constraints" {
   435  			return fmt.Errorf("bad constraint")
   436  		}
   437  		return nil
   438  	}
   439  	validateStorage := func(c string) error {
   440  		if c == "bad storage constraints" {
   441  			return fmt.Errorf("bad storage constraint")
   442  		}
   443  		return nil
   444  	}
   445  
   446  	if charms != nil {
   447  		err = bd.VerifyWithCharms(validateConstraints, validateStorage, charms)
   448  	} else {
   449  		err = bd.VerifyLocal("internal/test-charm-repo/bundle", validateConstraints, validateStorage)
   450  	}
   451  
   452  	if len(expectErrors) == 0 {
   453  		if err == nil {
   454  			return
   455  		}
   456  		// Let the rest of the function deal with the
   457  		// error, so that we'll see the actual errors
   458  		// that resulted.
   459  	}
   460  	c.Assert(err, gc.FitsTypeOf, (*charm.VerificationError)(nil))
   461  	errors := err.(*charm.VerificationError).Errors
   462  	errStrings := make([]string, len(errors))
   463  	for i, err := range errors {
   464  		errStrings[i] = err.Error()
   465  	}
   466  	sort.Strings(errStrings)
   467  	sort.Strings(expectErrors)
   468  	c.Assert(errStrings, jc.DeepEquals, expectErrors)
   469  }
   470  
   471  func (*bundleDataSuite) TestVerifyCharmURL(c *gc.C) {
   472  	bd, err := charm.ReadBundleData(strings.NewReader(mediawikiBundle))
   473  	c.Assert(err, gc.IsNil)
   474  	for i, u := range []string{
   475  		"wordpress",
   476  		"cs:wordpress",
   477  		"cs:precise/wordpress",
   478  		"precise/wordpress",
   479  		"precise/wordpress-2",
   480  		"local:foo",
   481  		"local:foo-45",
   482  	} {
   483  		c.Logf("test %d: %s", i, u)
   484  		bd.Applications["mediawiki"].Charm = u
   485  		err := bd.Verify(nil, nil)
   486  		c.Check(err, gc.IsNil, gc.Commentf("charm url %q", u))
   487  	}
   488  }
   489  
   490  func (*bundleDataSuite) TestVerifyLocalCharm(c *gc.C) {
   491  	bd, err := charm.ReadBundleData(strings.NewReader(mediawikiBundle))
   492  	c.Assert(err, gc.IsNil)
   493  	bundleDir := c.MkDir()
   494  	relativeCharmDir := filepath.Join(bundleDir, "charm")
   495  	err = os.MkdirAll(relativeCharmDir, 0700)
   496  	c.Assert(err, jc.ErrorIsNil)
   497  	for i, u := range []string{
   498  		"wordpress",
   499  		"cs:wordpress",
   500  		"cs:precise/wordpress",
   501  		"precise/wordpress",
   502  		"precise/wordpress-2",
   503  		"local:foo",
   504  		"local:foo-45",
   505  		c.MkDir(),
   506  		"./charm",
   507  	} {
   508  		c.Logf("test %d: %s", i, u)
   509  		bd.Applications["mediawiki"].Charm = u
   510  		err := bd.VerifyLocal(bundleDir, nil, nil)
   511  		c.Check(err, gc.IsNil, gc.Commentf("charm url %q", u))
   512  	}
   513  }
   514  
   515  func (s *bundleDataSuite) TestVerifyBundleUsingJujuInfoRelation(c *gc.C) {
   516  	err := s.testPrepareAndMutateBeforeVerifyWithCharms(c, nil)
   517  	c.Assert(err, gc.IsNil)
   518  }
   519  
   520  func (s *bundleDataSuite) testPrepareAndMutateBeforeVerifyWithCharms(c *gc.C, mutator func(bd *charm.BundleData)) error {
   521  	b := readBundleDir(c, "wordpress-with-logging")
   522  	bd := b.Data()
   523  
   524  	charms := map[string]charm.Charm{
   525  		"wordpress": readCharmDir(c, "wordpress"),
   526  		"mysql":     readCharmDir(c, "mysql"),
   527  		"logging":   readCharmDir(c, "logging"),
   528  	}
   529  
   530  	if mutator != nil {
   531  		mutator(bd)
   532  	}
   533  
   534  	return bd.VerifyWithCharms(nil, nil, charms)
   535  }
   536  
   537  func (s *bundleDataSuite) TestVerifyBundleWithUnknownEndpointBindingGiven(c *gc.C) {
   538  	err := s.testPrepareAndMutateBeforeVerifyWithCharms(c, func(bd *charm.BundleData) {
   539  		bd.Applications["wordpress"].EndpointBindings["foo"] = "bar"
   540  	})
   541  	c.Assert(err, gc.ErrorMatches,
   542  		`application "wordpress" wants to bind endpoint "foo" to space "bar", `+
   543  			`but the endpoint is not defined by the charm`,
   544  	)
   545  }
   546  
   547  func (s *bundleDataSuite) TestVerifyBundleWithExtraBindingsSuccess(c *gc.C) {
   548  	err := s.testPrepareAndMutateBeforeVerifyWithCharms(c, func(bd *charm.BundleData) {
   549  		// Both of these are specified in extra-bindings.
   550  		bd.Applications["wordpress"].EndpointBindings["admin-api"] = "internal"
   551  		bd.Applications["wordpress"].EndpointBindings["foo-bar"] = "test"
   552  	})
   553  	c.Assert(err, gc.IsNil)
   554  }
   555  
   556  func (s *bundleDataSuite) TestVerifyBundleWithRelationNameBindingSuccess(c *gc.C) {
   557  	err := s.testPrepareAndMutateBeforeVerifyWithCharms(c, func(bd *charm.BundleData) {
   558  		// Both of these are specified in as relations.
   559  		bd.Applications["wordpress"].EndpointBindings["cache"] = "foo"
   560  		bd.Applications["wordpress"].EndpointBindings["monitoring-port"] = "bar"
   561  	})
   562  	c.Assert(err, gc.IsNil)
   563  }
   564  
   565  func (*bundleDataSuite) TestRequiredCharms(c *gc.C) {
   566  	bd, err := charm.ReadBundleData(strings.NewReader(mediawikiBundle))
   567  	c.Assert(err, gc.IsNil)
   568  	reqCharms := bd.RequiredCharms()
   569  
   570  	c.Assert(reqCharms, gc.DeepEquals, []string{"cs:precise/mediawiki-10", "cs:precise/mysql-28"})
   571  }
   572  
   573  // testCharm returns a charm with the given name
   574  // and relations. The relations are specified as
   575  // a string of the form:
   576  //
   577  //	<provides-relations> | <requires-relations>
   578  //
   579  // Within each section, each white-space separated
   580  // relation is specified as:
   581  ///	<relation-name>:<interface>
   582  //
   583  // So, for example:
   584  //
   585  //     testCharm("wordpress", "web:http | db:mysql")
   586  //
   587  // is equivalent to a charm with metadata.yaml containing
   588  //
   589  //	name: wordpress
   590  //	description: wordpress
   591  //	provides:
   592  //	    web:
   593  //	        interface: http
   594  //	requires:
   595  //	    db:
   596  //	        interface: mysql
   597  //
   598  // If the charm name has a "-sub" suffix, the
   599  // returned charm will have Meta.Subordinate = true.
   600  //
   601  func testCharm(name string, relations string) charm.Charm {
   602  	var provides, requires string
   603  	parts := strings.Split(relations, "|")
   604  	provides = parts[0]
   605  	if len(parts) > 1 {
   606  		requires = parts[1]
   607  	}
   608  	meta := &charm.Meta{
   609  		Name:        name,
   610  		Summary:     name,
   611  		Description: name,
   612  		Provides:    parseRelations(provides, charm.RoleProvider),
   613  		Requires:    parseRelations(requires, charm.RoleRequirer),
   614  	}
   615  	if strings.HasSuffix(name, "-sub") {
   616  		meta.Subordinate = true
   617  	}
   618  	configStr := `
   619  options:
   620    title: {default: My Title, description: title, type: string}
   621    skill-level: {description: skill, type: int}
   622  `
   623  	config, err := charm.ReadConfig(strings.NewReader(configStr))
   624  	if err != nil {
   625  		panic(err)
   626  	}
   627  	return testCharmImpl{
   628  		meta:   meta,
   629  		config: config,
   630  	}
   631  }
   632  
   633  func parseRelations(s string, role charm.RelationRole) map[string]charm.Relation {
   634  	rels := make(map[string]charm.Relation)
   635  	for _, r := range strings.Fields(s) {
   636  		parts := strings.Split(r, ":")
   637  		if len(parts) != 2 {
   638  			panic(fmt.Errorf("invalid relation specifier %q", r))
   639  		}
   640  		name, interf := parts[0], parts[1]
   641  		rels[name] = charm.Relation{
   642  			Name:      name,
   643  			Role:      role,
   644  			Interface: interf,
   645  			Scope:     charm.ScopeGlobal,
   646  		}
   647  	}
   648  	return rels
   649  }
   650  
   651  type testCharmImpl struct {
   652  	meta   *charm.Meta
   653  	config *charm.Config
   654  	// Implement charm.Charm, but panic if anything other than
   655  	// Meta or Config methods are called.
   656  	charm.Charm
   657  }
   658  
   659  func (c testCharmImpl) Meta() *charm.Meta {
   660  	return c.meta
   661  }
   662  
   663  func (c testCharmImpl) Config() *charm.Config {
   664  	return c.config
   665  }
   666  
   667  var verifyWithCharmsErrorsTests = []struct {
   668  	about  string
   669  	data   string
   670  	charms map[string]charm.Charm
   671  
   672  	errors []string
   673  }{{
   674  	about:  "no charms",
   675  	data:   mediawikiBundle,
   676  	charms: map[string]charm.Charm{},
   677  	errors: []string{
   678  		`application "mediawiki" refers to non-existent charm "cs:precise/mediawiki-10"`,
   679  		`application "mysql" refers to non-existent charm "cs:precise/mysql-28"`,
   680  	},
   681  }, {
   682  	about: "all present and correct",
   683  	data: `
   684  applications:
   685      application1:
   686          charm: "test"
   687      application2:
   688          charm: "test"
   689      application3:
   690          charm: "test"
   691  relations:
   692      - ["application1:prova", "application2:reqa"]
   693      - ["application1:reqa", "application3:prova"]
   694      - ["application3:provb", "application2:reqb"]
   695  `,
   696  	charms: map[string]charm.Charm{
   697  		"test": testCharm("test", "prova:a provb:b | reqa:a reqb:b"),
   698  	},
   699  }, {
   700  	about: "undefined relations",
   701  	data: `
   702  applications:
   703      application1:
   704          charm: "test"
   705      application2:
   706          charm: "test"
   707  relations:
   708      - ["application1:prova", "application2:blah"]
   709      - ["application1:blah", "application2:prova"]
   710  `,
   711  	charms: map[string]charm.Charm{
   712  		"test": testCharm("test", "prova:a provb:b | reqa:a reqb:b"),
   713  	},
   714  	errors: []string{
   715  		`charm "test" used by application "application1" does not define relation "blah"`,
   716  		`charm "test" used by application "application2" does not define relation "blah"`,
   717  	},
   718  }, {
   719  	about: "undefined applications",
   720  	data: `
   721  applications:
   722      application1:
   723          charm: "test"
   724      application2:
   725          charm: "test"
   726  relations:
   727      - ["unknown:prova", "application2:blah"]
   728      - ["application1:blah", "unknown:prova"]
   729  `,
   730  	charms: map[string]charm.Charm{
   731  		"test": testCharm("test", "prova:a provb:b | reqa:a reqb:b"),
   732  	},
   733  	errors: []string{
   734  		`relation ["application1:blah" "unknown:prova"] refers to application "unknown" not defined in this bundle`,
   735  		`relation ["unknown:prova" "application2:blah"] refers to application "unknown" not defined in this bundle`,
   736  	},
   737  }, {
   738  	about: "equal applications",
   739  	data: `
   740  applications:
   741      application1:
   742          charm: "test"
   743      application2:
   744          charm: "test"
   745  relations:
   746      - ["application2:prova", "application2:reqa"]
   747  `,
   748  	charms: map[string]charm.Charm{
   749  		"test": testCharm("test", "prova:a provb:b | reqa:a reqb:b"),
   750  	},
   751  	errors: []string{
   752  		`relation ["application2:prova" "application2:reqa"] relates an application to itself`,
   753  	},
   754  }, {
   755  	about: "provider to provider relation",
   756  	data: `
   757  applications:
   758      application1:
   759          charm: "test"
   760      application2:
   761          charm: "test"
   762  relations:
   763      - ["application1:prova", "application2:prova"]
   764  `,
   765  	charms: map[string]charm.Charm{
   766  		"test": testCharm("test", "prova:a provb:b | reqa:a reqb:b"),
   767  	},
   768  	errors: []string{
   769  		`relation "application1:prova" to "application2:prova" relates provider to provider`,
   770  	},
   771  }, {
   772  	about: "provider to provider relation",
   773  	data: `
   774  applications:
   775      application1:
   776          charm: "test"
   777      application2:
   778          charm: "test"
   779  relations:
   780      - ["application1:reqa", "application2:reqa"]
   781  `,
   782  	charms: map[string]charm.Charm{
   783  		"test": testCharm("test", "prova:a provb:b | reqa:a reqb:b"),
   784  	},
   785  	errors: []string{
   786  		`relation "application1:reqa" to "application2:reqa" relates requirer to requirer`,
   787  	},
   788  }, {
   789  	about: "interface mismatch",
   790  	data: `
   791  applications:
   792      application1:
   793          charm: "test"
   794      application2:
   795          charm: "test"
   796  relations:
   797      - ["application1:reqa", "application2:provb"]
   798  `,
   799  	charms: map[string]charm.Charm{
   800  		"test": testCharm("test", "prova:a provb:b | reqa:a reqb:b"),
   801  	},
   802  	errors: []string{
   803  		`mismatched interface between "application2:provb" and "application1:reqa" ("b" vs "a")`,
   804  	},
   805  }, {
   806  	about: "different charms",
   807  	data: `
   808  applications:
   809      application1:
   810          charm: "test1"
   811      application2:
   812          charm: "test2"
   813  relations:
   814      - ["application1:reqa", "application2:prova"]
   815  `,
   816  	charms: map[string]charm.Charm{
   817  		"test1": testCharm("test", "prova:a provb:b | reqa:a reqb:b"),
   818  		"test2": testCharm("test", ""),
   819  	},
   820  	errors: []string{
   821  		`charm "test2" used by application "application2" does not define relation "prova"`,
   822  	},
   823  }, {
   824  	about: "ambiguous relation",
   825  	data: `
   826  applications:
   827      application1:
   828          charm: "test1"
   829      application2:
   830          charm: "test2"
   831  relations:
   832      - [application1, application2]
   833  `,
   834  	charms: map[string]charm.Charm{
   835  		"test1": testCharm("test", "prova:a provb:b | reqa:a reqb:b"),
   836  		"test2": testCharm("test", "prova:a provb:b | reqa:a reqb:b"),
   837  	},
   838  	errors: []string{
   839  		`cannot infer endpoint between application1 and application2: ambiguous relation: application1 application2 could refer to "application1:prova application2:reqa"; "application1:provb application2:reqb"; "application1:reqa application2:prova"; "application1:reqb application2:provb"`,
   840  	},
   841  }, {
   842  	about: "relation using juju-info",
   843  	data: `
   844  applications:
   845      application1:
   846          charm: "provider"
   847      application2:
   848          charm: "requirer"
   849  relations:
   850      - [application1, application2]
   851  `,
   852  	charms: map[string]charm.Charm{
   853  		"provider": testCharm("provider", ""),
   854  		"requirer": testCharm("requirer", "| req:juju-info"),
   855  	},
   856  }, {
   857  	about: "ambiguous when implicit relations taken into account",
   858  	data: `
   859  applications:
   860      application1:
   861          charm: "provider"
   862      application2:
   863          charm: "requirer"
   864  relations:
   865      - [application1, application2]
   866  `,
   867  	charms: map[string]charm.Charm{
   868  		"provider": testCharm("provider", "provdb:db | "),
   869  		"requirer": testCharm("requirer", "| reqdb:db reqinfo:juju-info"),
   870  	},
   871  }, {
   872  	about: "half of relation left open",
   873  	data: `
   874  applications:
   875      application1:
   876          charm: "provider"
   877      application2:
   878          charm: "requirer"
   879  relations:
   880      - ["application1:prova2", application2]
   881  `,
   882  	charms: map[string]charm.Charm{
   883  		"provider": testCharm("provider", "prova1:a prova2:a | "),
   884  		"requirer": testCharm("requirer", "| reqa:a"),
   885  	},
   886  }, {
   887  	about: "duplicate relation between open and fully-specified relations",
   888  	data: `
   889  applications:
   890      application1:
   891          charm: "provider"
   892      application2:
   893          charm: "requirer"
   894  relations:
   895      - ["application1:prova", "application2:reqa"]
   896      - ["application1", "application2"]
   897  `,
   898  	charms: map[string]charm.Charm{
   899  		"provider": testCharm("provider", "prova:a | "),
   900  		"requirer": testCharm("requirer", "| reqa:a"),
   901  	},
   902  	errors: []string{
   903  		`relation ["application1" "application2"] is defined more than once`,
   904  	},
   905  }, {
   906  	about: "configuration options specified",
   907  	data: `
   908  applications:
   909      application1:
   910          charm: "test"
   911          options:
   912              title: "some title"
   913              skill-level: 245
   914      application2:
   915          charm: "test"
   916          options:
   917              title: "another title"
   918  `,
   919  	charms: map[string]charm.Charm{
   920  		"test": testCharm("test", "prova:a provb:b | reqa:a reqb:b"),
   921  	},
   922  }, {
   923  	about: "invalid type for option",
   924  	data: `
   925  applications:
   926      application1:
   927          charm: "test"
   928          options:
   929              title: "some title"
   930              skill-level: "too much"
   931      application2:
   932          charm: "test"
   933          options:
   934              title: "another title"
   935  `,
   936  	charms: map[string]charm.Charm{
   937  		"test": testCharm("test", "prova:a provb:b | reqa:a reqb:b"),
   938  	},
   939  	errors: []string{
   940  		`cannot validate application "application1": option "skill-level" expected int, got "too much"`,
   941  	},
   942  }, {
   943  	about: "unknown option",
   944  	data: `
   945  applications:
   946      application1:
   947          charm: "test"
   948          options:
   949              title: "some title"
   950              unknown-option: 2345
   951  `,
   952  	charms: map[string]charm.Charm{
   953  		"test": testCharm("test", "prova:a provb:b | reqa:a reqb:b"),
   954  	},
   955  	errors: []string{
   956  		`cannot validate application "application1": configuration option "unknown-option" not found in charm "test"`,
   957  	},
   958  }, {
   959  	about: "multiple config problems",
   960  	data: `
   961  applications:
   962      application1:
   963          charm: "test"
   964          options:
   965              title: "some title"
   966              unknown-option: 2345
   967      application2:
   968          charm: "test"
   969          options:
   970              title: 123
   971              another-unknown: 2345
   972  `,
   973  	charms: map[string]charm.Charm{
   974  		"test": testCharm("test", "prova:a provb:b | reqa:a reqb:b"),
   975  	},
   976  	errors: []string{
   977  		`cannot validate application "application1": configuration option "unknown-option" not found in charm "test"`,
   978  		`cannot validate application "application2": configuration option "another-unknown" not found in charm "test"`,
   979  		`cannot validate application "application2": option "title" expected string, got 123`,
   980  	},
   981  }, {
   982  	about: "subordinate charm with more than zero units",
   983  	data: `
   984  applications:
   985      testsub:
   986          charm: "testsub"
   987          num_units: 1
   988  `,
   989  	charms: map[string]charm.Charm{
   990  		"testsub": testCharm("test-sub", ""),
   991  	},
   992  	errors: []string{
   993  		`application "testsub" is subordinate but has non-zero num_units`,
   994  	},
   995  }, {
   996  	about: "subordinate charm with more than one unit",
   997  	data: `
   998  applications:
   999      testsub:
  1000          charm: "testsub"
  1001          num_units: 1
  1002  `,
  1003  	charms: map[string]charm.Charm{
  1004  		"testsub": testCharm("test-sub", ""),
  1005  	},
  1006  	errors: []string{
  1007  		`application "testsub" is subordinate but has non-zero num_units`,
  1008  	},
  1009  }, {
  1010  	about: "subordinate charm with to-clause",
  1011  	data: `
  1012  applications:
  1013      testsub:
  1014          charm: "testsub"
  1015          to: [0]
  1016  machines:
  1017      0:
  1018  `,
  1019  	charms: map[string]charm.Charm{
  1020  		"testsub": testCharm("test-sub", ""),
  1021  	},
  1022  	errors: []string{
  1023  		`application "testsub" is subordinate but specifies unit placement`,
  1024  		`too many units specified in unit placement for application "testsub"`,
  1025  	},
  1026  }, {
  1027  	about: "charm with unspecified units and more than one to: entry",
  1028  	data: `
  1029  applications:
  1030      test:
  1031          charm: "test"
  1032          to: [0, 1]
  1033  machines:
  1034      0:
  1035      1:
  1036  `,
  1037  	errors: []string{
  1038  		`too many units specified in unit placement for application "test"`,
  1039  	},
  1040  }}
  1041  
  1042  func (*bundleDataSuite) TestVerifyWithCharmsErrors(c *gc.C) {
  1043  	for i, test := range verifyWithCharmsErrorsTests {
  1044  		c.Logf("test %d: %s", i, test.about)
  1045  		assertVerifyErrors(c, test.data, test.charms, test.errors)
  1046  	}
  1047  }
  1048  
  1049  var parsePlacementTests = []struct {
  1050  	placement string
  1051  	expect    *charm.UnitPlacement
  1052  	expectErr string
  1053  }{{
  1054  	placement: "lxc:application/0",
  1055  	expect: &charm.UnitPlacement{
  1056  		ContainerType: "lxc",
  1057  		Application:   "application",
  1058  		Unit:          0,
  1059  	},
  1060  }, {
  1061  	placement: "lxc:application",
  1062  	expect: &charm.UnitPlacement{
  1063  		ContainerType: "lxc",
  1064  		Application:   "application",
  1065  		Unit:          -1,
  1066  	},
  1067  }, {
  1068  	placement: "lxc:99",
  1069  	expect: &charm.UnitPlacement{
  1070  		ContainerType: "lxc",
  1071  		Machine:       "99",
  1072  		Unit:          -1,
  1073  	},
  1074  }, {
  1075  	placement: "lxc:new",
  1076  	expect: &charm.UnitPlacement{
  1077  		ContainerType: "lxc",
  1078  		Machine:       "new",
  1079  		Unit:          -1,
  1080  	},
  1081  }, {
  1082  	placement: "application/0",
  1083  	expect: &charm.UnitPlacement{
  1084  		Application: "application",
  1085  		Unit:        0,
  1086  	},
  1087  }, {
  1088  	placement: "application",
  1089  	expect: &charm.UnitPlacement{
  1090  		Application: "application",
  1091  		Unit:        -1,
  1092  	},
  1093  }, {
  1094  	placement: "application45",
  1095  	expect: &charm.UnitPlacement{
  1096  		Application: "application45",
  1097  		Unit:        -1,
  1098  	},
  1099  }, {
  1100  	placement: "99",
  1101  	expect: &charm.UnitPlacement{
  1102  		Machine: "99",
  1103  		Unit:    -1,
  1104  	},
  1105  }, {
  1106  	placement: "new",
  1107  	expect: &charm.UnitPlacement{
  1108  		Machine: "new",
  1109  		Unit:    -1,
  1110  	},
  1111  }, {
  1112  	placement: ":0",
  1113  	expectErr: `invalid placement syntax ":0"`,
  1114  }, {
  1115  	placement: "05",
  1116  	expectErr: `invalid placement syntax "05"`,
  1117  }, {
  1118  	placement: "new/2",
  1119  	expectErr: `invalid placement syntax "new/2"`,
  1120  }}
  1121  
  1122  func (*bundleDataSuite) TestParsePlacement(c *gc.C) {
  1123  	for i, test := range parsePlacementTests {
  1124  		c.Logf("test %d: %q", i, test.placement)
  1125  		up, err := charm.ParsePlacement(test.placement)
  1126  		if test.expectErr != "" {
  1127  			c.Assert(err, gc.ErrorMatches, test.expectErr)
  1128  		} else {
  1129  			c.Assert(err, gc.IsNil)
  1130  			c.Assert(up, jc.DeepEquals, test.expect)
  1131  		}
  1132  	}
  1133  }