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  }