github.com/juju/charm/v11@v11.2.0/meta.go (about) 1 // Copyright 2011, 2012, 2013 Canonical Ltd. 2 // Licensed under the LGPLv3, see LICENCE file for details. 3 4 package charm 5 6 import ( 7 "fmt" 8 "io" 9 "io/ioutil" 10 "regexp" 11 "sort" 12 "strconv" 13 "strings" 14 15 "github.com/juju/collections/set" 16 "github.com/juju/errors" 17 "github.com/juju/names/v4" 18 "github.com/juju/os/v2" 19 "github.com/juju/os/v2/series" 20 "github.com/juju/schema" 21 "github.com/juju/utils/v3" 22 "github.com/juju/version/v2" 23 "gopkg.in/yaml.v2" 24 25 "github.com/juju/charm/v11/assumes" 26 "github.com/juju/charm/v11/hooks" 27 "github.com/juju/charm/v11/resource" 28 ) 29 30 // RelationScope describes the scope of a relation. 31 type RelationScope string 32 33 // Note that schema doesn't support custom string types, 34 // so when we use these values in a schema.Checker, 35 // we must store them as strings, not RelationScopes. 36 37 const ( 38 ScopeGlobal RelationScope = "global" 39 ScopeContainer RelationScope = "container" 40 ) 41 42 // RelationRole defines the role of a relation. 43 type RelationRole string 44 45 const ( 46 RoleProvider RelationRole = "provider" 47 RoleRequirer RelationRole = "requirer" 48 RolePeer RelationRole = "peer" 49 ) 50 51 // StorageType defines a storage type. 52 type StorageType string 53 54 const ( 55 StorageBlock StorageType = "block" 56 StorageFilesystem StorageType = "filesystem" 57 ) 58 59 // Storage represents a charm's storage requirement. 60 type Storage struct { 61 // Name is the name of the store. 62 // 63 // Name has no default, and must be specified. 64 Name string `bson:"name"` 65 66 // Description is a description of the store. 67 // 68 // Description has no default, and is optional. 69 Description string `bson:"description"` 70 71 // Type is the storage type: filesystem or block-device. 72 // 73 // Type has no default, and must be specified. 74 Type StorageType `bson:"type"` 75 76 // Shared indicates that the storage is shared between all units of 77 // an application deployed from the charm. It is an error to attempt to 78 // assign non-shareable storage to a "shared" storage requirement. 79 // 80 // Shared defaults to false. 81 Shared bool `bson:"shared"` 82 83 // ReadOnly indicates that the storage should be made read-only if 84 // possible. If the storage cannot be made read-only, Juju will warn 85 // the user. 86 // 87 // ReadOnly defaults to false. 88 ReadOnly bool `bson:"read-only"` 89 90 // CountMin is the number of storage instances that must be attached 91 // to the charm for it to be useful; the charm will not install until 92 // this number has been satisfied. This must be a non-negative number. 93 // 94 // CountMin defaults to 1 for singleton stores. 95 CountMin int `bson:"countmin"` 96 97 // CountMax is the largest number of storage instances that can be 98 // attached to the charm. If CountMax is -1, then there is no upper 99 // bound. 100 // 101 // CountMax defaults to 1 for singleton stores. 102 CountMax int `bson:"countmax"` 103 104 // MinimumSize is the minimum size of store that the charm needs to 105 // work at all. This is not a recommended size or a comfortable size 106 // or a will-work-well size, just a bare minimum below which the charm 107 // is going to break. 108 // MinimumSize requires a unit, one of MGTPEZY, and is stored as MiB. 109 // 110 // There is no default MinimumSize; if left unspecified, a provider 111 // specific default will be used, typically 1GB for block storage. 112 MinimumSize uint64 `bson:"minimum-size"` 113 114 // Location is the mount location for filesystem stores. For multi- 115 // stores, the location acts as the parent directory for each mounted 116 // store. 117 // 118 // Location has no default, and is optional. 119 Location string `bson:"location,omitempty"` 120 121 // Properties allow the charm author to characterise the relative storage 122 // performance requirements and sensitivities for each store. 123 // eg “transient” is used to indicate that non persistent storage is acceptable, 124 // such as tmpfs or ephemeral instance disks. 125 // 126 // Properties has no default, and is optional. 127 Properties []string `bson:"properties,omitempty"` 128 } 129 130 // DeviceType defines a device type. 131 type DeviceType string 132 133 // Device represents a charm's device requirement (GPU for example). 134 type Device struct { 135 // Name is the name of the device. 136 Name string `bson:"name"` 137 138 // Description is a description of the device. 139 Description string `bson:"description"` 140 141 // Type is the device type. 142 // currently supported types are 143 // - gpu 144 // - nvidia.com/gpu 145 // - amd.com/gpu 146 Type DeviceType `bson:"type"` 147 148 // CountMin is the min number of devices that the charm requires. 149 CountMin int64 `bson:"countmin"` 150 151 // CountMax is the max number of devices that the charm requires. 152 CountMax int64 `bson:"countmax"` 153 } 154 155 // DeploymentType defines a deployment type. 156 type DeploymentType string 157 158 const ( 159 DeploymentStateless DeploymentType = "stateless" 160 DeploymentStateful DeploymentType = "stateful" 161 DeploymentDaemon DeploymentType = "daemon" 162 ) 163 164 // DeploymentMode defines a deployment mode. 165 type DeploymentMode string 166 167 const ( 168 ModeOperator DeploymentMode = "operator" 169 ModeWorkload DeploymentMode = "workload" 170 ) 171 172 // ServiceType defines a service type. 173 type ServiceType string 174 175 const ( 176 ServiceCluster ServiceType = "cluster" 177 ServiceLoadBalancer ServiceType = "loadbalancer" 178 ServiceExternal ServiceType = "external" 179 ServiceOmit ServiceType = "omit" 180 ) 181 182 var validServiceTypes = map[os.OSType][]ServiceType{ 183 os.Kubernetes: { 184 ServiceCluster, 185 ServiceLoadBalancer, 186 ServiceExternal, 187 ServiceOmit, 188 }, 189 } 190 191 // Deployment represents a charm's deployment requirements in the charm 192 // metadata.yaml file. 193 type Deployment struct { 194 DeploymentType DeploymentType `bson:"type"` 195 DeploymentMode DeploymentMode `bson:"mode"` 196 ServiceType ServiceType `bson:"service"` 197 MinVersion string `bson:"min-version"` 198 } 199 200 // Relation represents a single relation defined in the charm 201 // metadata.yaml file. 202 type Relation struct { 203 Name string `bson:"name"` 204 Role RelationRole `bson:"role"` 205 Interface string `bson:"interface"` 206 Optional bool `bson:"optional"` 207 Limit int `bson:"limit"` 208 Scope RelationScope `bson:"scope"` 209 } 210 211 // ImplementedBy returns whether the relation is implemented by the supplied charm. 212 func (r Relation) ImplementedBy(ch Charm) bool { 213 if r.IsImplicit() { 214 return true 215 } 216 var m map[string]Relation 217 switch r.Role { 218 case RoleProvider: 219 m = ch.Meta().Provides 220 case RoleRequirer: 221 m = ch.Meta().Requires 222 case RolePeer: 223 m = ch.Meta().Peers 224 default: 225 panic(errors.Errorf("unknown relation role %q", r.Role)) 226 } 227 rel, found := m[r.Name] 228 if !found { 229 return false 230 } 231 if rel.Interface == r.Interface { 232 switch r.Scope { 233 case ScopeGlobal: 234 return rel.Scope != ScopeContainer 235 case ScopeContainer: 236 return true 237 default: 238 panic(errors.Errorf("unknown relation scope %q", r.Scope)) 239 } 240 } 241 return false 242 } 243 244 // IsImplicit returns whether the relation is supplied by juju itself, 245 // rather than by a charm. 246 func (r Relation) IsImplicit() bool { 247 return (r.Name == "juju-info" && 248 r.Interface == "juju-info" && 249 r.Role == RoleProvider) 250 } 251 252 // Meta represents all the known content that may be defined 253 // within a charm's metadata.yaml file. 254 // Note: Series is serialised for backward compatibility 255 // as "supported-series" because a previous 256 // charm version had an incompatible Series field that 257 // was unused in practice but still serialized. This 258 // only applies to JSON because Meta has a custom 259 // YAML marshaller. 260 type Meta struct { 261 Name string `bson:"name" json:"Name"` 262 Summary string `bson:"summary" json:"Summary"` 263 Description string `bson:"description" json:"Description"` 264 Subordinate bool `bson:"subordinate" json:"Subordinate"` 265 Provides map[string]Relation `bson:"provides,omitempty" json:"Provides,omitempty"` 266 Requires map[string]Relation `bson:"requires,omitempty" json:"Requires,omitempty"` 267 Peers map[string]Relation `bson:"peers,omitempty" json:"Peers,omitempty"` 268 ExtraBindings map[string]ExtraBinding `bson:"extra-bindings,omitempty" json:"ExtraBindings,omitempty"` 269 Categories []string `bson:"categories,omitempty" json:"Categories,omitempty"` 270 Tags []string `bson:"tags,omitempty" json:"Tags,omitempty"` 271 Series []string `bson:"series,omitempty" json:"SupportedSeries,omitempty"` 272 Storage map[string]Storage `bson:"storage,omitempty" json:"Storage,omitempty"` 273 Devices map[string]Device `bson:"devices,omitempty" json:"Devices,omitempty"` 274 Deployment *Deployment `bson:"deployment,omitempty" json:"Deployment,omitempty"` 275 PayloadClasses map[string]PayloadClass `bson:"payloadclasses,omitempty" json:"PayloadClasses,omitempty"` 276 Resources map[string]resource.Meta `bson:"resources,omitempty" json:"Resources,omitempty"` 277 Terms []string `bson:"terms,omitempty" json:"Terms,omitempty"` 278 MinJujuVersion version.Number `bson:"min-juju-version,omitempty" json:"min-juju-version,omitempty"` 279 280 // v2 281 Containers map[string]Container `bson:"containers,omitempty" json:"containers,omitempty" yaml:"containers,omitempty"` 282 Assumes *assumes.ExpressionTree `bson:"assumes,omitempty" json:"assumes,omitempty" yaml:"assumes,omitempty"` 283 } 284 285 // Container specifies the possible systems it supports and mounts it wants. 286 type Container struct { 287 Resource string `bson:"resource,omitempty" json:"resource,omitempty" yaml:"resource,omitempty"` 288 Mounts []Mount `bson:"mounts,omitempty" json:"mounts,omitempty" yaml:"mounts,omitempty"` 289 } 290 291 // Mount allows a container to mount a storage filesystem from the storage top-level directive. 292 type Mount struct { 293 Storage string `bson:"storage,omitempty" json:"storage,omitempty" yaml:"storage,omitempty"` 294 Location string `bson:"location,omitempty" json:"location,omitempty" yaml:"location,omitempty"` 295 } 296 297 func generateRelationHooks(relName string, allHooks map[string]bool) { 298 for _, hookName := range hooks.RelationHooks() { 299 allHooks[fmt.Sprintf("%s-%s", relName, hookName)] = true 300 } 301 } 302 303 func generateContainerHooks(containerName string, allHooks map[string]bool) { 304 // Containers using pebble trigger workload hooks. 305 for _, hookName := range hooks.WorkloadHooks() { 306 allHooks[fmt.Sprintf("%s-%s", containerName, hookName)] = true 307 } 308 } 309 310 func generateStorageHooks(storageName string, allHooks map[string]bool) { 311 for _, hookName := range hooks.StorageHooks() { 312 allHooks[fmt.Sprintf("%s-%s", storageName, hookName)] = true 313 } 314 } 315 316 // Hooks returns a map of all possible valid hooks, taking relations 317 // into account. It's a map to enable fast lookups, and the value is 318 // always true. 319 func (m Meta) Hooks() map[string]bool { 320 allHooks := make(map[string]bool) 321 // Unit hooks 322 for _, hookName := range hooks.UnitHooks() { 323 allHooks[string(hookName)] = true 324 } 325 // Secret hooks 326 for _, hookName := range hooks.SecretHooks() { 327 allHooks[string(hookName)] = true 328 } 329 // Relation hooks 330 for hookName := range m.Provides { 331 generateRelationHooks(hookName, allHooks) 332 } 333 for hookName := range m.Requires { 334 generateRelationHooks(hookName, allHooks) 335 } 336 for hookName := range m.Peers { 337 generateRelationHooks(hookName, allHooks) 338 } 339 for storageName := range m.Storage { 340 generateStorageHooks(storageName, allHooks) 341 } 342 for containerName := range m.Containers { 343 generateContainerHooks(containerName, allHooks) 344 } 345 return allHooks 346 } 347 348 // Used for parsing Categories and Tags. 349 func parseStringList(list interface{}) []string { 350 if list == nil { 351 return nil 352 } 353 slice := list.([]interface{}) 354 result := make([]string, 0, len(slice)) 355 for _, elem := range slice { 356 result = append(result, elem.(string)) 357 } 358 return result 359 } 360 361 var validTermName = regexp.MustCompile(`^[a-z](-?[a-z0-9]+)+$`) 362 363 // TermsId represents a single term id. The term can either be owned 364 // or "public" (meaning there is no owner). 365 // The Revision starts at 1. Therefore a value of 0 means the revision 366 // is unset. 367 type TermsId struct { 368 Tenant string 369 Owner string 370 Name string 371 Revision int 372 } 373 374 // Validate returns an error if the Term contains invalid data. 375 func (t *TermsId) Validate() error { 376 if t.Tenant != "" && t.Tenant != "cs" { 377 if !validTermName.MatchString(t.Tenant) { 378 return errors.Errorf("wrong term tenant format %q", t.Tenant) 379 } 380 } 381 if t.Owner != "" && !names.IsValidUser(t.Owner) { 382 return errors.Errorf("wrong owner format %q", t.Owner) 383 } 384 if !validTermName.MatchString(t.Name) { 385 return errors.Errorf("wrong term name format %q", t.Name) 386 } 387 if t.Revision < 0 { 388 return errors.Errorf("negative term revision") 389 } 390 return nil 391 } 392 393 // String returns the term in canonical form. 394 // This would be one of: 395 // 396 // tenant:owner/name/revision 397 // tenant:name 398 // owner/name/revision 399 // owner/name 400 // name/revision 401 // name 402 func (t *TermsId) String() string { 403 id := make([]byte, 0, len(t.Tenant)+1+len(t.Owner)+1+len(t.Name)+4) 404 if t.Tenant != "" { 405 id = append(id, t.Tenant...) 406 id = append(id, ':') 407 } 408 if t.Owner != "" { 409 id = append(id, t.Owner...) 410 id = append(id, '/') 411 } 412 id = append(id, t.Name...) 413 if t.Revision != 0 { 414 id = append(id, '/') 415 id = strconv.AppendInt(id, int64(t.Revision), 10) 416 } 417 return string(id) 418 } 419 420 // ParseTerm takes a termID as a string and parses it into a Term. 421 // A complete term is in the form: 422 // tenant:owner/name/revision 423 // This function accepts partially specified identifiers 424 // typically in one of the following forms: 425 // name 426 // owner/name 427 // owner/name/27 # Revision 27 428 // name/283 # Revision 283 429 // cs:owner/name # Tenant cs 430 func ParseTerm(s string) (*TermsId, error) { 431 tenant := "" 432 termid := s 433 if t := strings.SplitN(s, ":", 2); len(t) == 2 { 434 tenant = t[0] 435 termid = t[1] 436 } 437 438 tokens := strings.Split(termid, "/") 439 var term TermsId 440 switch len(tokens) { 441 case 1: // "name" 442 term = TermsId{ 443 Tenant: tenant, 444 Name: tokens[0], 445 } 446 case 2: // owner/name or name/123 447 termRevision, err := strconv.Atoi(tokens[1]) 448 if err != nil { // owner/name 449 term = TermsId{ 450 Tenant: tenant, 451 Owner: tokens[0], 452 Name: tokens[1], 453 } 454 } else { // name/123 455 term = TermsId{ 456 Tenant: tenant, 457 Name: tokens[0], 458 Revision: termRevision, 459 } 460 } 461 case 3: // owner/name/123 462 termRevision, err := strconv.Atoi(tokens[2]) 463 if err != nil { 464 return nil, errors.Errorf("invalid revision number %q %v", tokens[2], err) 465 } 466 term = TermsId{ 467 Tenant: tenant, 468 Owner: tokens[0], 469 Name: tokens[1], 470 Revision: termRevision, 471 } 472 default: 473 return nil, errors.Errorf("unknown term id format %q", s) 474 } 475 if err := term.Validate(); err != nil { 476 return nil, errors.Trace(err) 477 } 478 return &term, nil 479 } 480 481 // ReadMeta reads the content of a metadata.yaml file and returns 482 // its representation. 483 // The data has verified as unambiguous, but not validated. 484 func ReadMeta(r io.Reader) (*Meta, error) { 485 data, err := ioutil.ReadAll(r) 486 if err != nil { 487 return nil, err 488 } 489 var meta Meta 490 err = yaml.Unmarshal(data, &meta) 491 if err != nil { 492 return nil, err 493 } 494 return &meta, nil 495 } 496 497 // UnmarshalYAML 498 func (meta *Meta) UnmarshalYAML(f func(interface{}) error) error { 499 raw := make(map[interface{}]interface{}) 500 err := f(&raw) 501 if err != nil { 502 return err 503 } 504 505 if err := ensureUnambiguousFormat(raw); err != nil { 506 return err 507 } 508 509 v, err := charmSchema.Coerce(raw, nil) 510 if err != nil { 511 return errors.New("metadata: " + err.Error()) 512 } 513 514 m := v.(map[string]interface{}) 515 meta1, err := parseMeta(m) 516 if err != nil { 517 return err 518 } 519 520 *meta = *meta1 521 522 // Assumes blocks have their own dedicated parser so we need to invoke 523 // it here and attach the resulting expression tree (if any) to the 524 // metadata 525 var assumesBlock = struct { 526 Assumes *assumes.ExpressionTree `yaml:"assumes"` 527 }{} 528 if err := f(&assumesBlock); err != nil { 529 return err 530 } 531 meta.Assumes = assumesBlock.Assumes 532 533 return nil 534 } 535 536 func parseMeta(m map[string]interface{}) (*Meta, error) { 537 var meta Meta 538 var err error 539 540 meta.Name = m["name"].(string) 541 // Schema decodes as int64, but the int range should be good 542 // enough for revisions. 543 meta.Summary = m["summary"].(string) 544 meta.Description = m["description"].(string) 545 meta.Provides = parseRelations(m["provides"], RoleProvider) 546 meta.Requires = parseRelations(m["requires"], RoleRequirer) 547 meta.Peers = parseRelations(m["peers"], RolePeer) 548 if meta.ExtraBindings, err = parseMetaExtraBindings(m["extra-bindings"]); err != nil { 549 return nil, err 550 } 551 meta.Categories = parseStringList(m["categories"]) 552 meta.Tags = parseStringList(m["tags"]) 553 if subordinate := m["subordinate"]; subordinate != nil { 554 meta.Subordinate = subordinate.(bool) 555 } 556 meta.Series = parseStringList(m["series"]) 557 meta.Storage = parseStorage(m["storage"]) 558 meta.Devices = parseDevices(m["devices"]) 559 meta.Deployment, err = parseDeployment(m["deployment"], meta.Series, meta.Storage) 560 if err != nil { 561 return nil, err 562 } 563 meta.PayloadClasses = parsePayloadClasses(m["payloads"]) 564 565 if ver := m["min-juju-version"]; ver != nil { 566 minver, err := version.Parse(ver.(string)) 567 if err != nil { 568 return &meta, errors.Annotate(err, "invalid min-juju-version") 569 } 570 meta.MinJujuVersion = minver 571 } 572 meta.Terms = parseStringList(m["terms"]) 573 574 meta.Resources, err = parseMetaResources(m["resources"]) 575 if err != nil { 576 return nil, err 577 } 578 579 // v2 parsing 580 meta.Containers, err = parseContainers(m["containers"], meta.Resources, meta.Storage) 581 if err != nil { 582 return nil, errors.Annotatef(err, "parsing containers") 583 } 584 return &meta, nil 585 } 586 587 // MarshalYAML implements yaml.Marshaler (yaml.v2). 588 // It is recommended to call Check() before calling this method, 589 // otherwise you make get metadata which is not v1 nor v2 format. 590 func (m Meta) MarshalYAML() (interface{}, error) { 591 var minver string 592 if m.MinJujuVersion != version.Zero { 593 minver = m.MinJujuVersion.String() 594 } 595 596 return struct { 597 Name string `yaml:"name"` 598 Summary string `yaml:"summary"` 599 Description string `yaml:"description"` 600 Provides map[string]marshaledRelation `yaml:"provides,omitempty"` 601 Requires map[string]marshaledRelation `yaml:"requires,omitempty"` 602 Peers map[string]marshaledRelation `yaml:"peers,omitempty"` 603 ExtraBindings map[string]interface{} `yaml:"extra-bindings,omitempty"` 604 Categories []string `yaml:"categories,omitempty"` 605 Tags []string `yaml:"tags,omitempty"` 606 Subordinate bool `yaml:"subordinate,omitempty"` 607 Series []string `yaml:"series,omitempty"` 608 Storage map[string]Storage `yaml:"storage,omitempty"` 609 Devices map[string]Device `yaml:"devices,omitempty"` 610 Deployment *Deployment `yaml:"deployment,omitempty"` 611 Terms []string `yaml:"terms,omitempty"` 612 MinJujuVersion string `yaml:"min-juju-version,omitempty"` 613 Resources map[string]marshaledResourceMeta `yaml:"resources,omitempty"` 614 Containers map[string]marshaledContainer `yaml:"containers,omitempty"` 615 Assumes *assumes.ExpressionTree `yaml:"assumes,omitempty"` 616 }{ 617 Name: m.Name, 618 Summary: m.Summary, 619 Description: m.Description, 620 Provides: marshaledRelations(m.Provides), 621 Requires: marshaledRelations(m.Requires), 622 Peers: marshaledRelations(m.Peers), 623 ExtraBindings: marshaledExtraBindings(m.ExtraBindings), 624 Categories: m.Categories, 625 Tags: m.Tags, 626 Subordinate: m.Subordinate, 627 Series: m.Series, 628 Storage: m.Storage, 629 Devices: m.Devices, 630 Deployment: m.Deployment, 631 Terms: m.Terms, 632 MinJujuVersion: minver, 633 Resources: marshaledResources(m.Resources), 634 Containers: marshaledContainers(m.Containers), 635 Assumes: m.Assumes, 636 }, nil 637 } 638 639 type marshaledResourceMeta struct { 640 Path string `yaml:"filename"` // TODO(ericsnow) Change to "path"? 641 Type string `yaml:"type,omitempty"` 642 Description string `yaml:"description,omitempty"` 643 } 644 645 func marshaledResources(rs map[string]resource.Meta) map[string]marshaledResourceMeta { 646 rs1 := make(map[string]marshaledResourceMeta, len(rs)) 647 for name, r := range rs { 648 r1 := marshaledResourceMeta{ 649 Path: r.Path, 650 Description: r.Description, 651 } 652 if r.Type != resource.TypeFile { 653 r1.Type = r.Type.String() 654 } 655 rs1[name] = r1 656 } 657 return rs1 658 } 659 660 func marshaledRelations(relations map[string]Relation) map[string]marshaledRelation { 661 marshaled := make(map[string]marshaledRelation) 662 for name, relation := range relations { 663 marshaled[name] = marshaledRelation(relation) 664 } 665 return marshaled 666 } 667 668 type marshaledRelation Relation 669 670 func (r marshaledRelation) MarshalYAML() (interface{}, error) { 671 // See calls to ifaceExpander in charmSchema. 672 var noLimit int 673 if !r.Optional && r.Limit == noLimit && r.Scope == ScopeGlobal { 674 // All attributes are default, so use the simple string form of the relation. 675 return r.Interface, nil 676 } 677 mr := struct { 678 Interface string `yaml:"interface"` 679 Limit *int `yaml:"limit,omitempty"` 680 Optional bool `yaml:"optional,omitempty"` 681 Scope RelationScope `yaml:"scope,omitempty"` 682 }{ 683 Interface: r.Interface, 684 Optional: r.Optional, 685 } 686 if r.Limit != noLimit { 687 mr.Limit = &r.Limit 688 } 689 if r.Scope != ScopeGlobal { 690 mr.Scope = r.Scope 691 } 692 return mr, nil 693 } 694 695 func marshaledExtraBindings(bindings map[string]ExtraBinding) map[string]interface{} { 696 marshaled := make(map[string]interface{}) 697 for _, binding := range bindings { 698 marshaled[binding.Name] = nil 699 } 700 return marshaled 701 } 702 703 type marshaledContainer Container 704 705 func marshaledContainers(c map[string]Container) map[string]marshaledContainer { 706 marshaled := make(map[string]marshaledContainer) 707 for k, v := range c { 708 marshaled[k] = marshaledContainer(v) 709 } 710 return marshaled 711 } 712 713 func (c marshaledContainer) MarshalYAML() (interface{}, error) { 714 mc := struct { 715 Resource string `yaml:"resource,omitempty"` 716 Mounts []Mount `yaml:"mounts,omitempty"` 717 }{ 718 Resource: c.Resource, 719 Mounts: c.Mounts, 720 } 721 return mc, nil 722 } 723 724 // Format of the parsed charm. 725 type Format int 726 727 // Formats are the different versions of charm metadata supported. 728 const ( 729 FormatUnknown Format = iota 730 FormatV1 Format = iota 731 FormatV2 Format = iota 732 ) 733 734 // Check checks that the metadata is well-formed. 735 func (m Meta) Check(format Format, reasons ...FormatSelectionReason) error { 736 switch format { 737 case FormatV1: 738 err := m.checkV1(reasons) 739 if err != nil { 740 return errors.Trace(err) 741 } 742 case FormatV2: 743 err := m.checkV2(reasons) 744 if err != nil { 745 return errors.Trace(err) 746 } 747 default: 748 return errors.Errorf("unknown format %v", format) 749 } 750 751 // Check for duplicate or forbidden relation names or interfaces. 752 names := make(map[string]bool) 753 checkRelations := func(src map[string]Relation, role RelationRole) error { 754 for name, rel := range src { 755 if rel.Name != name { 756 return errors.Errorf("charm %q has mismatched relation name %q; expected %q", m.Name, rel.Name, name) 757 } 758 if rel.Role != role { 759 return errors.Errorf("charm %q has mismatched role %q; expected %q", m.Name, rel.Role, role) 760 } 761 // Container-scoped require relations on subordinates are allowed 762 // to use the otherwise-reserved juju-* namespace. 763 if !m.Subordinate || role != RoleRequirer || rel.Scope != ScopeContainer { 764 if reserved, _ := reservedName(m.Name, name); reserved { 765 return errors.Errorf("charm %q using a reserved relation name: %q", m.Name, name) 766 } 767 } 768 if role != RoleRequirer { 769 if reserved, _ := reservedName(m.Name, rel.Interface); reserved { 770 return errors.Errorf("charm %q relation %q using a reserved interface: %q", m.Name, name, rel.Interface) 771 } 772 } 773 if names[name] { 774 return errors.Errorf("charm %q using a duplicated relation name: %q", m.Name, name) 775 } 776 names[name] = true 777 } 778 return nil 779 } 780 if err := checkRelations(m.Provides, RoleProvider); err != nil { 781 return err 782 } 783 if err := checkRelations(m.Requires, RoleRequirer); err != nil { 784 return err 785 } 786 if err := checkRelations(m.Peers, RolePeer); err != nil { 787 return err 788 } 789 790 if err := validateMetaExtraBindings(m); err != nil { 791 return errors.Errorf("charm %q has invalid extra bindings: %v", m.Name, err) 792 } 793 794 // Subordinate charms must have at least one relation that 795 // has container scope, otherwise they can't relate to the 796 // principal. 797 if m.Subordinate { 798 valid := false 799 if m.Requires != nil { 800 for _, relationData := range m.Requires { 801 if relationData.Scope == ScopeContainer { 802 valid = true 803 break 804 } 805 } 806 } 807 if !valid { 808 return errors.Errorf("subordinate charm %q lacks \"requires\" relation with container scope", m.Name) 809 } 810 } 811 812 for _, series := range m.Series { 813 if !IsValidSeries(series) { 814 return errors.Errorf("charm %q declares invalid series: %q", m.Name, series) 815 } 816 } 817 818 names = make(map[string]bool) 819 for name, store := range m.Storage { 820 if store.Location != "" && store.Type != StorageFilesystem { 821 return errors.Errorf(`charm %q storage %q: location may not be specified for "type: %s"`, m.Name, name, store.Type) 822 } 823 if store.Type == "" { 824 return errors.Errorf("charm %q storage %q: type must be specified", m.Name, name) 825 } 826 if store.CountMin < 0 { 827 return errors.Errorf("charm %q storage %q: invalid minimum count %d", m.Name, name, store.CountMin) 828 } 829 if store.CountMax == 0 || store.CountMax < -1 { 830 return errors.Errorf("charm %q storage %q: invalid maximum count %d", m.Name, name, store.CountMax) 831 } 832 if names[name] { 833 return errors.Errorf("charm %q storage %q: duplicated storage name", m.Name, name) 834 } 835 names[name] = true 836 } 837 838 names = make(map[string]bool) 839 for name, device := range m.Devices { 840 if device.Type == "" { 841 return errors.Errorf("charm %q device %q: type must be specified", m.Name, name) 842 } 843 if device.CountMax >= 0 && device.CountMin >= 0 && device.CountMin > device.CountMax { 844 return errors.Errorf( 845 "charm %q device %q: maximum count %d can not be smaller than minimum count %d", 846 m.Name, name, device.CountMax, device.CountMin) 847 } 848 if names[name] { 849 return errors.Errorf("charm %q device %q: duplicated device name", m.Name, name) 850 } 851 names[name] = true 852 } 853 854 for name, payloadClass := range m.PayloadClasses { 855 if payloadClass.Name != name { 856 return errors.Errorf("mismatch on payload class name (%q != %q)", payloadClass.Name, name) 857 } 858 if err := payloadClass.Validate(); err != nil { 859 return err 860 } 861 } 862 863 if err := validateMetaResources(m.Resources); err != nil { 864 return err 865 } 866 867 for _, term := range m.Terms { 868 if _, terr := ParseTerm(term); terr != nil { 869 return errors.Trace(terr) 870 } 871 } 872 873 return nil 874 } 875 876 func (m Meta) checkV1(reasons []FormatSelectionReason) error { 877 if m.Assumes != nil { 878 return errors.NotValidf("assumes in metadata v1") 879 } 880 if len(m.Containers) != 0 { 881 if !hasReason(reasons, SelectionManifest) { 882 return errors.NotValidf("containers without a manifest.yaml") 883 } 884 return errors.NotValidf("containers in metadata v1") 885 } 886 return nil 887 } 888 889 func (m Meta) checkV2(reasons []FormatSelectionReason) error { 890 if len(reasons) == 0 { 891 return errors.NotValidf("metadata v2 without manifest.yaml") 892 } 893 if len(m.Series) != 0 { 894 if hasReason(reasons, SelectionManifest) { 895 return errors.NotValidf("metadata v2 manifest.yaml with series slice") 896 } 897 return errors.NotValidf("series slice in metadata v2") 898 } 899 if m.MinJujuVersion != version.Zero { 900 return errors.NotValidf("min-juju-version in metadata v2") 901 } 902 if m.Deployment != nil { 903 return errors.NotValidf("deployment in metadata v2") 904 } 905 return nil 906 } 907 908 func hasReason(reasons []FormatSelectionReason, reason FormatSelectionReason) bool { 909 return set.NewStrings(reasons...).Contains(reason) 910 } 911 912 func reservedName(charmName, endpointName string) (reserved bool, reason string) { 913 if strings.HasPrefix(charmName, "juju-") { 914 return false, "" 915 } 916 if endpointName == "juju" { 917 return true, `"juju" is a reserved name` 918 } 919 if strings.HasPrefix(endpointName, "juju-") { 920 return true, `the "juju-" prefix is reserved` 921 } 922 return false, "" 923 } 924 925 func parseRelations(relations interface{}, role RelationRole) map[string]Relation { 926 if relations == nil { 927 return nil 928 } 929 result := make(map[string]Relation) 930 for name, rel := range relations.(map[string]interface{}) { 931 relMap := rel.(map[string]interface{}) 932 relation := Relation{ 933 Name: name, 934 Role: role, 935 Interface: relMap["interface"].(string), 936 Optional: relMap["optional"].(bool), 937 } 938 if scope := relMap["scope"]; scope != nil { 939 relation.Scope = RelationScope(scope.(string)) 940 } 941 if relMap["limit"] != nil { 942 // Schema defaults to int64, but we know 943 // the int range should be more than enough. 944 relation.Limit = int(relMap["limit"].(int64)) 945 } 946 result[name] = relation 947 } 948 return result 949 } 950 951 // CombinedRelations returns all defined relations, regardless of their type in 952 // a single map. 953 func (m Meta) CombinedRelations() map[string]Relation { 954 combined := make(map[string]Relation) 955 for name, relation := range m.Provides { 956 combined[name] = relation 957 } 958 for name, relation := range m.Requires { 959 combined[name] = relation 960 } 961 for name, relation := range m.Peers { 962 combined[name] = relation 963 } 964 return combined 965 } 966 967 // Schema coercer that expands the interface shorthand notation. 968 // A consistent format is easier to work with than considering the 969 // potential difference everywhere. 970 // 971 // Supports the following variants:: 972 // 973 // provides: 974 // server: riak 975 // admin: http 976 // foobar: 977 // interface: blah 978 // 979 // provides: 980 // server: 981 // interface: mysql 982 // limit: 983 // optional: false 984 // 985 // In all input cases, the output is the fully specified interface 986 // representation as seen in the mysql interface description above. 987 func ifaceExpander(limit interface{}) schema.Checker { 988 return ifaceExpC{limit} 989 } 990 991 type ifaceExpC struct { 992 limit interface{} 993 } 994 995 var ( 996 stringC = schema.String() 997 mapC = schema.StringMap(schema.Any()) 998 ) 999 1000 func (c ifaceExpC) Coerce(v interface{}, path []string) (newv interface{}, err error) { 1001 s, err := stringC.Coerce(v, path) 1002 if err == nil { 1003 newv = map[string]interface{}{ 1004 "interface": s, 1005 "limit": c.limit, 1006 "optional": false, 1007 "scope": string(ScopeGlobal), 1008 } 1009 return 1010 } 1011 1012 v, err = mapC.Coerce(v, path) 1013 if err != nil { 1014 return 1015 } 1016 m := v.(map[string]interface{}) 1017 if _, ok := m["limit"]; !ok { 1018 m["limit"] = c.limit 1019 } 1020 return ifaceSchema.Coerce(m, path) 1021 } 1022 1023 var ifaceSchema = schema.FieldMap( 1024 schema.Fields{ 1025 "interface": schema.String(), 1026 "limit": schema.OneOf(schema.Const(nil), schema.Int()), 1027 "scope": schema.OneOf(schema.Const(string(ScopeGlobal)), schema.Const(string(ScopeContainer))), 1028 "optional": schema.Bool(), 1029 }, 1030 schema.Defaults{ 1031 "scope": string(ScopeGlobal), 1032 "optional": false, 1033 }, 1034 ) 1035 1036 func parseStorage(stores interface{}) map[string]Storage { 1037 if stores == nil { 1038 return nil 1039 } 1040 result := make(map[string]Storage) 1041 for name, store := range stores.(map[string]interface{}) { 1042 storeMap := store.(map[string]interface{}) 1043 store := Storage{ 1044 Name: name, 1045 Type: StorageType(storeMap["type"].(string)), 1046 Shared: storeMap["shared"].(bool), 1047 ReadOnly: storeMap["read-only"].(bool), 1048 CountMin: 1, 1049 CountMax: 1, 1050 } 1051 if desc, ok := storeMap["description"].(string); ok { 1052 store.Description = desc 1053 } 1054 if multiple, ok := storeMap["multiple"].(map[string]interface{}); ok { 1055 if r, ok := multiple["range"].([2]int); ok { 1056 store.CountMin, store.CountMax = r[0], r[1] 1057 } 1058 } 1059 if minSize, ok := storeMap["minimum-size"].(uint64); ok { 1060 store.MinimumSize = minSize 1061 } 1062 if loc, ok := storeMap["location"].(string); ok { 1063 store.Location = loc 1064 } 1065 if properties, ok := storeMap["properties"].([]interface{}); ok { 1066 for _, p := range properties { 1067 store.Properties = append(store.Properties, p.(string)) 1068 } 1069 } 1070 result[name] = store 1071 } 1072 return result 1073 } 1074 1075 func parseDevices(devices interface{}) map[string]Device { 1076 if devices == nil { 1077 return nil 1078 } 1079 result := make(map[string]Device) 1080 for name, device := range devices.(map[string]interface{}) { 1081 deviceMap := device.(map[string]interface{}) 1082 device := Device{ 1083 Name: name, 1084 Type: DeviceType(deviceMap["type"].(string)), 1085 CountMin: 1, 1086 CountMax: 1, 1087 } 1088 if desc, ok := deviceMap["description"].(string); ok { 1089 device.Description = desc 1090 } 1091 if countmin, ok := deviceMap["countmin"].(int64); ok { 1092 device.CountMin = countmin 1093 } 1094 if countmax, ok := deviceMap["countmax"].(int64); ok { 1095 device.CountMax = countmax 1096 } 1097 result[name] = device 1098 } 1099 return result 1100 } 1101 1102 func parseDeployment(deployment interface{}, charmSeries []string, storage map[string]Storage) (*Deployment, error) { 1103 if deployment == nil { 1104 return nil, nil 1105 } 1106 if len(charmSeries) == 0 { 1107 return nil, errors.New("charm with deployment metadata must declare at least one series") 1108 } 1109 if charmSeries[0] != kubernetes { 1110 return nil, errors.Errorf("charms with deployment metadata only supported for %q", kubernetes) 1111 } 1112 deploymentMap := deployment.(map[string]interface{}) 1113 var result Deployment 1114 if deploymentType, ok := deploymentMap["type"].(string); ok { 1115 result.DeploymentType = DeploymentType(deploymentType) 1116 } 1117 if deploymentMode, ok := deploymentMap["mode"].(string); ok { 1118 result.DeploymentMode = DeploymentMode(deploymentMode) 1119 } 1120 if serviceType, ok := deploymentMap["service"].(string); ok { 1121 result.ServiceType = ServiceType(serviceType) 1122 } 1123 if minVersion, ok := deploymentMap["min-version"].(string); ok { 1124 result.MinVersion = minVersion 1125 } 1126 if result.ServiceType != "" { 1127 osForSeries, err := series.GetOSFromSeries(charmSeries[0]) 1128 if err != nil { 1129 return nil, errors.NotValidf("series %q", charmSeries[0]) 1130 } 1131 valid := false 1132 allowed := validServiceTypes[osForSeries] 1133 for _, st := range allowed { 1134 if st == result.ServiceType { 1135 valid = true 1136 break 1137 } 1138 } 1139 if !valid { 1140 return nil, errors.NotValidf("service type %q for OS %q", result.ServiceType, osForSeries) 1141 } 1142 } 1143 return &result, nil 1144 } 1145 1146 func parseContainers(input interface{}, resources map[string]resource.Meta, storage map[string]Storage) (map[string]Container, error) { 1147 var err error 1148 if input == nil { 1149 return nil, nil 1150 } 1151 containers := map[string]Container{} 1152 for name, v := range input.(map[string]interface{}) { 1153 containerMap := v.(map[string]interface{}) 1154 container := Container{} 1155 1156 if value, ok := containerMap["resource"]; ok { 1157 container.Resource = value.(string) 1158 } 1159 if container.Resource != "" { 1160 if r, ok := resources[container.Resource]; !ok { 1161 return nil, errors.NotFoundf("referenced resource %q", container.Resource) 1162 } else if r.Type != resource.TypeContainerImage { 1163 return nil, errors.Errorf("referenced resource %q is not a %s", 1164 container.Resource, 1165 resource.TypeContainerImage.String()) 1166 } 1167 } 1168 1169 container.Mounts, err = parseMounts(containerMap["mounts"], storage) 1170 if err != nil { 1171 return nil, errors.Annotatef(err, "container %q", name) 1172 } 1173 containers[name] = container 1174 } 1175 if len(containers) == 0 { 1176 return nil, nil 1177 } 1178 return containers, nil 1179 } 1180 1181 func parseMounts(input interface{}, storage map[string]Storage) ([]Mount, error) { 1182 if input == nil { 1183 return nil, nil 1184 } 1185 mounts := []Mount(nil) 1186 for _, v := range input.([]interface{}) { 1187 mount := Mount{} 1188 mountMap := v.(map[string]interface{}) 1189 if value, ok := mountMap["storage"].(string); ok { 1190 mount.Storage = value 1191 } 1192 if value, ok := mountMap["location"].(string); ok { 1193 mount.Location = value 1194 } 1195 if mount.Storage == "" { 1196 return nil, errors.Errorf("storage must be specifed on mount") 1197 } 1198 if mount.Location == "" { 1199 return nil, errors.Errorf("location must be specifed on mount") 1200 } 1201 if _, ok := storage[mount.Storage]; !ok { 1202 return nil, errors.NotValidf("storage %q", mount.Storage) 1203 } 1204 mounts = append(mounts, mount) 1205 } 1206 return mounts, nil 1207 } 1208 1209 var storageSchema = schema.FieldMap( 1210 schema.Fields{ 1211 "type": schema.OneOf(schema.Const(string(StorageBlock)), schema.Const(string(StorageFilesystem))), 1212 "shared": schema.Bool(), 1213 "read-only": schema.Bool(), 1214 "multiple": schema.FieldMap( 1215 schema.Fields{ 1216 "range": storageCountC{}, // m, m-n, m+, m- 1217 }, 1218 schema.Defaults{}, 1219 ), 1220 "minimum-size": storageSizeC{}, 1221 "location": schema.String(), 1222 "description": schema.String(), 1223 "properties": schema.List(propertiesC{}), 1224 }, 1225 schema.Defaults{ 1226 "shared": false, 1227 "read-only": false, 1228 "multiple": schema.Omit, 1229 "location": schema.Omit, 1230 "description": schema.Omit, 1231 "properties": schema.Omit, 1232 "minimum-size": schema.Omit, 1233 }, 1234 ) 1235 1236 var deviceSchema = schema.FieldMap( 1237 schema.Fields{ 1238 "description": schema.String(), 1239 "type": schema.String(), 1240 "countmin": deviceCountC{}, 1241 "countmax": deviceCountC{}, 1242 }, schema.Defaults{ 1243 "description": schema.Omit, 1244 "countmin": schema.Omit, 1245 "countmax": schema.Omit, 1246 }, 1247 ) 1248 1249 type deviceCountC struct{} 1250 1251 func (c deviceCountC) Coerce(v interface{}, path []string) (interface{}, error) { 1252 s, err := schema.Int().Coerce(v, path) 1253 if err != nil { 1254 return 0, err 1255 } 1256 if m, ok := s.(int64); ok { 1257 if m >= 0 { 1258 return m, nil 1259 } 1260 } 1261 return 0, errors.Errorf("invalid device count %d", s) 1262 } 1263 1264 type storageCountC struct{} 1265 1266 var storageCountRE = regexp.MustCompile("^([0-9]+)([-+]|-[0-9]+)$") 1267 1268 func (c storageCountC) Coerce(v interface{}, path []string) (newv interface{}, err error) { 1269 s, err := schema.OneOf(schema.Int(), stringC).Coerce(v, path) 1270 if err != nil { 1271 return nil, err 1272 } 1273 if m, ok := s.(int64); ok { 1274 // We've got a count of the form "m": m represents 1275 // both the minimum and maximum. 1276 if m <= 0 { 1277 return nil, errors.Errorf("%s: invalid count %v", strings.Join(path[1:], ""), m) 1278 } 1279 return [2]int{int(m), int(m)}, nil 1280 } 1281 match := storageCountRE.FindStringSubmatch(s.(string)) 1282 if match == nil { 1283 return nil, errors.Errorf("%s: value %q does not match 'm', 'm-n', or 'm+'", strings.Join(path[1:], ""), s) 1284 } 1285 var m, n int 1286 if m, err = strconv.Atoi(match[1]); err != nil { 1287 return nil, err 1288 } 1289 if len(match[2]) == 1 { 1290 // We've got a count of the form "m+" or "m-": 1291 // m represents the minimum, and there is no 1292 // upper bound. 1293 n = -1 1294 } else { 1295 if n, err = strconv.Atoi(match[2][1:]); err != nil { 1296 return nil, err 1297 } 1298 } 1299 return [2]int{m, n}, nil 1300 } 1301 1302 type storageSizeC struct{} 1303 1304 func (c storageSizeC) Coerce(v interface{}, path []string) (newv interface{}, err error) { 1305 s, err := schema.String().Coerce(v, path) 1306 if err != nil { 1307 return nil, err 1308 } 1309 return utils.ParseSize(s.(string)) 1310 } 1311 1312 type propertiesC struct{} 1313 1314 func (c propertiesC) Coerce(v interface{}, path []string) (newv interface{}, err error) { 1315 return schema.OneOf(schema.Const("transient")).Coerce(v, path) 1316 } 1317 1318 var deploymentSchema = schema.FieldMap( 1319 schema.Fields{ 1320 "type": schema.OneOf( 1321 schema.Const(string(DeploymentStateful)), 1322 schema.Const(string(DeploymentStateless)), 1323 schema.Const(string(DeploymentDaemon)), 1324 ), 1325 "mode": schema.OneOf( 1326 schema.Const(string(ModeOperator)), 1327 schema.Const(string(ModeWorkload)), 1328 ), 1329 "service": schema.OneOf( 1330 schema.Const(string(ServiceCluster)), 1331 schema.Const(string(ServiceLoadBalancer)), 1332 schema.Const(string(ServiceExternal)), 1333 schema.Const(string(ServiceOmit)), 1334 ), 1335 "min-version": schema.String(), 1336 }, schema.Defaults{ 1337 "type": schema.Omit, 1338 "mode": string(ModeWorkload), 1339 "service": schema.Omit, 1340 "min-version": schema.Omit, 1341 }, 1342 ) 1343 1344 var containerSchema = schema.FieldMap( 1345 schema.Fields{ 1346 "resource": schema.String(), 1347 "mounts": schema.List(mountSchema), 1348 }, schema.Defaults{ 1349 "resource": schema.Omit, 1350 "mounts": schema.Omit, 1351 }) 1352 1353 var mountSchema = schema.FieldMap( 1354 schema.Fields{ 1355 "storage": schema.String(), 1356 "location": schema.String(), 1357 }, schema.Defaults{ 1358 "storage": schema.Omit, 1359 "location": schema.Omit, 1360 }) 1361 1362 var charmSchema = schema.FieldMap( 1363 schema.Fields{ 1364 "name": schema.String(), 1365 "summary": schema.String(), 1366 "description": schema.String(), 1367 "peers": schema.StringMap(ifaceExpander(nil)), 1368 "provides": schema.StringMap(ifaceExpander(nil)), 1369 "requires": schema.StringMap(ifaceExpander(nil)), 1370 "extra-bindings": extraBindingsSchema, 1371 "revision": schema.Int(), // Obsolete 1372 "format": schema.Int(), // Obsolete 1373 "subordinate": schema.Bool(), 1374 "categories": schema.List(schema.String()), 1375 "tags": schema.List(schema.String()), 1376 "series": schema.List(schema.String()), 1377 "storage": schema.StringMap(storageSchema), 1378 "devices": schema.StringMap(deviceSchema), 1379 "deployment": deploymentSchema, 1380 "payloads": schema.StringMap(payloadClassSchema), 1381 "resources": schema.StringMap(resourceSchema), 1382 "terms": schema.List(schema.String()), 1383 "min-juju-version": schema.String(), 1384 "assumes": schema.List(schema.Any()), 1385 "containers": schema.StringMap(containerSchema), 1386 }, 1387 schema.Defaults{ 1388 "provides": schema.Omit, 1389 "requires": schema.Omit, 1390 "peers": schema.Omit, 1391 "extra-bindings": schema.Omit, 1392 "revision": schema.Omit, 1393 "format": schema.Omit, 1394 "subordinate": schema.Omit, 1395 "categories": schema.Omit, 1396 "tags": schema.Omit, 1397 "series": schema.Omit, 1398 "storage": schema.Omit, 1399 "devices": schema.Omit, 1400 "deployment": schema.Omit, 1401 "payloads": schema.Omit, 1402 "resources": schema.Omit, 1403 "terms": schema.Omit, 1404 "min-juju-version": schema.Omit, 1405 "assumes": schema.Omit, 1406 "containers": schema.Omit, 1407 }, 1408 ) 1409 1410 // ensureUnambiguousFormat returns an error if the raw data contains 1411 // both metadata v1 and v2 contents. However is it unable to definitively 1412 // determine which format the charm is as metadata does not contain bases. 1413 func ensureUnambiguousFormat(raw map[interface{}]interface{}) error { 1414 format := FormatUnknown 1415 matched := []string(nil) 1416 mismatched := []string(nil) 1417 keys := []string(nil) 1418 for k, _ := range raw { 1419 key, ok := k.(string) 1420 if !ok { 1421 // Non-string keys will be an error handled by the schema lib. 1422 continue 1423 } 1424 keys = append(keys, key) 1425 } 1426 sort.Strings(keys) 1427 for _, key := range keys { 1428 detected := FormatUnknown 1429 switch key { 1430 case "containers", "assumes": 1431 detected = FormatV2 1432 case "series", "deployment", "min-juju-version": 1433 detected = FormatV1 1434 } 1435 if detected == FormatUnknown { 1436 continue 1437 } 1438 if format == FormatUnknown { 1439 format = detected 1440 } 1441 if format == detected { 1442 matched = append(matched, key) 1443 } else { 1444 mismatched = append(mismatched, key) 1445 } 1446 } 1447 if mismatched != nil { 1448 return errors.Errorf("ambiguous metadata: keys %s cannot be used with %s", 1449 `"`+strings.Join(mismatched, `", "`)+`"`, 1450 `"`+strings.Join(matched, `", "`)+`"`) 1451 } 1452 return nil 1453 }