github.com/juju/charm/v11@v11.2.0/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/mgo/v3/bson"
    14  	"github.com/juju/testing"
    15  	jc "github.com/juju/testing/checkers"
    16  	gc "gopkg.in/check.v1"
    17  
    18  	"github.com/juju/charm/v11"
    19  )
    20  
    21  type bundleDataSuite struct {
    22  	testing.IsolationSuite
    23  }
    24  
    25  var _ = gc.Suite(&bundleDataSuite{})
    26  
    27  const mediawikiBundle = `
    28  default-base: ubuntu@20.04
    29  applications:
    30      mediawiki:
    31          charm: "mediawiki"
    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: "mysql"
    50          num_units: 2
    51          to: [0, mediawiki/0]
    52          base: ubuntu@22.04
    53          options:
    54              "binlog-format": MIXED
    55              "block-size": 5.3
    56              "dataset-size": "80%"
    57              flavor: distro
    58              "ha-bindiface": eth0
    59              "ha-mcastport": 5411.1
    60          annotations:
    61              "gui-x": 610
    62              "gui-y": 255
    63          constraints: "mem=8g"
    64          bindings:
    65              db: db
    66          resources:
    67              data: "resources/data.tar"
    68  relations:
    69      - ["mediawiki:db", "mysql:db"]
    70      - ["mysql:foo", "mediawiki:bar"]
    71  machines:
    72      0:
    73           constraints: 'arch=amd64 mem=4g'
    74           annotations:
    75               foo: bar
    76  tags:
    77      - super
    78      - awesome
    79  description: |
    80      Everything is awesome. Everything is cool when we work as a team.
    81      Lovely day.
    82  `
    83  
    84  // Revision are an *int, create a few ints for their addresses used in tests.
    85  var (
    86  	five        = 5
    87  	ten         = 10
    88  	twentyEight = 28
    89  )
    90  
    91  var parseTests = []struct {
    92  	about       string
    93  	data        string
    94  	expectedBD  *charm.BundleData
    95  	expectedErr string
    96  }{{
    97  	about: "mediawiki",
    98  	data:  mediawikiBundle,
    99  	expectedBD: &charm.BundleData{
   100  		DefaultBase: "ubuntu@20.04",
   101  		Applications: map[string]*charm.ApplicationSpec{
   102  			"mediawiki": {
   103  				Charm:    "mediawiki",
   104  				NumUnits: 1,
   105  				Expose:   true,
   106  				Options: map[string]interface{}{
   107  					"debug": false,
   108  					"name":  "Please set name of wiki",
   109  					"skin":  "vector",
   110  				},
   111  				Annotations: map[string]string{
   112  					"gui-x": "609",
   113  					"gui-y": "-15",
   114  				},
   115  				Storage: map[string]string{
   116  					"valid-store": "10G",
   117  				},
   118  				EndpointBindings: map[string]string{
   119  					"db":      "db",
   120  					"website": "public",
   121  				},
   122  				Resources: map[string]interface{}{
   123  					"data": 3,
   124  				},
   125  			},
   126  			"mysql": {
   127  				Charm:    "mysql",
   128  				NumUnits: 2,
   129  				To:       []string{"0", "mediawiki/0"},
   130  				Base:     "ubuntu@22.04",
   131  				Options: map[string]interface{}{
   132  					"binlog-format": "MIXED",
   133  					"block-size":    5.3,
   134  					"dataset-size":  "80%",
   135  					"flavor":        "distro",
   136  					"ha-bindiface":  "eth0",
   137  					"ha-mcastport":  5411.1,
   138  				},
   139  				Annotations: map[string]string{
   140  					"gui-x": "610",
   141  					"gui-y": "255",
   142  				},
   143  				Constraints: "mem=8g",
   144  				EndpointBindings: map[string]string{
   145  					"db": "db",
   146  				},
   147  				Resources: map[string]interface{}{"data": "resources/data.tar"},
   148  			},
   149  		},
   150  		Machines: map[string]*charm.MachineSpec{
   151  			"0": {
   152  				Constraints: "arch=amd64 mem=4g",
   153  				Annotations: map[string]string{
   154  					"foo": "bar",
   155  				},
   156  			},
   157  		},
   158  		Relations: [][]string{
   159  			{"mediawiki:db", "mysql:db"},
   160  			{"mysql:foo", "mediawiki:bar"},
   161  		},
   162  		Tags: []string{"super", "awesome"},
   163  		Description: `Everything is awesome. Everything is cool when we work as a team.
   164  Lovely day.
   165  `,
   166  	},
   167  }, {
   168  	about: "relations specified with hyphens",
   169  	data: `
   170  relations:
   171      - - "mediawiki:db"
   172        - "mysql:db"
   173      - - "mysql:foo"
   174        - "mediawiki:bar"
   175  `,
   176  	expectedBD: &charm.BundleData{
   177  		Relations: [][]string{
   178  			{"mediawiki:db", "mysql:db"},
   179  			{"mysql:foo", "mediawiki:bar"},
   180  		},
   181  	},
   182  }, {
   183  	about: "scale alias for num_units",
   184  	data: `
   185  applications:
   186      mysql:
   187          charm: mysql
   188          scale: 1
   189  `,
   190  	expectedBD: &charm.BundleData{
   191  		Applications: map[string]*charm.ApplicationSpec{
   192  			"mysql": {
   193  				Charm:    "mysql",
   194  				NumUnits: 1,
   195  			},
   196  		},
   197  	},
   198  }, {
   199  	about: "application requiring explicit trust",
   200  	data: `
   201  applications:
   202      aws-integrator:
   203          charm: aws-integrator
   204          num_units: 1
   205          trust: true
   206  `,
   207  	expectedBD: &charm.BundleData{
   208  		Applications: map[string]*charm.ApplicationSpec{
   209  			"aws-integrator": {
   210  				Charm:         "aws-integrator",
   211  				NumUnits:      1,
   212  				RequiresTrust: true,
   213  			},
   214  		},
   215  	},
   216  }, {
   217  	about: "application defining offers",
   218  	data: `
   219  applications:
   220      apache2:
   221        charm: "apache2"
   222        revision: 28
   223        num_units: 1
   224        offers:
   225          offer1:
   226            endpoints:
   227              - "apache-website"
   228              - "apache-proxy"
   229            acl:
   230              admin: "admin"
   231              foo: "consume"
   232          offer2:
   233            endpoints:
   234              - "apache-website"
   235  `,
   236  	expectedBD: &charm.BundleData{
   237  		Applications: map[string]*charm.ApplicationSpec{
   238  			"apache2": {
   239  				Charm:    "apache2",
   240  				Revision: &twentyEight,
   241  				NumUnits: 1,
   242  				Offers: map[string]*charm.OfferSpec{
   243  					"offer1": {
   244  						Endpoints: []string{
   245  							"apache-website",
   246  							"apache-proxy",
   247  						},
   248  						ACL: map[string]string{
   249  							"admin": "admin",
   250  							"foo":   "consume",
   251  						},
   252  					},
   253  					"offer2": {
   254  						Endpoints: []string{
   255  							"apache-website",
   256  						},
   257  					},
   258  				},
   259  			},
   260  		},
   261  	},
   262  }, {
   263  	about: "saas offerings",
   264  	data: `
   265  saas:
   266      apache2:
   267          url: production:admin/info.apache
   268  applications:
   269      apache2:
   270        charm: "apache2"
   271        revision: 10
   272        num_units: 1
   273  `,
   274  	expectedBD: &charm.BundleData{
   275  		Saas: map[string]*charm.SaasSpec{
   276  			"apache2": {
   277  				URL: "production:admin/info.apache",
   278  			},
   279  		},
   280  		Applications: map[string]*charm.ApplicationSpec{
   281  			"apache2": {
   282  				Charm:    "apache2",
   283  				Revision: &ten,
   284  				NumUnits: 1,
   285  			},
   286  		},
   287  	},
   288  }, {
   289  	about: "saas offerings with relations",
   290  	data: `
   291  saas:
   292      mysql:
   293          url: production:admin/info.mysql
   294  applications:
   295      wordpress:
   296        charm: "ch:wordpress"
   297        series: "trusty"
   298        revision: 10
   299        num_units: 1
   300  relations:
   301  - - wordpress:db
   302    - mysql:db
   303  `,
   304  	expectedBD: &charm.BundleData{
   305  		Saas: map[string]*charm.SaasSpec{
   306  			"mysql": {
   307  				URL: "production:admin/info.mysql",
   308  			},
   309  		},
   310  		Applications: map[string]*charm.ApplicationSpec{
   311  			"wordpress": {
   312  				Charm:    "ch:wordpress",
   313  				Series:   "trusty",
   314  				Revision: &ten,
   315  				NumUnits: 1,
   316  			},
   317  		},
   318  		Relations: [][]string{
   319  			{"wordpress:db", "mysql:db"},
   320  		},
   321  	},
   322  }, {
   323  	about: "charm channel",
   324  	data: `
   325  applications:
   326      wordpress:
   327        charm: "wordpress"
   328        revision: 10
   329        series: trusty
   330        channel: edge
   331        num_units: 1
   332  `,
   333  	expectedBD: &charm.BundleData{
   334  		Applications: map[string]*charm.ApplicationSpec{
   335  			"wordpress": {
   336  				Charm:    "wordpress",
   337  				Channel:  "edge",
   338  				Revision: &ten,
   339  				NumUnits: 1,
   340  				Series:   "trusty",
   341  			},
   342  		},
   343  	},
   344  }, {
   345  	about: "charm revision and channel",
   346  	data: `
   347  applications:
   348      wordpress:
   349        charm: "wordpress"
   350        revision: 5
   351        channel: edge
   352        num_units: 1
   353  `,
   354  	expectedBD: &charm.BundleData{
   355  		Applications: map[string]*charm.ApplicationSpec{
   356  			"wordpress": {
   357  				Charm:    "wordpress",
   358  				Revision: &five,
   359  				Channel:  "edge",
   360  				NumUnits: 1,
   361  			},
   362  		},
   363  	},
   364  }}
   365  
   366  func (*bundleDataSuite) TestParse(c *gc.C) {
   367  	for i, test := range parseTests {
   368  		c.Logf("test %d: %s", i, test.about)
   369  		bd, err := charm.ReadBundleData(strings.NewReader(test.data))
   370  		if test.expectedErr != "" {
   371  			c.Assert(err, gc.ErrorMatches, test.expectedErr)
   372  			continue
   373  		}
   374  		c.Assert(err, gc.IsNil)
   375  		c.Assert(bd, jc.DeepEquals, test.expectedBD)
   376  	}
   377  }
   378  
   379  func (*bundleDataSuite) TestCodecRoundTrip(c *gc.C) {
   380  	for i, test := range parseTests {
   381  		if test.expectedErr != "" {
   382  			continue
   383  		}
   384  		// Check that for all the known codecs, we can
   385  		// round-trip the bundle data through them.
   386  		for _, codec := range codecs {
   387  
   388  			c.Logf("Code Test %s for test %d: %s", codec.Name, i, test.about)
   389  
   390  			data, err := codec.Marshal(test.expectedBD)
   391  			c.Assert(err, gc.IsNil)
   392  			var bd charm.BundleData
   393  			err = codec.Unmarshal(data, &bd)
   394  			c.Assert(err, gc.IsNil)
   395  
   396  			for _, app := range bd.Applications {
   397  				for resName, res := range app.Resources {
   398  					if val, ok := res.(float64); ok {
   399  						app.Resources[resName] = int(val)
   400  					}
   401  				}
   402  			}
   403  
   404  			c.Assert(&bd, jc.DeepEquals, test.expectedBD)
   405  		}
   406  	}
   407  }
   408  
   409  func (*bundleDataSuite) TestParseLocalWithSeries(c *gc.C) {
   410  	path := "internal/test-charm-repo/quanta/riak"
   411  	data := fmt.Sprintf(`
   412          applications:
   413              dummy:
   414                  charm: %s
   415                  series: xenial
   416                  num_units: 1
   417      `, path)
   418  	bd, err := charm.ReadBundleData(strings.NewReader(data))
   419  	c.Assert(err, gc.IsNil)
   420  	c.Assert(bd, jc.DeepEquals, &charm.BundleData{
   421  		Applications: map[string]*charm.ApplicationSpec{
   422  			"dummy": {
   423  				Charm:    path,
   424  				Series:   "xenial",
   425  				NumUnits: 1,
   426  			},
   427  		}})
   428  }
   429  
   430  func (s *bundleDataSuite) TestBSONNilData(c *gc.C) {
   431  	bd := map[string]*charm.BundleData{
   432  		"test": nil,
   433  	}
   434  	data, err := bson.Marshal(bd)
   435  	c.Assert(err, jc.ErrorIsNil)
   436  	var result map[string]*charm.BundleData
   437  	err = bson.Unmarshal(data, &result)
   438  	c.Assert(err, gc.IsNil)
   439  	c.Assert(result["test"], gc.IsNil)
   440  }
   441  
   442  var verifyErrorsTests = []struct {
   443  	about  string
   444  	data   string
   445  	errors []string
   446  }{{
   447  	about: "as many errors as possible",
   448  	data: `
   449  series: "9wrong"
   450  default-base: "invalidbase"
   451  
   452  saas:
   453      apache2:
   454          url: '!some-bogus/url'
   455      riak:
   456          url: production:admin/info.riak
   457  machines:
   458      0:
   459          constraints: 'bad constraints'
   460          annotations:
   461              foo: bar
   462          series: 'bad series'
   463          base: 'bad base'
   464      bogus:
   465      3:
   466  applications:
   467      mediawiki:
   468          charm: "bogus:precise/mediawiki-10"
   469          num_units: -4
   470          options:
   471              debug: false
   472              name: Please set name of wiki
   473              skin: vector
   474          annotations:
   475              "gui-x": 609
   476              "gui-y": -15
   477          resources:
   478              "": 42
   479              "foo":
   480                 "not": int
   481      riak:
   482          charm: "./somepath"
   483      mysql:
   484          charm: "mysql"
   485          num_units: 2
   486          to: [0, mediawiki/0, nowhere/3, 2, "bad placement"]
   487          options:
   488              "binlog-format": MIXED
   489              "block-size": 5
   490              "dataset-size": "80%"
   491              flavor: distro
   492              "ha-bindiface": eth0
   493              "ha-mcastport": 5411
   494          annotations:
   495              "gui-x": 610
   496              "gui-y": 255
   497          constraints: "bad constraints"
   498      wordpress:
   499            charm: wordpress
   500      postgres:
   501          charm: "postgres"
   502          series: trusty
   503      terracotta:
   504          charm: "terracotta"
   505          base: "ubuntu@22.04"
   506      ceph:
   507            charm: ceph
   508            storage:
   509                valid-storage: 3,10G
   510                no_underscores: 123
   511      ceph-osd:
   512            charm: ceph-osd
   513            storage:
   514                invalid-storage: "bad storage constraints"
   515  relations:
   516      - ["mediawiki:db", "mysql:db"]
   517      - ["mysql:foo", "mediawiki:bar"]
   518      - ["arble:bar"]
   519      - ["arble:bar", "mediawiki:db"]
   520      - ["mysql:foo", "mysql:bar"]
   521      - ["mysql:db", "mediawiki:db"]
   522      - ["mediawiki/db", "mysql:db"]
   523      - ["wordpress", "mysql"]
   524      - ["wordpress:db", "riak:db"]
   525  `,
   526  	errors: []string{
   527  		`bundle declares an invalid series "9wrong"`,
   528  		`bundle declares an invalid base "invalidbase"`,
   529  		`invalid offer URL "!some-bogus/url" for SAAS apache2`,
   530  		`invalid storage name "no_underscores" in application "ceph"`,
   531  		`invalid storage "invalid-storage" in application "ceph-osd": bad storage constraint`,
   532  		`machine "3" is not referred to by a placement directive`,
   533  		`machine "bogus" is not referred to by a placement directive`,
   534  		`invalid machine id "bogus" found in machines`,
   535  		`invalid constraints "bad constraints" in machine "0": bad constraint`,
   536  		`invalid charm URL in application "mediawiki": cannot parse URL "bogus:precise/mediawiki-10": schema "bogus" not valid`,
   537  		`charm path in application "riak" does not exist: internal/test-charm-repo/bundle/somepath`,
   538  		`invalid constraints "bad constraints" in application "mysql": bad constraint`,
   539  		`negative number of units specified on application "mediawiki"`,
   540  		`missing resource name on application "mediawiki"`,
   541  		`resource revision "mediawiki" is not int or string`,
   542  		`too many units specified in unit placement for application "mysql"`,
   543  		`placement "nowhere/3" refers to an application not defined in this bundle`,
   544  		`placement "mediawiki/0" specifies a unit greater than the -4 unit(s) started by the target application`,
   545  		`placement "2" refers to a machine not defined in this bundle`,
   546  		`relation ["arble:bar"] has 1 endpoint(s), not 2`,
   547  		`relation ["arble:bar" "mediawiki:db"] refers to application "arble" not defined in this bundle`,
   548  		`relation ["mysql:foo" "mysql:bar"] relates an application to itself`,
   549  		`relation ["mysql:db" "mediawiki:db"] is defined more than once`,
   550  		`invalid placement syntax "bad placement"`,
   551  		`invalid relation syntax "mediawiki/db"`,
   552  		`invalid series "bad series" for machine "0"`,
   553  		`invalid base "bad base" for machine "0"`,
   554  		`ambiguous relation "riak" refers to a application and a SAAS in this bundle`,
   555  		`SAAS "riak" already exists with application "riak" name`,
   556  		`application "riak" already exists with SAAS "riak" name`,
   557  	},
   558  }, {
   559  	about: "mediawiki should be ok",
   560  	data:  mediawikiBundle,
   561  }, {
   562  	about: "malformed offer and endpoint names",
   563  	data: `
   564  applications:
   565      aws-integrator:
   566          charm: aws-integrator
   567          num_units: 1
   568          trust: true
   569          offers:
   570            $bad-name:
   571              endpoints:
   572                - "nope!"
   573  `,
   574  	errors: []string{
   575  		`invalid offer name "$bad-name" in application "aws-integrator"`,
   576  		`invalid endpoint name "nope!" for offer "$bad-name" in application "aws-integrator"`,
   577  	},
   578  }, {
   579  	about: "expose parameters provided together with expose:true",
   580  	data: `
   581  applications: 
   582    aws-integrator: 
   583      charm: "aws-integrator"
   584      expose: true
   585      exposed-endpoints:
   586        admin:
   587          expose-to-spaces:
   588            - alpha
   589          expose-to-cidrs:
   590            - 13.37.0.0/16
   591      num_units: 1
   592  `,
   593  	errors: []string{
   594  		`exposed-endpoints cannot be specified together with "exposed:true" in application "aws-integrator" as this poses a security risk when deploying bundles to older controllers`,
   595  	},
   596  }, {
   597  	about: "invalid CIDR in expose-to-cidrs parameter when the app is exposed",
   598  	data: `
   599  applications: 
   600    aws-integrator: 
   601      charm: "aws-integrator"
   602      exposed-endpoints:
   603        admin:
   604          expose-to-spaces:
   605            - alpha
   606          expose-to-cidrs:
   607            - not-a-cidr
   608      num_units: 1
   609  `,
   610  	errors: []string{
   611  		`invalid CIDR "not-a-cidr" for expose to CIDRs field for endpoint "admin" in application "aws-integrator"`,
   612  	},
   613  }}
   614  
   615  func (*bundleDataSuite) TestVerifyErrors(c *gc.C) {
   616  	for i, test := range verifyErrorsTests {
   617  		c.Logf("test %d: %s", i, test.about)
   618  		assertVerifyErrors(c, test.data, nil, test.errors)
   619  	}
   620  }
   621  
   622  func assertVerifyErrors(c *gc.C, bundleData string, charms map[string]charm.Charm, expectErrors []string) {
   623  	bd, err := charm.ReadBundleData(strings.NewReader(bundleData))
   624  	c.Assert(err, gc.IsNil)
   625  
   626  	validateConstraints := func(c string) error {
   627  		if c == "bad constraints" {
   628  			return fmt.Errorf("bad constraint")
   629  		}
   630  		return nil
   631  	}
   632  	validateStorage := func(c string) error {
   633  		if c == "bad storage constraints" {
   634  			return fmt.Errorf("bad storage constraint")
   635  		}
   636  		return nil
   637  	}
   638  	validateDevices := func(c string) error {
   639  		if c == "bad device constraints" {
   640  			return fmt.Errorf("bad device constraint")
   641  		}
   642  		return nil
   643  	}
   644  	if charms != nil {
   645  		err = bd.VerifyWithCharms(validateConstraints, validateStorage, validateDevices, charms)
   646  	} else {
   647  		err = bd.VerifyLocal("internal/test-charm-repo/bundle", validateConstraints, validateStorage, validateDevices)
   648  	}
   649  
   650  	if len(expectErrors) == 0 {
   651  		if err == nil {
   652  			return
   653  		}
   654  		// Let the rest of the function deal with the
   655  		// error, so that we'll see the actual errors
   656  		// that resulted.
   657  	}
   658  	c.Assert(err, gc.FitsTypeOf, (*charm.VerificationError)(nil))
   659  	errors := err.(*charm.VerificationError).Errors
   660  	errStrings := make([]string, len(errors))
   661  	for i, err := range errors {
   662  		errStrings[i] = err.Error()
   663  	}
   664  	sort.Strings(errStrings)
   665  	sort.Strings(expectErrors)
   666  	c.Assert(errStrings, jc.DeepEquals, expectErrors)
   667  }
   668  
   669  func (*bundleDataSuite) TestVerifyCharmURL(c *gc.C) {
   670  	bd, err := charm.ReadBundleData(strings.NewReader(mediawikiBundle))
   671  	c.Assert(err, gc.IsNil)
   672  	for i, u := range []string{
   673  		"ch:wordpress",
   674  		"local:foo",
   675  		"local:foo-45",
   676  	} {
   677  		c.Logf("test %d: %s", i, u)
   678  		bd.Applications["mediawiki"].Charm = u
   679  		err := bd.Verify(nil, nil, nil)
   680  		c.Check(err, gc.IsNil, gc.Commentf("charm url %q", u))
   681  	}
   682  }
   683  
   684  func (*bundleDataSuite) TestVerifyLocalCharm(c *gc.C) {
   685  	bd, err := charm.ReadBundleData(strings.NewReader(mediawikiBundle))
   686  	c.Assert(err, gc.IsNil)
   687  	bundleDir := c.MkDir()
   688  	relativeCharmDir := filepath.Join(bundleDir, "charm")
   689  	err = os.MkdirAll(relativeCharmDir, 0700)
   690  	c.Assert(err, jc.ErrorIsNil)
   691  	for i, u := range []string{
   692  		"ch:wordpress",
   693  		"local:foo",
   694  		"local:foo-45",
   695  		c.MkDir(),
   696  		"./charm",
   697  	} {
   698  		c.Logf("test %d: %s", i, u)
   699  		bd.Applications["mediawiki"].Charm = u
   700  		err := bd.VerifyLocal(bundleDir, nil, nil, nil)
   701  		c.Check(err, gc.IsNil, gc.Commentf("charm url %q", u))
   702  	}
   703  }
   704  
   705  func (s *bundleDataSuite) TestVerifyBundleUsingJujuInfoRelation(c *gc.C) {
   706  	err := s.testPrepareAndMutateBeforeVerifyWithCharms(c, nil)
   707  	c.Assert(err, gc.IsNil)
   708  }
   709  
   710  func (s *bundleDataSuite) testPrepareAndMutateBeforeVerifyWithCharms(c *gc.C, mutator func(bd *charm.BundleData)) error {
   711  	b := readBundleDir(c, "wordpress-with-logging")
   712  	bd := b.Data()
   713  
   714  	charms := map[string]charm.Charm{
   715  		"ch:wordpress": readCharmDir(c, "wordpress"),
   716  		"ch:mysql":     readCharmDir(c, "mysql"),
   717  		"logging":      readCharmDir(c, "logging"),
   718  	}
   719  
   720  	if mutator != nil {
   721  		mutator(bd)
   722  	}
   723  
   724  	return bd.VerifyWithCharms(nil, nil, nil, charms)
   725  }
   726  
   727  func (s *bundleDataSuite) TestVerifyBundleWithUnknownEndpointBindingGiven(c *gc.C) {
   728  	err := s.testPrepareAndMutateBeforeVerifyWithCharms(c, func(bd *charm.BundleData) {
   729  		bd.Applications["wordpress"].EndpointBindings["foo"] = "bar"
   730  	})
   731  	c.Assert(err, gc.ErrorMatches,
   732  		`application "wordpress" wants to bind endpoint "foo" to space "bar", `+
   733  			`but the endpoint is not defined by the charm`,
   734  	)
   735  }
   736  
   737  func (s *bundleDataSuite) TestVerifyBundleWithExtraBindingsSuccess(c *gc.C) {
   738  	err := s.testPrepareAndMutateBeforeVerifyWithCharms(c, func(bd *charm.BundleData) {
   739  		// Both of these are specified in extra-bindings.
   740  		bd.Applications["wordpress"].EndpointBindings["admin-api"] = "internal"
   741  		bd.Applications["wordpress"].EndpointBindings["foo-bar"] = "test"
   742  	})
   743  	c.Assert(err, gc.IsNil)
   744  }
   745  
   746  func (s *bundleDataSuite) TestVerifyBundleWithRelationNameBindingSuccess(c *gc.C) {
   747  	err := s.testPrepareAndMutateBeforeVerifyWithCharms(c, func(bd *charm.BundleData) {
   748  		// Both of these are specified in as relations.
   749  		bd.Applications["wordpress"].EndpointBindings["cache"] = "foo"
   750  		bd.Applications["wordpress"].EndpointBindings["monitoring-port"] = "bar"
   751  	})
   752  	c.Assert(err, gc.IsNil)
   753  }
   754  
   755  func (s *bundleDataSuite) TestParseKubernetesBundleType(c *gc.C) {
   756  	data := `
   757  bundle: kubernetes
   758  
   759  applications:
   760      mariadb:
   761          charm: "mariadb-k8s"
   762          scale: 2
   763          placement: foo=bar
   764      gitlab:
   765          charm: "gitlab-k8s"
   766          num_units: 3
   767          to: [foo=baz]
   768          series: kubernetes
   769      redis:
   770          charm: "redis-k8s"
   771          scale: 3
   772          to: [foo=baz]
   773  `
   774  	bd, err := charm.ReadBundleData(strings.NewReader(data))
   775  	c.Assert(err, gc.IsNil)
   776  	err = bd.Verify(nil, nil, nil)
   777  	c.Assert(err, jc.ErrorIsNil)
   778  	c.Assert(bd, jc.DeepEquals, &charm.BundleData{
   779  		Type: "kubernetes",
   780  		Applications: map[string]*charm.ApplicationSpec{
   781  			"mariadb": {
   782  				Charm:    "mariadb-k8s",
   783  				To:       []string{"foo=bar"},
   784  				NumUnits: 2,
   785  			},
   786  			"gitlab": {
   787  				Charm:    "gitlab-k8s",
   788  				Series:   "kubernetes",
   789  				To:       []string{"foo=baz"},
   790  				NumUnits: 3,
   791  			},
   792  			"redis": {
   793  				Charm:    "redis-k8s",
   794  				To:       []string{"foo=baz"},
   795  				NumUnits: 3,
   796  			}},
   797  	})
   798  }
   799  
   800  func (s *bundleDataSuite) TestInvalidBundleType(c *gc.C) {
   801  	data := `
   802  bundle: foo
   803  
   804  applications:
   805      mariadb:
   806          charm: mariadb-k8s
   807          scale: 2
   808  `
   809  	bd, err := charm.ReadBundleData(strings.NewReader(data))
   810  	c.Assert(err, gc.IsNil)
   811  	err = bd.Verify(nil, nil, nil)
   812  	c.Assert(err, gc.ErrorMatches, `bundle has an invalid type "foo"`)
   813  }
   814  
   815  func (s *bundleDataSuite) TestInvalidScaleAndNumUnits(c *gc.C) {
   816  	data := `
   817  bundle: kubernetes
   818  
   819  applications:
   820      mariadb:
   821          charm: "mariadb-k8s"
   822          scale: 2
   823          num_units: 2
   824  `
   825  	_, err := charm.ReadBundleData(strings.NewReader(data))
   826  	c.Assert(err, gc.ErrorMatches, `.*cannot specify both scale and num_units for application "mariadb"`)
   827  }
   828  
   829  func (s *bundleDataSuite) TestInvalidPlacementAndTo(c *gc.C) {
   830  	data := `
   831  bundle: kubernetes
   832  
   833  applications:
   834      mariadb:
   835          charm: "mariadb-k8s"
   836          placement: foo=bar
   837          to: [foo=bar]
   838  `
   839  	_, err := charm.ReadBundleData(strings.NewReader(data))
   840  	c.Assert(err, gc.ErrorMatches, `.*cannot specify both placement and to for application "mariadb"`)
   841  }
   842  
   843  func (s *bundleDataSuite) TestInvalidIAASPlacement(c *gc.C) {
   844  	data := `
   845  applications:
   846      mariadb:
   847          charm: "mariadb"
   848          placement: foo=bar
   849  `
   850  	_, err := charm.ReadBundleData(strings.NewReader(data))
   851  	c.Assert(err, gc.ErrorMatches, `.*placement \(foo=bar\) not valid for non-Kubernetes application "mariadb"`)
   852  }
   853  
   854  func (s *bundleDataSuite) TestKubernetesBundleErrors(c *gc.C) {
   855  	data := `
   856  bundle: "kubernetes"
   857  series: "xenial"
   858  
   859  machines:
   860      0:
   861  
   862  applications:
   863      mariadb:
   864          charm: "mariadb-k8s"
   865          series: "xenial"
   866          scale: 2
   867      casandra:
   868          charm: "casnadra-k8s"
   869          to: ["foo=bar", "foo=baz"]
   870      hadoop:
   871          charm: "hadoop-k8s"
   872          to: ["foo"]
   873  `
   874  	errors := []string{
   875  		`expected "key=value", got "foo" for application "hadoop"`,
   876  		`bundle machines not valid for Kubernetes bundles`,
   877  		`too many placement directives for application "casandra"`,
   878  	}
   879  
   880  	assertVerifyErrors(c, data, nil, errors)
   881  }
   882  
   883  func (*bundleDataSuite) TestRequiredCharms(c *gc.C) {
   884  	bd, err := charm.ReadBundleData(strings.NewReader(mediawikiBundle))
   885  	c.Assert(err, gc.IsNil)
   886  	reqCharms := bd.RequiredCharms()
   887  
   888  	c.Assert(reqCharms, gc.DeepEquals, []string{"mediawiki", "mysql"})
   889  }
   890  
   891  // testCharm returns a charm with the given name
   892  // and relations. The relations are specified as
   893  // a string of the form:
   894  //
   895  //	<provides-relations> | <requires-relations>
   896  //
   897  // Within each section, each white-space separated
   898  // relation is specified as:
   899  // /	<relation-name>:<interface>
   900  //
   901  // So, for example:
   902  //
   903  //	testCharm("wordpress", "web:http | db:mysql")
   904  //
   905  // is equivalent to a charm with metadata.yaml containing
   906  //
   907  //	name: wordpress
   908  //	description: wordpress
   909  //	provides:
   910  //	    web:
   911  //	        interface: http
   912  //	requires:
   913  //	    db:
   914  //	        interface: mysql
   915  //
   916  // If the charm name has a "-sub" suffix, the
   917  // returned charm will have Meta.Subordinate = true.
   918  func testCharm(name string, relations string) charm.Charm {
   919  	var provides, requires string
   920  	parts := strings.Split(relations, "|")
   921  	provides = parts[0]
   922  	if len(parts) > 1 {
   923  		requires = parts[1]
   924  	}
   925  	meta := &charm.Meta{
   926  		Name:        name,
   927  		Summary:     name,
   928  		Description: name,
   929  		Provides:    parseRelations(provides, charm.RoleProvider),
   930  		Requires:    parseRelations(requires, charm.RoleRequirer),
   931  	}
   932  	if strings.HasSuffix(name, "-sub") {
   933  		meta.Subordinate = true
   934  	}
   935  	configStr := `
   936  options:
   937    title: {default: My Title, description: title, type: string}
   938    skill-level: {description: skill, type: int}
   939  `
   940  	config, err := charm.ReadConfig(strings.NewReader(configStr))
   941  	if err != nil {
   942  		panic(err)
   943  	}
   944  	return testCharmImpl{
   945  		meta:   meta,
   946  		config: config,
   947  	}
   948  }
   949  
   950  func parseRelations(s string, role charm.RelationRole) map[string]charm.Relation {
   951  	rels := make(map[string]charm.Relation)
   952  	for _, r := range strings.Fields(s) {
   953  		parts := strings.Split(r, ":")
   954  		if len(parts) != 2 {
   955  			panic(fmt.Errorf("invalid relation specifier %q", r))
   956  		}
   957  		name, interf := parts[0], parts[1]
   958  		rels[name] = charm.Relation{
   959  			Name:      name,
   960  			Role:      role,
   961  			Interface: interf,
   962  			Scope:     charm.ScopeGlobal,
   963  		}
   964  	}
   965  	return rels
   966  }
   967  
   968  type testCharmImpl struct {
   969  	meta   *charm.Meta
   970  	config *charm.Config
   971  	// Implement charm.Charm, but panic if anything other than
   972  	// Meta or Config methods are called.
   973  	charm.Charm
   974  }
   975  
   976  func (c testCharmImpl) Meta() *charm.Meta {
   977  	return c.meta
   978  }
   979  
   980  func (c testCharmImpl) Config() *charm.Config {
   981  	return c.config
   982  }
   983  
   984  var verifyWithCharmsErrorsTests = []struct {
   985  	about  string
   986  	data   string
   987  	charms map[string]charm.Charm
   988  
   989  	errors []string
   990  }{{
   991  	about:  "no charms",
   992  	data:   mediawikiBundle,
   993  	charms: map[string]charm.Charm{},
   994  	errors: []string{
   995  		`application "mediawiki" refers to non-existent charm "mediawiki"`,
   996  		`application "mysql" refers to non-existent charm "mysql"`,
   997  	},
   998  }, {
   999  	about: "all present and correct",
  1000  	data: `
  1001  applications:
  1002      application1:
  1003          charm: "test"
  1004      application2:
  1005          charm: "test"
  1006      application3:
  1007          charm: "test"
  1008  relations:
  1009      - ["application1:prova", "application2:reqa"]
  1010      - ["application1:reqa", "application3:prova"]
  1011      - ["application3:provb", "application2:reqb"]
  1012  `,
  1013  	charms: map[string]charm.Charm{
  1014  		"test": testCharm("test", "prova:a provb:b | reqa:a reqb:b"),
  1015  	},
  1016  }, {
  1017  	about: "undefined relations",
  1018  	data: `
  1019  applications:
  1020      application1:
  1021          charm: "test"
  1022      application2:
  1023          charm: "test"
  1024  relations:
  1025      - ["application1:prova", "application2:blah"]
  1026      - ["application1:blah", "application2:prova"]
  1027  `,
  1028  	charms: map[string]charm.Charm{
  1029  		"test": testCharm("test", "prova:a provb:b | reqa:a reqb:b"),
  1030  	},
  1031  	errors: []string{
  1032  		`charm "test" used by application "application1" does not define relation "blah"`,
  1033  		`charm "test" used by application "application2" does not define relation "blah"`,
  1034  	},
  1035  }, {
  1036  	about: "undefined applications",
  1037  	data: `
  1038  applications:
  1039      application1:
  1040          charm: "test"
  1041      application2:
  1042          charm: "test"
  1043  relations:
  1044      - ["unknown:prova", "application2:blah"]
  1045      - ["application1:blah", "unknown:prova"]
  1046  `,
  1047  	charms: map[string]charm.Charm{
  1048  		"test": testCharm("test", "prova:a provb:b | reqa:a reqb:b"),
  1049  	},
  1050  	errors: []string{
  1051  		`relation ["application1:blah" "unknown:prova"] refers to application "unknown" not defined in this bundle`,
  1052  		`relation ["unknown:prova" "application2:blah"] refers to application "unknown" not defined in this bundle`,
  1053  	},
  1054  }, {
  1055  	about: "equal applications",
  1056  	data: `
  1057  applications:
  1058      application1:
  1059          charm: "test"
  1060      application2:
  1061          charm: "test"
  1062  relations:
  1063      - ["application2:prova", "application2:reqa"]
  1064  `,
  1065  	charms: map[string]charm.Charm{
  1066  		"test": testCharm("test", "prova:a provb:b | reqa:a reqb:b"),
  1067  	},
  1068  	errors: []string{
  1069  		`relation ["application2:prova" "application2:reqa"] relates an application to itself`,
  1070  	},
  1071  }, {
  1072  	about: "provider to provider relation",
  1073  	data: `
  1074  applications:
  1075      application1:
  1076          charm: "test"
  1077      application2:
  1078          charm: "test"
  1079  relations:
  1080      - ["application1:prova", "application2:prova"]
  1081  `,
  1082  	charms: map[string]charm.Charm{
  1083  		"test": testCharm("test", "prova:a provb:b | reqa:a reqb:b"),
  1084  	},
  1085  	errors: []string{
  1086  		`relation "application1:prova" to "application2:prova" relates provider to provider`,
  1087  	},
  1088  }, {
  1089  	about: "provider to provider relation",
  1090  	data: `
  1091  applications:
  1092      application1:
  1093          charm: "test"
  1094      application2:
  1095          charm: "test"
  1096  relations:
  1097      - ["application1:reqa", "application2:reqa"]
  1098  `,
  1099  	charms: map[string]charm.Charm{
  1100  		"test": testCharm("test", "prova:a provb:b | reqa:a reqb:b"),
  1101  	},
  1102  	errors: []string{
  1103  		`relation "application1:reqa" to "application2:reqa" relates requirer to requirer`,
  1104  	},
  1105  }, {
  1106  	about: "interface mismatch",
  1107  	data: `
  1108  applications:
  1109      application1:
  1110          charm: "test"
  1111      application2:
  1112          charm: "test"
  1113  relations:
  1114      - ["application1:reqa", "application2:provb"]
  1115  `,
  1116  	charms: map[string]charm.Charm{
  1117  		"test": testCharm("test", "prova:a provb:b | reqa:a reqb:b"),
  1118  	},
  1119  	errors: []string{
  1120  		`mismatched interface between "application2:provb" and "application1:reqa" ("b" vs "a")`,
  1121  	},
  1122  }, {
  1123  	about: "different charms",
  1124  	data: `
  1125  applications:
  1126      application1:
  1127          charm: "test1"
  1128      application2:
  1129          charm: "test2"
  1130  relations:
  1131      - ["application1:reqa", "application2:prova"]
  1132  `,
  1133  	charms: map[string]charm.Charm{
  1134  		"test1": testCharm("test", "prova:a provb:b | reqa:a reqb:b"),
  1135  		"test2": testCharm("test", ""),
  1136  	},
  1137  	errors: []string{
  1138  		`charm "test2" used by application "application2" does not define relation "prova"`,
  1139  	},
  1140  }, {
  1141  	about: "ambiguous relation",
  1142  	data: `
  1143  applications:
  1144      application1:
  1145          charm: "test1"
  1146      application2:
  1147          charm: "test2"
  1148  relations:
  1149      - [application1, application2]
  1150  `,
  1151  	charms: map[string]charm.Charm{
  1152  		"test1": testCharm("test", "prova:a provb:b | reqa:a reqb:b"),
  1153  		"test2": testCharm("test", "prova:a provb:b | reqa:a reqb:b"),
  1154  	},
  1155  	errors: []string{
  1156  		`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"`,
  1157  	},
  1158  }, {
  1159  	about: "relation using juju-info",
  1160  	data: `
  1161  applications:
  1162      application1:
  1163          charm: "provider"
  1164      application2:
  1165          charm: "requirer"
  1166  relations:
  1167      - [application1, application2]
  1168  `,
  1169  	charms: map[string]charm.Charm{
  1170  		"provider": testCharm("provider", ""),
  1171  		"requirer": testCharm("requirer", "| req:juju-info"),
  1172  	},
  1173  }, {
  1174  	about: "ambiguous when implicit relations taken into account",
  1175  	data: `
  1176  applications:
  1177      application1:
  1178          charm: "provider"
  1179      application2:
  1180          charm: "requirer"
  1181  relations:
  1182      - [application1, application2]
  1183  `,
  1184  	charms: map[string]charm.Charm{
  1185  		"provider": testCharm("provider", "provdb:db | "),
  1186  		"requirer": testCharm("requirer", "| reqdb:db reqinfo:juju-info"),
  1187  	},
  1188  }, {
  1189  	about: "half of relation left open",
  1190  	data: `
  1191  applications:
  1192      application1:
  1193          charm: "provider"
  1194      application2:
  1195          charm: "requirer"
  1196  relations:
  1197      - ["application1:prova2", application2]
  1198  `,
  1199  	charms: map[string]charm.Charm{
  1200  		"provider": testCharm("provider", "prova1:a prova2:a | "),
  1201  		"requirer": testCharm("requirer", "| reqa:a"),
  1202  	},
  1203  }, {
  1204  	about: "duplicate relation between open and fully-specified relations",
  1205  	data: `
  1206  applications:
  1207      application1:
  1208          charm: "provider"
  1209      application2:
  1210          charm: "requirer"
  1211  relations:
  1212      - ["application1:prova", "application2:reqa"]
  1213      - ["application1", "application2"]
  1214  `,
  1215  	charms: map[string]charm.Charm{
  1216  		"provider": testCharm("provider", "prova:a | "),
  1217  		"requirer": testCharm("requirer", "| reqa:a"),
  1218  	},
  1219  	errors: []string{
  1220  		`relation ["application1" "application2"] is defined more than once`,
  1221  	},
  1222  }, {
  1223  	about: "configuration options specified",
  1224  	data: `
  1225  applications:
  1226      application1:
  1227          charm: "test"
  1228          options:
  1229              title: "some title"
  1230              skill-level: 245
  1231      application2:
  1232          charm: "test"
  1233          options:
  1234              title: "another title"
  1235  `,
  1236  	charms: map[string]charm.Charm{
  1237  		"test": testCharm("test", "prova:a provb:b | reqa:a reqb:b"),
  1238  	},
  1239  }, {
  1240  	about: "invalid type for option",
  1241  	data: `
  1242  applications:
  1243      application1:
  1244          charm: "test"
  1245          options:
  1246              title: "some title"
  1247              skill-level: "too much"
  1248      application2:
  1249          charm: "test"
  1250          options:
  1251              title: "another title"
  1252  `,
  1253  	charms: map[string]charm.Charm{
  1254  		"test": testCharm("test", "prova:a provb:b | reqa:a reqb:b"),
  1255  	},
  1256  	errors: []string{
  1257  		`cannot validate application "application1": option "skill-level" expected int, got "too much"`,
  1258  	},
  1259  }, {
  1260  	about: "unknown option",
  1261  	data: `
  1262  applications:
  1263      application1:
  1264          charm: "test"
  1265          options:
  1266              title: "some title"
  1267              unknown-option: 2345
  1268  `,
  1269  	charms: map[string]charm.Charm{
  1270  		"test": testCharm("test", "prova:a provb:b | reqa:a reqb:b"),
  1271  	},
  1272  	errors: []string{
  1273  		`cannot validate application "application1": configuration option "unknown-option" not found in charm "test"`,
  1274  	},
  1275  }, {
  1276  	about: "multiple config problems",
  1277  	data: `
  1278  applications:
  1279      application1:
  1280          charm: "test"
  1281          options:
  1282              title: "some title"
  1283              unknown-option: 2345
  1284      application2:
  1285          charm: "test"
  1286          options:
  1287              title: 123
  1288              another-unknown: 2345
  1289  `,
  1290  	charms: map[string]charm.Charm{
  1291  		"test": testCharm("test", "prova:a provb:b | reqa:a reqb:b"),
  1292  	},
  1293  	errors: []string{
  1294  		`cannot validate application "application1": configuration option "unknown-option" not found in charm "test"`,
  1295  		`cannot validate application "application2": configuration option "another-unknown" not found in charm "test"`,
  1296  		`cannot validate application "application2": option "title" expected string, got 123`,
  1297  	},
  1298  }, {
  1299  	about: "subordinate charm with more than zero units",
  1300  	data: `
  1301  applications:
  1302      testsub:
  1303          charm: "testsub"
  1304          num_units: 1
  1305  `,
  1306  	charms: map[string]charm.Charm{
  1307  		"testsub": testCharm("test-sub", ""),
  1308  	},
  1309  	errors: []string{
  1310  		`application "testsub" is subordinate but has non-zero num_units`,
  1311  	},
  1312  }, {
  1313  	about: "subordinate charm with more than one unit",
  1314  	data: `
  1315  applications:
  1316      testsub:
  1317          charm: "testsub"
  1318          num_units: 1
  1319  `,
  1320  	charms: map[string]charm.Charm{
  1321  		"testsub": testCharm("test-sub", ""),
  1322  	},
  1323  	errors: []string{
  1324  		`application "testsub" is subordinate but has non-zero num_units`,
  1325  	},
  1326  }, {
  1327  	about: "subordinate charm with to-clause",
  1328  	data: `
  1329  applications:
  1330      testsub:
  1331          charm: "testsub"
  1332          to: [0]
  1333  machines:
  1334      0:
  1335  `,
  1336  	charms: map[string]charm.Charm{
  1337  		"testsub": testCharm("test-sub", ""),
  1338  	},
  1339  	errors: []string{
  1340  		`application "testsub" is subordinate but specifies unit placement`,
  1341  		`too many units specified in unit placement for application "testsub"`,
  1342  	},
  1343  }, {
  1344  	about: "charm with unspecified units and more than one to: entry",
  1345  	data: `
  1346  applications:
  1347      test:
  1348          charm: "test"
  1349          to: [0, 1]
  1350  machines:
  1351      0:
  1352      1:
  1353  `,
  1354  	errors: []string{
  1355  		`too many units specified in unit placement for application "test"`,
  1356  	},
  1357  }, {
  1358  	about: "charmhub charm revision and no channel",
  1359  	data: `
  1360  applications:
  1361      wordpress:
  1362        charm: "wordpress"
  1363        revision: 5
  1364        num_units: 1
  1365  `,
  1366  	errors: []string{
  1367  		`application "wordpress" with a revision requires a channel for future upgrades, please use channel`,
  1368  	},
  1369  }, {
  1370  	about: "charmhub charm revision in charm url",
  1371  	data: `
  1372  applications:
  1373      wordpress:
  1374        charm: "wordpress-9"
  1375        num_units: 1
  1376  `,
  1377  	errors: []string{
  1378  		`cannot specify revision in "ch:wordpress-9", please use revision`,
  1379  	},
  1380  }, {
  1381  	about: "charmstore charm url revision value less than 0",
  1382  	data: `
  1383  applications:
  1384      wordpress:
  1385        charm: "wordpress"
  1386        revision: -5
  1387        channel: edge
  1388        num_units: 1
  1389  `,
  1390  	errors: []string{
  1391  		`the revision for application "wordpress" must be zero or greater`,
  1392  	},
  1393  }}
  1394  
  1395  func (*bundleDataSuite) TestVerifyWithCharmsErrors(c *gc.C) {
  1396  	for i, test := range verifyWithCharmsErrorsTests {
  1397  		c.Logf("test %d: %s", i, test.about)
  1398  		assertVerifyErrors(c, test.data, test.charms, test.errors)
  1399  	}
  1400  }
  1401  
  1402  var parsePlacementTests = []struct {
  1403  	placement string
  1404  	expect    *charm.UnitPlacement
  1405  	expectErr string
  1406  }{{
  1407  	placement: "lxc:application/0",
  1408  	expect: &charm.UnitPlacement{
  1409  		ContainerType: "lxc",
  1410  		Application:   "application",
  1411  		Unit:          0,
  1412  	},
  1413  }, {
  1414  	placement: "lxc:application",
  1415  	expect: &charm.UnitPlacement{
  1416  		ContainerType: "lxc",
  1417  		Application:   "application",
  1418  		Unit:          -1,
  1419  	},
  1420  }, {
  1421  	placement: "lxc:99",
  1422  	expect: &charm.UnitPlacement{
  1423  		ContainerType: "lxc",
  1424  		Machine:       "99",
  1425  		Unit:          -1,
  1426  	},
  1427  }, {
  1428  	placement: "lxc:new",
  1429  	expect: &charm.UnitPlacement{
  1430  		ContainerType: "lxc",
  1431  		Machine:       "new",
  1432  		Unit:          -1,
  1433  	},
  1434  }, {
  1435  	placement: "application/0",
  1436  	expect: &charm.UnitPlacement{
  1437  		Application: "application",
  1438  		Unit:        0,
  1439  	},
  1440  }, {
  1441  	placement: "application",
  1442  	expect: &charm.UnitPlacement{
  1443  		Application: "application",
  1444  		Unit:        -1,
  1445  	},
  1446  }, {
  1447  	placement: "application45",
  1448  	expect: &charm.UnitPlacement{
  1449  		Application: "application45",
  1450  		Unit:        -1,
  1451  	},
  1452  }, {
  1453  	placement: "99",
  1454  	expect: &charm.UnitPlacement{
  1455  		Machine: "99",
  1456  		Unit:    -1,
  1457  	},
  1458  }, {
  1459  	placement: "new",
  1460  	expect: &charm.UnitPlacement{
  1461  		Machine: "new",
  1462  		Unit:    -1,
  1463  	},
  1464  }, {
  1465  	placement: ":0",
  1466  	expectErr: `invalid placement syntax ":0"`,
  1467  }, {
  1468  	placement: "05",
  1469  	expectErr: `invalid placement syntax "05"`,
  1470  }, {
  1471  	placement: "new/2",
  1472  	expectErr: `invalid placement syntax "new/2"`,
  1473  }}
  1474  
  1475  func (*bundleDataSuite) TestParsePlacement(c *gc.C) {
  1476  	for i, test := range parsePlacementTests {
  1477  		c.Logf("test %d: %q", i, test.placement)
  1478  		up, err := charm.ParsePlacement(test.placement)
  1479  		if test.expectErr != "" {
  1480  			c.Assert(err, gc.ErrorMatches, test.expectErr)
  1481  		} else {
  1482  			c.Assert(err, gc.IsNil)
  1483  			c.Assert(up, jc.DeepEquals, test.expect)
  1484  		}
  1485  	}
  1486  }
  1487  
  1488  // Tests that empty/nil applications cause an error
  1489  func (*bundleDataSuite) TestApplicationEmpty(c *gc.C) {
  1490  	tstDatas := []string{
  1491  		`
  1492  applications:
  1493      application1:
  1494      application2:
  1495          charm: "test"
  1496          plan: "testisv/test2"
  1497  `,
  1498  		`
  1499  applications:
  1500      application1:
  1501          charm: "test"
  1502          plan: "testisv/test2"
  1503      application2:
  1504  `,
  1505  		`
  1506  applications:
  1507      application1:
  1508          charm: "test"
  1509          plan: "testisv/test2"
  1510      application2: ~
  1511  `,
  1512  	}
  1513  
  1514  	for _, d := range tstDatas {
  1515  		bd, err := charm.ReadBundleData(strings.NewReader(d))
  1516  		c.Assert(err, gc.IsNil)
  1517  
  1518  		err = bd.Verify(nil, nil, nil)
  1519  		c.Assert(err, gc.ErrorMatches, "bundle application for key .+ is undefined")
  1520  	}
  1521  }
  1522  
  1523  func (*bundleDataSuite) TestApplicationPlans(c *gc.C) {
  1524  	data := `
  1525  applications:
  1526      application1:
  1527          charm: "test"
  1528          plan: "testisv/test"
  1529      application2:
  1530          charm: "test"
  1531          plan: "testisv/test2"
  1532      application3:
  1533          charm: "test"
  1534          plan: "default"
  1535  relations:
  1536      - ["application1:prova", "application2:reqa"]
  1537      - ["application1:reqa", "application3:prova"]
  1538      - ["application3:provb", "application2:reqb"]
  1539  `
  1540  
  1541  	bd, err := charm.ReadBundleData(strings.NewReader(data))
  1542  	c.Assert(err, gc.IsNil)
  1543  
  1544  	c.Assert(bd.Applications, jc.DeepEquals, map[string]*charm.ApplicationSpec{
  1545  		"application1": {
  1546  			Charm: "test",
  1547  			Plan:  "testisv/test",
  1548  		},
  1549  		"application2": {
  1550  			Charm: "test",
  1551  			Plan:  "testisv/test2",
  1552  		},
  1553  		"application3": {
  1554  			Charm: "test",
  1555  			Plan:  "default",
  1556  		},
  1557  	})
  1558  
  1559  }