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 }