Permalink
Fetching contributors…
Cannot retrieve contributors at this time
700 lines (642 sloc) 20.3 KB
// Copyright 2011, 2012, 2013 Canonical Ltd.
// Licensed under the LGPLv3, see LICENCE file for details.
package charm
import (
"errors"
"fmt"
"io"
"io/ioutil"
"regexp"
"strconv"
"strings"
"github.com/juju/schema"
"github.com/juju/utils"
"gopkg.in/yaml.v1"
"gopkg.in/juju/charm.v5/hooks"
)
// RelationScope describes the scope of a relation.
type RelationScope string
// Note that schema doesn't support custom string types,
// so when we use these values in a schema.Checker,
// we must store them as strings, not RelationScopes.
const (
ScopeGlobal RelationScope = "global"
ScopeContainer RelationScope = "container"
)
// RelationRole defines the role of a relation.
type RelationRole string
const (
RoleProvider RelationRole = "provider"
RoleRequirer RelationRole = "requirer"
RolePeer RelationRole = "peer"
)
// StorageType defines a storage type.
type StorageType string
const (
StorageBlock StorageType = "block"
StorageFilesystem StorageType = "filesystem"
)
// Storage represents a charm's storage requirement.
type Storage struct {
// Name is the name of the store.
//
// Name has no default, and must be specified.
Name string `bson:"name"`
// Description is a description of the store.
//
// Description has no default, and is optional.
Description string `bson:"description"`
// Type is the storage type: filesystem or block-device.
//
// Type has no default, and must be specified.
Type StorageType `bson:"type"`
// Shared indicates that the storage is shared between all units of
// a service deployed from the charm. It is an error to attempt to
// assign non-shareable storage to a "shared" storage requirement.
//
// Shared defaults to false.
Shared bool `bson:"shared"`
// ReadOnly indicates that the storage should be made read-only if
// possible. If the storage cannot be made read-only, Juju will warn
// the user.
//
// ReadOnly defaults to false.
ReadOnly bool `bson:"read-only"`
// CountMin is the number of storage instances that must be attached
// to the charm for it to be useful; the charm will not install until
// this number has been satisfied. This must be a non-negative number.
//
// CountMin defaults to 1 for singleton stores.
CountMin int `bson:"countmin"`
// CountMax is the largest number of storage instances that can be
// attached to the charm. If CountMax is -1, then there is no upper
// bound.
//
// CountMax defaults to 1 for singleton stores.
CountMax int `bson:"countmax"`
// MinimumSize is the minimum size of store that the charm needs to
// work at all. This is not a recommended size or a comfortable size
// or a will-work-well size, just a bare minimum below which the charm
// is going to break.
// MinimumSize requires a unit, one of MGTPEZY, and is stored as MiB.
//
// There is no default MinimumSize; if left unspecified, a provider
// specific default will be used, typically 1GB for block storage.
MinimumSize uint64 `bson:"minimum-size"`
// Location is the mount location for filesystem stores. For multi-
// stores, the location acts as the parent directory for each mounted
// store.
//
// Location has no default, and is optional.
Location string `bson:"location,omitempty"`
// Properties allow the charm author to characterise the relative storage
// performance requirements and sensitivities for each store.
// eg “transient” is used to indicate that non persistent storage is acceptable,
// such as tmpfs or ephemeral instance disks.
//
// Properties has no default, and is optional.
Properties []string `bson:"properties,omitempty"`
}
// Relation represents a single relation defined in the charm
// metadata.yaml file.
type Relation struct {
Name string `bson:"name"`
Role RelationRole `bson:"role"`
Interface string `bson:"interface"`
Optional bool `bson:"optional"`
Limit int `bson:"limit"`
Scope RelationScope `bson:"scope"`
}
// ImplementedBy returns whether the relation is implemented by the supplied charm.
func (r Relation) ImplementedBy(ch Charm) bool {
if r.IsImplicit() {
return true
}
var m map[string]Relation
switch r.Role {
case RoleProvider:
m = ch.Meta().Provides
case RoleRequirer:
m = ch.Meta().Requires
case RolePeer:
m = ch.Meta().Peers
default:
panic(fmt.Errorf("unknown relation role %q", r.Role))
}
rel, found := m[r.Name]
if !found {
return false
}
if rel.Interface == r.Interface {
switch r.Scope {
case ScopeGlobal:
return rel.Scope != ScopeContainer
case ScopeContainer:
return true
default:
panic(fmt.Errorf("unknown relation scope %q", r.Scope))
}
}
return false
}
// IsImplicit returns whether the relation is supplied by juju itself,
// rather than by a charm.
func (r Relation) IsImplicit() bool {
return (r.Name == "juju-info" &&
r.Interface == "juju-info" &&
r.Role == RoleProvider)
}
// Meta represents all the known content that may be defined
// within a charm's metadata.yaml file.
type Meta struct {
Name string `bson:"name"`
Summary string `bson:"summary"`
Description string `bson:"description"`
Subordinate bool `bson:"subordinate"`
Provides map[string]Relation `bson:"provides,omitempty"`
Requires map[string]Relation `bson:"requires,omitempty"`
Peers map[string]Relation `bson:"peers,omitempty"`
Format int `bson:"format,omitempty"`
OldRevision int `bson:"oldrevision,omitempty"` // Obsolete
Categories []string `bson:"categories,omitempty"`
Tags []string `bson:"tags,omitempty"`
Series string `bson:"series,omitempty"`
Storage map[string]Storage `bson:"storage,omitempty"`
PayloadClasses map[string]PayloadClass `bson:"payloadclasses,omitempty" json:"payloadclasses,omitempty"`
}
func generateRelationHooks(relName string, allHooks map[string]bool) {
for _, hookName := range hooks.RelationHooks() {
allHooks[fmt.Sprintf("%s-%s", relName, hookName)] = true
}
}
// Hooks returns a map of all possible valid hooks, taking relations
// into account. It's a map to enable fast lookups, and the value is
// always true.
func (m Meta) Hooks() map[string]bool {
allHooks := make(map[string]bool)
// Unit hooks
for _, hookName := range hooks.UnitHooks() {
allHooks[string(hookName)] = true
}
// Relation hooks
for hookName := range m.Provides {
generateRelationHooks(hookName, allHooks)
}
for hookName := range m.Requires {
generateRelationHooks(hookName, allHooks)
}
for hookName := range m.Peers {
generateRelationHooks(hookName, allHooks)
}
return allHooks
}
// Used for parsing Categories and Tags.
func parseStringList(list interface{}) []string {
if list == nil {
return nil
}
slice := list.([]interface{})
result := make([]string, 0, len(slice))
for _, elem := range slice {
result = append(result, elem.(string))
}
return result
}
// ReadMeta reads the content of a metadata.yaml file and returns
// its representation.
func ReadMeta(r io.Reader) (meta *Meta, err error) {
data, err := ioutil.ReadAll(r)
if err != nil {
return
}
raw := make(map[interface{}]interface{})
err = yaml.Unmarshal(data, raw)
if err != nil {
return
}
v, err := charmSchema.Coerce(raw, nil)
if err != nil {
return nil, errors.New("metadata: " + err.Error())
}
m := v.(map[string]interface{})
meta = &Meta{}
meta.Name = m["name"].(string)
// Schema decodes as int64, but the int range should be good
// enough for revisions.
meta.Summary = m["summary"].(string)
meta.Description = m["description"].(string)
meta.Provides = parseRelations(m["provides"], RoleProvider)
meta.Requires = parseRelations(m["requires"], RoleRequirer)
meta.Peers = parseRelations(m["peers"], RolePeer)
meta.Format = int(m["format"].(int64))
meta.Categories = parseStringList(m["categories"])
meta.Tags = parseStringList(m["tags"])
if subordinate := m["subordinate"]; subordinate != nil {
meta.Subordinate = subordinate.(bool)
}
if rev := m["revision"]; rev != nil {
// Obsolete
meta.OldRevision = int(m["revision"].(int64))
}
if series, ok := m["series"]; ok && series != nil {
multiseries, ok := series.([]interface{})
if ok {
if len(multiseries) > 0 {
meta.Series = multiseries[0].(string)
}
} else {
meta.Series = series.(string)
}
}
meta.Storage = parseStorage(m["storage"])
meta.PayloadClasses = parsePayloadClasses(m["payloads"])
if err := meta.Check(); err != nil {
return nil, err
}
return meta, nil
}
// GetYAML implements yaml.Getter.GetYAML.
func (m Meta) GetYAML() (tag string, value interface{}) {
marshaledRelations := func(rs map[string]Relation) map[string]marshaledRelation {
mrs := make(map[string]marshaledRelation)
for name, r := range rs {
mrs[name] = marshaledRelation(r)
}
return mrs
}
return "", struct {
Name string `yaml:"name"`
Summary string `yaml:"summary"`
Description string `yaml:"description"`
Provides map[string]marshaledRelation `yaml:"provides,omitempty"`
Requires map[string]marshaledRelation `yaml:"requires,omitempty"`
Peers map[string]marshaledRelation `yaml:"peers,omitempty"`
Categories []string `yaml:"categories,omitempty"`
Tags []string `yaml:"tags,omitempty"`
Subordinate bool `yaml:"subordinate,omitempty"`
Series string `yaml:"series,omitempty"`
}{
Name: m.Name,
Summary: m.Summary,
Description: m.Description,
Provides: marshaledRelations(m.Provides),
Requires: marshaledRelations(m.Requires),
Peers: marshaledRelations(m.Peers),
Categories: m.Categories,
Tags: m.Tags,
Subordinate: m.Subordinate,
Series: m.Series,
}
}
type marshaledRelation Relation
func (r marshaledRelation) GetYAML() (tag string, value interface{}) {
// See calls to ifaceExpander in charmSchema.
noLimit := 1
if r.Role == RoleProvider {
noLimit = 0
}
if !r.Optional && r.Limit == noLimit && r.Scope == ScopeGlobal {
// All attributes are default, so use the simple string form of the relation.
return "", r.Interface
}
mr := struct {
Interface string `yaml:"interface"`
Limit *int `yaml:"limit,omitempty"`
Optional bool `yaml:"optional,omitempty"`
Scope RelationScope `yaml:"scope,omitempty"`
}{
Interface: r.Interface,
Optional: r.Optional,
}
if r.Limit != noLimit {
mr.Limit = &r.Limit
}
if r.Scope != ScopeGlobal {
mr.Scope = r.Scope
}
return "", mr
}
// Check checks that the metadata is well-formed.
func (meta Meta) Check() error {
// Check for duplicate or forbidden relation names or interfaces.
names := map[string]bool{}
checkRelations := func(src map[string]Relation, role RelationRole) error {
for name, rel := range src {
if rel.Name != name {
return fmt.Errorf("charm %q has mismatched relation name %q; expected %q", meta.Name, rel.Name, name)
}
if rel.Role != role {
return fmt.Errorf("charm %q has mismatched role %q; expected %q", meta.Name, rel.Role, role)
}
// Container-scoped require relations on subordinates are allowed
// to use the otherwise-reserved juju-* namespace.
if !meta.Subordinate || role != RoleRequirer || rel.Scope != ScopeContainer {
if reservedName(name) {
return fmt.Errorf("charm %q using a reserved relation name: %q", meta.Name, name)
}
}
if role != RoleRequirer {
if reservedName(rel.Interface) {
return fmt.Errorf("charm %q relation %q using a reserved interface: %q", meta.Name, name, rel.Interface)
}
}
if names[name] {
return fmt.Errorf("charm %q using a duplicated relation name: %q", meta.Name, name)
}
names[name] = true
}
return nil
}
if err := checkRelations(meta.Provides, RoleProvider); err != nil {
return err
}
if err := checkRelations(meta.Requires, RoleRequirer); err != nil {
return err
}
if err := checkRelations(meta.Peers, RolePeer); err != nil {
return err
}
// Subordinate charms must have at least one relation that
// has container scope, otherwise they can't relate to the
// principal.
if meta.Subordinate {
valid := false
if meta.Requires != nil {
for _, relationData := range meta.Requires {
if relationData.Scope == ScopeContainer {
valid = true
break
}
}
}
if !valid {
return fmt.Errorf("subordinate charm %q lacks \"requires\" relation with container scope", meta.Name)
}
}
if meta.Series != "" {
if !IsValidSeries(meta.Series) {
return fmt.Errorf("charm %q declares invalid series: %q", meta.Name, meta.Series)
}
}
names = make(map[string]bool)
for name, store := range meta.Storage {
if store.Location != "" && store.Type != StorageFilesystem {
return fmt.Errorf(`charm %q storage %q: location may not be specified for "type: %s"`, meta.Name, name, store.Type)
}
if store.Type == "" {
return fmt.Errorf("charm %q storage %q: type must be specified", meta.Name, name)
}
if store.CountMin < 0 {
return fmt.Errorf("charm %q storage %q: invalid minimum count %d", meta.Name, name, store.CountMin)
}
if store.CountMax == 0 || store.CountMax < -1 {
return fmt.Errorf("charm %q storage %q: invalid maximum count %d", meta.Name, name, store.CountMax)
}
if names[name] {
return fmt.Errorf("charm %q storage %q: duplicated storage name", meta.Name, name)
}
names[name] = true
}
for name, payloadClass := range meta.PayloadClasses {
if payloadClass.Name != name {
return fmt.Errorf("mismatch on payload class name (%q != %q)", payloadClass.Name, name)
}
if err := payloadClass.Validate(); err != nil {
return err
}
}
return nil
}
func reservedName(name string) bool {
return name == "juju" || strings.HasPrefix(name, "juju-")
}
func parseRelations(relations interface{}, role RelationRole) map[string]Relation {
if relations == nil {
return nil
}
result := make(map[string]Relation)
for name, rel := range relations.(map[string]interface{}) {
relMap := rel.(map[string]interface{})
relation := Relation{
Name: name,
Role: role,
Interface: relMap["interface"].(string),
Optional: relMap["optional"].(bool),
}
if scope := relMap["scope"]; scope != nil {
relation.Scope = RelationScope(scope.(string))
}
if relMap["limit"] != nil {
// Schema defaults to int64, but we know
// the int range should be more than enough.
relation.Limit = int(relMap["limit"].(int64))
}
result[name] = relation
}
return result
}
// Schema coercer that expands the interface shorthand notation.
// A consistent format is easier to work with than considering the
// potential difference everywhere.
//
// Supports the following variants::
//
// provides:
// server: riak
// admin: http
// foobar:
// interface: blah
//
// provides:
// server:
// interface: mysql
// limit:
// optional: false
//
// In all input cases, the output is the fully specified interface
// representation as seen in the mysql interface description above.
func ifaceExpander(limit interface{}) schema.Checker {
return ifaceExpC{limit}
}
type ifaceExpC struct {
limit interface{}
}
var (
stringC = schema.String()
mapC = schema.StringMap(schema.Any())
)
func (c ifaceExpC) Coerce(v interface{}, path []string) (newv interface{}, err error) {
s, err := stringC.Coerce(v, path)
if err == nil {
newv = map[string]interface{}{
"interface": s,
"limit": c.limit,
"optional": false,
"scope": string(ScopeGlobal),
}
return
}
v, err = mapC.Coerce(v, path)
if err != nil {
return
}
m := v.(map[string]interface{})
if _, ok := m["limit"]; !ok {
m["limit"] = c.limit
}
return ifaceSchema.Coerce(m, path)
}
var ifaceSchema = schema.FieldMap(
schema.Fields{
"interface": schema.String(),
"limit": schema.OneOf(schema.Const(nil), schema.Int()),
"scope": schema.OneOf(schema.Const(string(ScopeGlobal)), schema.Const(string(ScopeContainer))),
"optional": schema.Bool(),
},
schema.Defaults{
"scope": string(ScopeGlobal),
"optional": false,
},
)
func parseStorage(stores interface{}) map[string]Storage {
if stores == nil {
return nil
}
result := make(map[string]Storage)
for name, store := range stores.(map[string]interface{}) {
storeMap := store.(map[string]interface{})
store := Storage{
Name: name,
Type: StorageType(storeMap["type"].(string)),
Shared: storeMap["shared"].(bool),
ReadOnly: storeMap["read-only"].(bool),
CountMin: 1,
CountMax: 1,
}
if desc, ok := storeMap["description"].(string); ok {
store.Description = desc
}
if multiple, ok := storeMap["multiple"].(map[string]interface{}); ok {
if r, ok := multiple["range"].([2]int); ok {
store.CountMin, store.CountMax = r[0], r[1]
}
}
if minSize, ok := storeMap["minimum-size"].(uint64); ok {
store.MinimumSize = minSize
}
if loc, ok := storeMap["location"].(string); ok {
store.Location = loc
}
if properties, ok := storeMap["properties"].([]interface{}); ok {
for _, p := range properties {
store.Properties = append(store.Properties, p.(string))
}
}
result[name] = store
}
return result
}
var storageSchema = schema.FieldMap(
schema.Fields{
"type": schema.OneOf(schema.Const(string(StorageBlock)), schema.Const(string(StorageFilesystem))),
"shared": schema.Bool(),
"read-only": schema.Bool(),
"multiple": schema.FieldMap(
schema.Fields{
"range": storageCountC{}, // m, m-n, m+, m-
},
schema.Defaults{},
),
"minimum-size": storageSizeC{},
"location": schema.String(),
"description": schema.String(),
"properties": schema.List(propertiesC{}),
},
schema.Defaults{
"shared": false,
"read-only": false,
"multiple": schema.Omit,
"location": schema.Omit,
"description": schema.Omit,
"properties": schema.Omit,
"minimum-size": schema.Omit,
},
)
type storageCountC struct{}
var storageCountRE = regexp.MustCompile("^([0-9]+)([-+]|-[0-9]+)$")
func (c storageCountC) Coerce(v interface{}, path []string) (newv interface{}, err error) {
s, err := schema.OneOf(schema.Int(), stringC).Coerce(v, path)
if err != nil {
return nil, err
}
if m, ok := s.(int64); ok {
// We've got a count of the form "m": m represents
// both the minimum and maximum.
if m <= 0 {
return nil, fmt.Errorf("%s: invalid count %v", strings.Join(path[1:], ""), m)
}
return [2]int{int(m), int(m)}, nil
}
match := storageCountRE.FindStringSubmatch(s.(string))
if match == nil {
return nil, fmt.Errorf("%s: value %q does not match 'm', 'm-n', or 'm+'", strings.Join(path[1:], ""), s)
}
var m, n int
if m, err = strconv.Atoi(match[1]); err != nil {
return nil, err
}
if len(match[2]) == 1 {
// We've got a count of the form "m+" or "m-":
// m represents the minimum, and there is no
// upper bound.
n = -1
} else {
if n, err = strconv.Atoi(match[2][1:]); err != nil {
return nil, err
}
}
return [2]int{m, n}, nil
}
type storageSizeC struct{}
func (c storageSizeC) Coerce(v interface{}, path []string) (newv interface{}, err error) {
s, err := schema.String().Coerce(v, path)
if err != nil {
return nil, err
}
return utils.ParseSize(s.(string))
}
type propertiesC struct{}
func (c propertiesC) Coerce(v interface{}, path []string) (newv interface{}, err error) {
return schema.OneOf(schema.Const("transient")).Coerce(v, path)
}
var charmSchema = schema.FieldMap(
schema.Fields{
"name": schema.String(),
"summary": schema.String(),
"description": schema.String(),
"peers": schema.StringMap(ifaceExpander(int64(1))),
"provides": schema.StringMap(ifaceExpander(nil)),
"requires": schema.StringMap(ifaceExpander(int64(1))),
"revision": schema.Int(), // Obsolete
"format": schema.Int(),
"subordinate": schema.Bool(),
"categories": schema.List(schema.String()),
"tags": schema.List(schema.String()),
"series": schema.OneOf(schema.String(), schema.List(schema.String())),
"storage": schema.StringMap(storageSchema),
"payloads": schema.StringMap(payloadClassSchema),
},
schema.Defaults{
"provides": schema.Omit,
"requires": schema.Omit,
"peers": schema.Omit,
"revision": schema.Omit,
"format": 1,
"subordinate": schema.Omit,
"categories": schema.Omit,
"tags": schema.Omit,
"series": schema.Omit,
"storage": schema.Omit,
"payloads": schema.Omit,
},
)