Permalink
Cannot retrieve contributors at this time
Fetching contributors…
| // Copyright 2014 Canonical Ltd. | |
| // Licensed under the LGPLv3, see LICENCE file for details. | |
| package charm | |
| import ( | |
| "encoding/json" | |
| "fmt" | |
| "io" | |
| "io/ioutil" | |
| "os" | |
| "path/filepath" | |
| "regexp" | |
| "sort" | |
| "strconv" | |
| "strings" | |
| "gopkg.in/juju/names.v2" | |
| "gopkg.in/mgo.v2/bson" | |
| "gopkg.in/yaml.v2" | |
| ) | |
| type noMethodsBundleData BundleData | |
| type legacyBundleData struct { | |
| noMethodsBundleData `bson:",inline" yaml:",inline" json:",inline"` | |
| // LegacyServices holds application entries for older bundle files | |
| // that have not been migrated to use the new "application" terminology. | |
| LegacyServices map[string]*ApplicationSpec `json:"services" yaml:"services" bson:"services"` | |
| } | |
| func (lbd *legacyBundleData) setBundleData(bd *BundleData) error { | |
| if len(lbd.Applications) > 0 && len(lbd.LegacyServices) > 0 { | |
| return fmt.Errorf("cannot specify both applications and services") | |
| } | |
| if len(lbd.LegacyServices) > 0 { | |
| // We account for the fact that the YAML may contain a legacy entry | |
| // for "services" instead of "applications". | |
| lbd.Applications = lbd.LegacyServices | |
| lbd.unmarshaledWithServices = true | |
| } | |
| *bd = BundleData(lbd.noMethodsBundleData) | |
| return nil | |
| } | |
| // UnmarshalJSON implements the json.Unmarshaler interface. | |
| func (bd *BundleData) UnmarshalJSON(b []byte) error { | |
| var bdc legacyBundleData | |
| if err := json.Unmarshal(b, &bdc); err != nil { | |
| return err | |
| } | |
| return bdc.setBundleData(bd) | |
| } | |
| // UnmarshalYAML implements the yaml.Unmarshaler interface. | |
| func (bd *BundleData) UnmarshalYAML(f func(interface{}) error) error { | |
| var bdc legacyBundleData | |
| if err := f(&bdc); err != nil { | |
| return err | |
| } | |
| return bdc.setBundleData(bd) | |
| } | |
| // SetBSON implements the bson.Setter interface. | |
| func (bd *BundleData) SetBSON(raw bson.Raw) error { | |
| // TODO(wallyworld) - bson deserialisation is not handling the inline directive, | |
| // so we need to unmarshal the bundle data manually. | |
| var b *noMethodsBundleData | |
| if err := raw.Unmarshal(&b); err != nil { | |
| return err | |
| } | |
| if b == nil { | |
| return bson.SetZero | |
| } | |
| var bdc legacyBundleData | |
| if err := raw.Unmarshal(&bdc); err != nil { | |
| return err | |
| } | |
| // As per the above TODO, we manually set the inline data. | |
| bdc.noMethodsBundleData = *b | |
| return bdc.setBundleData(bd) | |
| } | |
| // BundleData holds the contents of the bundle. | |
| type BundleData struct { | |
| // Applications holds one entry for each application | |
| // that the bundle will create, indexed by | |
| // the application name. | |
| Applications map[string]*ApplicationSpec `bson:"applications,omitempty" json:"applications,omitempty" yaml:"applications,omitempty"` | |
| // Machines holds one entry for each machine referred to | |
| // by unit placements. These will be mapped onto actual | |
| // machines at bundle deployment time. | |
| // It is an error if a machine is specified but | |
| // not referred to by a unit placement directive. | |
| Machines map[string]*MachineSpec `bson:",omitempty" json:",omitempty" yaml:",omitempty"` | |
| // Series holds the default series to use when | |
| // the bundle chooses charms. | |
| Series string `bson:",omitempty" json:",omitempty" yaml:",omitempty"` | |
| // Relations holds a slice of 2-element slices, | |
| // each specifying a relation between two applications. | |
| // Each two-element slice holds two endpoints, | |
| // each specified as either colon-separated | |
| // (application, relation) pair or just an application name. | |
| // The relation is made between each. If the relation | |
| // name is omitted, it will be inferred from the available | |
| // relations defined in the applications' charms. | |
| Relations [][]string `bson:",omitempty" json:",omitempty" yaml:",omitempty"` | |
| // White listed set of tags to categorize bundles as we do charms. | |
| Tags []string `bson:",omitempty" json:",omitempty" yaml:",omitempty"` | |
| // Short paragraph explaining what the bundle is useful for. | |
| Description string `bson:",omitempty" json:",omitempty" yaml:",omitempty"` | |
| // unmarshaledWithServices holds whether the original marshaled data held a | |
| // legacy "services" field rather than the "applications" field. | |
| unmarshaledWithServices bool | |
| } | |
| // UnmarshaledWithServices reports whether the bundle data was | |
| // unmarshaled from a representation that used the legacy "services" | |
| // field rather than the "applications" field. | |
| func (d *BundleData) UnmarshaledWithServices() bool { | |
| return d.unmarshaledWithServices | |
| } | |
| // MachineSpec represents a notional machine that will be mapped | |
| // onto an actual machine at bundle deployment time. | |
| type MachineSpec struct { | |
| Constraints string `bson:",omitempty" json:",omitempty" yaml:",omitempty"` | |
| Annotations map[string]string `bson:",omitempty" json:",omitempty" yaml:",omitempty"` | |
| Series string `bson:",omitempty" json:",omitempty" yaml:",omitempty"` | |
| } | |
| // ApplicationSpec represents a single application that will | |
| // be deployed as part of the bundle. | |
| type ApplicationSpec struct { | |
| // Charm holds the charm URL of the charm to | |
| // use for the given application. | |
| Charm string | |
| // Series is the series to use when deploying a local charm, | |
| // if the charm does not specify a default or the default | |
| // is not the desired value. | |
| // Series is not compatible with charm store charms where | |
| // the series is specified in the URL. | |
| Series string `bson:",omitempty" yaml:",omitempty" json:",omitempty"` | |
| // Resources is the set of resource revisions to deploy for the | |
| // application. Bundles only support charm store resources and not ones | |
| // that were uploaded to the controller. | |
| Resources map[string]interface{} `bson:",omitempty" yaml:",omitempty" json:",omitempty"` | |
| // NumUnits holds the number of units of the | |
| // application that will be deployed. | |
| // | |
| // For a subordinate application, this actually represents | |
| // an arbitrary number of units depending on | |
| // the application it is related to. | |
| NumUnits int `bson:",omitempty" yaml:"num_units,omitempty" json:",omitempty"` | |
| // To may hold up to NumUnits members with | |
| // each member specifying a desired placement | |
| // for the respective unit of the application. | |
| // | |
| // In regular-expression-like notation, each | |
| // element matches the following pattern: | |
| // | |
| // (<containertype>:)?(<unit>|<machine>|new) | |
| // | |
| // If containertype is specified, the unit is deployed | |
| // into a new container of that type, otherwise | |
| // it will be "hulk-smashed" into the specified location, | |
| // by co-locating it with any other units that happen to | |
| // be there, which may result in unintended behavior. | |
| // | |
| // The second part (after the colon) specifies where | |
| // the new unit should be placed - it may refer to | |
| // a unit of another application specified in the bundle, | |
| // a machine id specified in the machines section, | |
| // or the special name "new" which specifies a newly | |
| // created machine. | |
| // | |
| // A unit placement may be specified with an application name only, | |
| // in which case its unit number is assumed to | |
| // be one more than the unit number of the previous | |
| // unit in the list with the same application, or zero | |
| // if there were none. | |
| // | |
| // If there are less elements in To than NumUnits, | |
| // the last element is replicated to fill it. If there | |
| // are no elements (or To is omitted), "new" is replicated. | |
| // | |
| // For example: | |
| // | |
| // wordpress/0 wordpress/1 lxc:0 kvm:new | |
| // | |
| // specifies that the first two units get hulk-smashed | |
| // onto the first two units of the wordpress application, | |
| // the third unit gets allocated onto an lxc container | |
| // on machine 0, and subsequent units get allocated | |
| // on kvm containers on new machines. | |
| // | |
| // The above example is the same as this: | |
| // | |
| // wordpress wordpress lxc:0 kvm:new | |
| To []string `bson:",omitempty" json:",omitempty" yaml:",omitempty"` | |
| // Expose holds whether the application must be exposed. | |
| Expose bool `bson:",omitempty" json:",omitempty" yaml:",omitempty"` | |
| // Options holds the configuration values | |
| // to apply to the new application. They should | |
| // be compatible with the charm configuration. | |
| Options map[string]interface{} `bson:",omitempty" json:",omitempty" yaml:",omitempty"` | |
| // Annotations holds any annotations to apply to the | |
| // application when deployed. | |
| Annotations map[string]string `bson:",omitempty" json:",omitempty" yaml:",omitempty"` | |
| // Constraints holds the default constraints to apply | |
| // when creating new machines for units of the application. | |
| // This is ignored for units with explicit placement directives. | |
| Constraints string `bson:",omitempty" json:",omitempty" yaml:",omitempty"` | |
| // Storage holds the constraints for storage to assign | |
| // to units of the application. | |
| Storage map[string]string `bson:",omitempty" json:",omitempty" yaml:",omitempty"` | |
| // EndpointBindings maps how endpoints are bound to spaces | |
| EndpointBindings map[string]string `bson:"bindings,omitempty" json:"bindings,omitempty" yaml:"bindings,omitempty"` | |
| } | |
| // ReadBundleData reads bundle data from the given reader. | |
| // The returned data is not verified - call Verify to ensure | |
| // that it is OK. | |
| func ReadBundleData(r io.Reader) (*BundleData, error) { | |
| bytes, err := ioutil.ReadAll(r) | |
| if err != nil { | |
| return nil, err | |
| } | |
| var bd BundleData | |
| if err := yaml.Unmarshal(bytes, &bd); err != nil { | |
| return nil, fmt.Errorf("cannot unmarshal bundle data: %v", err) | |
| } | |
| return &bd, nil | |
| } | |
| // VerificationError holds an error generated by BundleData.Verify, | |
| // holding all the verification errors found when verifying. | |
| type VerificationError struct { | |
| Errors []error | |
| } | |
| func (err *VerificationError) Error() string { | |
| switch len(err.Errors) { | |
| case 0: | |
| return "no verification errors!" | |
| case 1: | |
| return err.Errors[0].Error() | |
| } | |
| return fmt.Sprintf("%s (and %d more errors)", err.Errors[0], len(err.Errors)-1) | |
| } | |
| type bundleDataVerifier struct { | |
| // bundleDir is the directory containing the bundle file | |
| bundleDir string | |
| bd *BundleData | |
| // machines holds the reference counts of all machines | |
| // as referred to by placement directives. | |
| machineRefCounts map[string]int | |
| charms map[string]Charm | |
| errors []error | |
| verifyConstraints func(c string) error | |
| verifyStorage func(s string) error | |
| } | |
| func (verifier *bundleDataVerifier) addErrorf(f string, a ...interface{}) { | |
| verifier.addError(fmt.Errorf(f, a...)) | |
| } | |
| func (verifier *bundleDataVerifier) addError(err error) { | |
| verifier.errors = append(verifier.errors, err) | |
| } | |
| func (verifier *bundleDataVerifier) err() error { | |
| if len(verifier.errors) > 0 { | |
| return &VerificationError{verifier.errors} | |
| } | |
| return nil | |
| } | |
| // RequiredCharms returns a sorted slice of all the charm URLs | |
| // required by the bundle. | |
| func (bd *BundleData) RequiredCharms() []string { | |
| req := make([]string, 0, len(bd.Applications)) | |
| for _, svc := range bd.Applications { | |
| req = append(req, svc.Charm) | |
| } | |
| sort.Strings(req) | |
| return req | |
| } | |
| // VerifyLocal verifies that a local bundle file is consistent. | |
| // A local bundle file may contain references to charms which are | |
| // referred to by a directory, either relative or absolute. | |
| // | |
| // bundleDir is used to construct the full path for charms specified | |
| // using a relative directory path. The charm path is therefore expected | |
| // to be relative to the bundle.yaml file. | |
| func (bd *BundleData) VerifyLocal( | |
| bundleDir string, | |
| verifyConstraints func(c string) error, | |
| verifyStorage func(s string) error, | |
| ) error { | |
| return bd.verifyBundle(bundleDir, verifyConstraints, verifyStorage, nil) | |
| } | |
| // Verify is a convenience method that calls VerifyWithCharms | |
| // with a nil charms map. | |
| func (bd *BundleData) Verify( | |
| verifyConstraints func(c string) error, | |
| verifyStorage func(s string) error, | |
| ) error { | |
| return bd.VerifyWithCharms(verifyConstraints, verifyStorage, nil) | |
| } | |
| // VerifyWithCharms verifies that the bundle is consistent. | |
| // The verifyConstraints function is called to verify any constraints | |
| // that are found. If verifyConstraints is nil, no checking | |
| // of constraints will be done. Similarly, a non-nil verifyStorage | |
| // function is called to verify any storage constraints. | |
| // | |
| // It verifies the following: | |
| // | |
| // - All defined machines are referred to by placement directives. | |
| // - All applications referred to by placement directives are specified in the bundle. | |
| // - All applications referred to by relations are specified in the bundle. | |
| // - All basic constraints are valid. | |
| // - All storage constraints are valid. | |
| // | |
| // If charms is not nil, it should hold a map with an entry for each | |
| // charm url returned by bd.RequiredCharms. The verification will then | |
| // also check that applications are defined with valid charms, | |
| // relations are correctly made and options are defined correctly. | |
| // | |
| // If the verification fails, Verify returns a *VerificationError describing | |
| // all the problems found. | |
| func (bd *BundleData) VerifyWithCharms( | |
| verifyConstraints func(c string) error, | |
| verifyStorage func(s string) error, | |
| charms map[string]Charm, | |
| ) error { | |
| return bd.verifyBundle("", verifyConstraints, verifyStorage, charms) | |
| } | |
| func (bd *BundleData) verifyBundle( | |
| bundleDir string, | |
| verifyConstraints func(c string) error, | |
| verifyStorage func(s string) error, | |
| charms map[string]Charm, | |
| ) error { | |
| if verifyConstraints == nil { | |
| verifyConstraints = func(string) error { | |
| return nil | |
| } | |
| } | |
| if verifyStorage == nil { | |
| verifyStorage = func(string) error { | |
| return nil | |
| } | |
| } | |
| verifier := &bundleDataVerifier{ | |
| bundleDir: bundleDir, | |
| verifyConstraints: verifyConstraints, | |
| verifyStorage: verifyStorage, | |
| bd: bd, | |
| machineRefCounts: make(map[string]int), | |
| charms: charms, | |
| } | |
| for id := range bd.Machines { | |
| verifier.machineRefCounts[id] = 0 | |
| } | |
| if bd.Series != "" && !IsValidSeries(bd.Series) { | |
| verifier.addErrorf("bundle declares an invalid series %q", bd.Series) | |
| } | |
| verifier.verifyMachines() | |
| verifier.verifyApplications() | |
| verifier.verifyRelations() | |
| verifier.verifyOptions() | |
| verifier.verifyEndpointBindings() | |
| for id, count := range verifier.machineRefCounts { | |
| if count == 0 { | |
| verifier.addErrorf("machine %q is not referred to by a placement directive", id) | |
| } | |
| } | |
| return verifier.err() | |
| } | |
| var ( | |
| validMachineId = regexp.MustCompile("^" + names.NumberSnippet + "$") | |
| validStorageName = regexp.MustCompile("^" + names.StorageNameSnippet + "$") | |
| ) | |
| func (verifier *bundleDataVerifier) verifyMachines() { | |
| for id, m := range verifier.bd.Machines { | |
| if !validMachineId.MatchString(id) { | |
| verifier.addErrorf("invalid machine id %q found in machines", id) | |
| } | |
| if m == nil { | |
| continue | |
| } | |
| if m.Constraints != "" { | |
| if err := verifier.verifyConstraints(m.Constraints); err != nil { | |
| verifier.addErrorf("invalid constraints %q in machine %q: %v", m.Constraints, id, err) | |
| } | |
| } | |
| if m.Series != "" && !IsValidSeries(m.Series) { | |
| verifier.addErrorf("invalid series %s for machine %q", m.Series, id) | |
| } | |
| } | |
| } | |
| func (verifier *bundleDataVerifier) verifyApplications() { | |
| if len(verifier.bd.Applications) == 0 { | |
| verifier.addErrorf("at least one application must be specified") | |
| return | |
| } | |
| for name, svc := range verifier.bd.Applications { | |
| if svc.Charm == "" { | |
| verifier.addErrorf("empty charm path") | |
| } | |
| // Charm may be a local directory or a charm URL. | |
| var curl *URL | |
| var err error | |
| if strings.HasPrefix(svc.Charm, ".") || filepath.IsAbs(svc.Charm) { | |
| charmPath := svc.Charm | |
| if !filepath.IsAbs(charmPath) { | |
| charmPath = filepath.Join(verifier.bundleDir, charmPath) | |
| } | |
| if _, err := os.Stat(charmPath); err != nil { | |
| if os.IsNotExist(err) { | |
| verifier.addErrorf("charm path in application %q does not exist: %v", name, charmPath) | |
| } else { | |
| verifier.addErrorf("invalid charm path in application %q: %v", name, err) | |
| } | |
| } | |
| } else if curl, err = ParseURL(svc.Charm); err != nil { | |
| verifier.addErrorf("invalid charm URL in application %q: %v", name, err) | |
| } | |
| // Check the series. | |
| if curl != nil && curl.Series != "" && svc.Series != "" && curl.Series != svc.Series { | |
| verifier.addErrorf("the charm URL for application %q has a series which does not match, please remove the series from the URL", name) | |
| } | |
| if svc.Series != "" && !IsValidSeries(svc.Series) { | |
| verifier.addErrorf("application %q declares an invalid series %q", name, svc.Series) | |
| } | |
| if err := verifier.verifyConstraints(svc.Constraints); err != nil { | |
| verifier.addErrorf("invalid constraints %q in application %q: %v", svc.Constraints, name, err) | |
| } | |
| for storageName, storageConstraints := range svc.Storage { | |
| if !validStorageName.MatchString(storageName) { | |
| verifier.addErrorf("invalid storage name %q in application %q", storageName, name) | |
| } | |
| if err := verifier.verifyStorage(storageConstraints); err != nil { | |
| verifier.addErrorf("invalid storage %q in application %q: %v", storageName, name, err) | |
| } | |
| } | |
| if verifier.charms != nil { | |
| if ch, ok := verifier.charms[svc.Charm]; ok { | |
| if ch.Meta().Subordinate { | |
| if len(svc.To) > 0 { | |
| verifier.addErrorf("application %q is subordinate but specifies unit placement", name) | |
| } | |
| if svc.NumUnits > 0 { | |
| verifier.addErrorf("application %q is subordinate but has non-zero num_units", name) | |
| } | |
| } | |
| } else { | |
| verifier.addErrorf("application %q refers to non-existent charm %q", name, svc.Charm) | |
| } | |
| } | |
| for resName := range svc.Resources { | |
| if resName == "" { | |
| verifier.addErrorf("missing resource name on application %q", name) | |
| } | |
| // We do not check the revisions because all values | |
| // are allowed. | |
| } | |
| if svc.NumUnits < 0 { | |
| verifier.addErrorf("negative number of units specified on application %q", name) | |
| } else if len(svc.To) > svc.NumUnits { | |
| verifier.addErrorf("too many units specified in unit placement for application %q", name) | |
| } | |
| verifier.verifyPlacement(svc.To) | |
| } | |
| } | |
| func (verifier *bundleDataVerifier) verifyPlacement(to []string) { | |
| for _, p := range to { | |
| up, err := ParsePlacement(p) | |
| if err != nil { | |
| verifier.addError(err) | |
| continue | |
| } | |
| switch { | |
| case up.Application != "": | |
| spec, ok := verifier.bd.Applications[up.Application] | |
| if !ok { | |
| verifier.addErrorf("placement %q refers to an application not defined in this bundle", p) | |
| continue | |
| } | |
| if up.Unit >= 0 && up.Unit >= spec.NumUnits { | |
| verifier.addErrorf("placement %q specifies a unit greater than the %d unit(s) started by the target application", p, spec.NumUnits) | |
| } | |
| case up.Machine == "new": | |
| default: | |
| _, ok := verifier.bd.Machines[up.Machine] | |
| if !ok { | |
| verifier.addErrorf("placement %q refers to a machine not defined in this bundle", p) | |
| continue | |
| } | |
| verifier.machineRefCounts[up.Machine]++ | |
| } | |
| } | |
| } | |
| func (verifier *bundleDataVerifier) getCharmMetaForApplication(appName string) (*Meta, error) { | |
| svc, ok := verifier.bd.Applications[appName] | |
| if !ok { | |
| return nil, fmt.Errorf("application %q not found", appName) | |
| } | |
| ch, ok := verifier.charms[svc.Charm] | |
| if !ok { | |
| return nil, fmt.Errorf("charm %q from application %q not found", svc.Charm, appName) | |
| } | |
| return ch.Meta(), nil | |
| } | |
| func (verifier *bundleDataVerifier) verifyRelations() { | |
| seen := make(map[[2]endpoint]bool) | |
| for _, relPair := range verifier.bd.Relations { | |
| if len(relPair) != 2 { | |
| verifier.addErrorf("relation %q has %d endpoint(s), not 2", relPair, len(relPair)) | |
| continue | |
| } | |
| var epPair [2]endpoint | |
| relParseErr := false | |
| for i, svcRel := range relPair { | |
| ep, err := parseEndpoint(svcRel) | |
| if err != nil { | |
| verifier.addError(err) | |
| relParseErr = true | |
| continue | |
| } | |
| if _, ok := verifier.bd.Applications[ep.application]; !ok { | |
| verifier.addErrorf("relation %q refers to application %q not defined in this bundle", relPair, ep.application) | |
| } | |
| epPair[i] = ep | |
| } | |
| if relParseErr { | |
| // We failed to parse at least one relation, so don't | |
| // bother checking further. | |
| continue | |
| } | |
| if epPair[0].application == epPair[1].application { | |
| verifier.addErrorf("relation %q relates an application to itself", relPair) | |
| } | |
| // Resolve endpoint relations if necessary and we have | |
| // the necessary charm information. | |
| if (epPair[0].relation == "" || epPair[1].relation == "") && verifier.charms != nil { | |
| iep0, iep1, err := inferEndpoints(epPair[0], epPair[1], verifier.getCharmMetaForApplication) | |
| if err != nil { | |
| verifier.addErrorf("cannot infer endpoint between %s and %s: %v", epPair[0], epPair[1], err) | |
| } else { | |
| // Change the endpoints that get recorded | |
| // as seen, so we'll diagnose a duplicate | |
| // relation even if one relation specifies | |
| // the relations explicitly and the other does | |
| // not. | |
| epPair[0], epPair[1] = iep0, iep1 | |
| } | |
| } | |
| // Re-order pairs so that we diagnose duplicate relations | |
| // whichever way they're specified. | |
| if epPair[1].less(epPair[0]) { | |
| epPair[1], epPair[0] = epPair[0], epPair[1] | |
| } | |
| if _, ok := seen[epPair]; ok { | |
| verifier.addErrorf("relation %q is defined more than once", relPair) | |
| } | |
| if verifier.charms != nil && epPair[0].relation != "" && epPair[1].relation != "" { | |
| // We have charms to verify against, and the | |
| // endpoint has been fully specified or inferred. | |
| verifier.verifyRelation(epPair[0], epPair[1]) | |
| } | |
| seen[epPair] = true | |
| } | |
| } | |
| func (verifier *bundleDataVerifier) verifyEndpointBindings() { | |
| for name, svc := range verifier.bd.Applications { | |
| charm, ok := verifier.charms[name] | |
| // Only thest the ok path here because the !ok path is tested in verifyApplications | |
| if !ok { | |
| continue | |
| } | |
| for endpoint, space := range svc.EndpointBindings { | |
| _, isInProvides := charm.Meta().Provides[endpoint] | |
| _, isInRequires := charm.Meta().Requires[endpoint] | |
| _, isInPeers := charm.Meta().Peers[endpoint] | |
| _, isInExtraBindings := charm.Meta().ExtraBindings[endpoint] | |
| if !(isInProvides || isInRequires || isInPeers || isInExtraBindings) { | |
| verifier.addErrorf( | |
| "application %q wants to bind endpoint %q to space %q, "+ | |
| "but the endpoint is not defined by the charm", | |
| name, endpoint, space) | |
| } | |
| } | |
| } | |
| } | |
| var infoRelation = Relation{ | |
| Name: "juju-info", | |
| Role: RoleProvider, | |
| Interface: "juju-info", | |
| Scope: ScopeContainer, | |
| } | |
| // verifyRelation verifies a single relation. | |
| // It checks that both endpoints of the relation are | |
| // defined, and that the relationship is correctly | |
| // symmetrical (provider to requirer) and shares | |
| // the same interface. | |
| func (verifier *bundleDataVerifier) verifyRelation(ep0, ep1 endpoint) { | |
| svc0 := verifier.bd.Applications[ep0.application] | |
| svc1 := verifier.bd.Applications[ep1.application] | |
| if svc0 == nil || svc1 == nil || svc0 == svc1 { | |
| // An error will be produced by verifyRelations for this case. | |
| return | |
| } | |
| charm0 := verifier.charms[svc0.Charm] | |
| charm1 := verifier.charms[svc1.Charm] | |
| if charm0 == nil || charm1 == nil { | |
| // An error will be produced by verifyApplications for this case. | |
| return | |
| } | |
| relProv0, okProv0 := charm0.Meta().Provides[ep0.relation] | |
| // The juju-info relation is provided implicitly by every | |
| // charm - use it if required. | |
| if !okProv0 && ep0.relation == infoRelation.Name { | |
| relProv0, okProv0 = infoRelation, true | |
| } | |
| relReq0, okReq0 := charm0.Meta().Requires[ep0.relation] | |
| if !okProv0 && !okReq0 { | |
| verifier.addErrorf("charm %q used by application %q does not define relation %q", svc0.Charm, ep0.application, ep0.relation) | |
| } | |
| relProv1, okProv1 := charm1.Meta().Provides[ep1.relation] | |
| // The juju-info relation is provided implicitly by every | |
| // charm - use it if required. | |
| if !okProv1 && ep1.relation == infoRelation.Name { | |
| relProv1, okProv1 = infoRelation, true | |
| } | |
| relReq1, okReq1 := charm1.Meta().Requires[ep1.relation] | |
| if !okProv1 && !okReq1 { | |
| verifier.addErrorf("charm %q used by application %q does not define relation %q", svc1.Charm, ep1.application, ep1.relation) | |
| } | |
| var relProv, relReq Relation | |
| var epProv, epReq endpoint | |
| switch { | |
| case okProv0 && okReq1: | |
| relProv, relReq = relProv0, relReq1 | |
| epProv, epReq = ep0, ep1 | |
| case okReq0 && okProv1: | |
| relProv, relReq = relProv1, relReq0 | |
| epProv, epReq = ep1, ep0 | |
| case okProv0 && okProv1: | |
| verifier.addErrorf("relation %q to %q relates provider to provider", ep0, ep1) | |
| return | |
| case okReq0 && okReq1: | |
| verifier.addErrorf("relation %q to %q relates requirer to requirer", ep0, ep1) | |
| return | |
| default: | |
| // Errors were added above. | |
| return | |
| } | |
| if relProv.Interface != relReq.Interface { | |
| verifier.addErrorf("mismatched interface between %q and %q (%q vs %q)", epProv, epReq, relProv.Interface, relReq.Interface) | |
| } | |
| } | |
| // verifyOptions verifies that the options are correctly defined | |
| // with respect to the charm config options. | |
| func (verifier *bundleDataVerifier) verifyOptions() { | |
| if verifier.charms == nil { | |
| return | |
| } | |
| for appName, svc := range verifier.bd.Applications { | |
| charm := verifier.charms[svc.Charm] | |
| if charm == nil { | |
| // An error will be produced by verifyApplications for this case. | |
| continue | |
| } | |
| config := charm.Config() | |
| for name, value := range svc.Options { | |
| opt, ok := config.Options[name] | |
| if !ok { | |
| verifier.addErrorf("cannot validate application %q: configuration option %q not found in charm %q", appName, name, svc.Charm) | |
| continue | |
| } | |
| _, err := opt.validate(name, value) | |
| if err != nil { | |
| verifier.addErrorf("cannot validate application %q: %v", appName, err) | |
| } | |
| } | |
| } | |
| } | |
| var validApplicationRelation = regexp.MustCompile("^(" + names.ApplicationSnippet + "):(" + names.RelationSnippet + ")$") | |
| type endpoint struct { | |
| application string | |
| relation string | |
| } | |
| func (ep endpoint) String() string { | |
| if ep.relation == "" { | |
| return ep.application | |
| } | |
| return fmt.Sprintf("%s:%s", ep.application, ep.relation) | |
| } | |
| func (ep1 endpoint) less(ep2 endpoint) bool { | |
| if ep1.application == ep2.application { | |
| return ep1.relation < ep2.relation | |
| } | |
| return ep1.application < ep2.application | |
| } | |
| func parseEndpoint(ep string) (endpoint, error) { | |
| m := validApplicationRelation.FindStringSubmatch(ep) | |
| if m != nil { | |
| return endpoint{ | |
| application: m[1], | |
| relation: m[2], | |
| }, nil | |
| } | |
| if !names.IsValidApplication(ep) { | |
| return endpoint{}, fmt.Errorf("invalid relation syntax %q", ep) | |
| } | |
| return endpoint{ | |
| application: ep, | |
| }, nil | |
| } | |
| // endpointInfo holds information about one endpoint of a relation. | |
| type endpointInfo struct { | |
| applicationName string | |
| Relation | |
| } | |
| // String returns the unique identifier of the relation endpoint. | |
| func (ep endpointInfo) String() string { | |
| return ep.applicationName + ":" + ep.Name | |
| } | |
| // canRelateTo returns whether a relation may be established between ep | |
| // and other. | |
| func (ep endpointInfo) canRelateTo(other endpointInfo) bool { | |
| return ep.applicationName != other.applicationName && | |
| ep.Interface == other.Interface && | |
| ep.Role != RolePeer && | |
| counterpartRole(ep.Role) == other.Role | |
| } | |
| // endpoint returns the endpoint specifier for ep. | |
| func (ep endpointInfo) endpoint() endpoint { | |
| return endpoint{ | |
| application: ep.applicationName, | |
| relation: ep.Name, | |
| } | |
| } | |
| // counterpartRole returns the RelationRole that the given RelationRole | |
| // can relate to. | |
| func counterpartRole(r RelationRole) RelationRole { | |
| switch r { | |
| case RoleProvider: | |
| return RoleRequirer | |
| case RoleRequirer: | |
| return RoleProvider | |
| case RolePeer: | |
| return RolePeer | |
| } | |
| panic(fmt.Errorf("unknown relation role %q", r)) | |
| } | |
| type UnitPlacement struct { | |
| // ContainerType holds the container type of the new | |
| // new unit, or empty if unspecified. | |
| ContainerType string | |
| // Machine holds the numeric machine id, or "new", | |
| // or empty if the placement specifies an application. | |
| Machine string | |
| // application holds the application name, or empty if | |
| // the placement specifies a machine. | |
| Application string | |
| // Unit holds the unit number of the application, or -1 | |
| // if unspecified. | |
| Unit int | |
| } | |
| var snippetReplacer = strings.NewReplacer( | |
| "container", names.ContainerTypeSnippet, | |
| "number", names.NumberSnippet, | |
| "application", names.ApplicationSnippet, | |
| ) | |
| // validPlacement holds regexp that matches valid placement requests. To | |
| // make the expression easier to comprehend and maintain, we replace | |
| // symbolic snippet references in the regexp by their actual regexps | |
| // using snippetReplacer. | |
| var validPlacement = regexp.MustCompile( | |
| snippetReplacer.Replace( | |
| "^(?:(container):)?(?:(application)(?:/(number))?|(number))$", | |
| ), | |
| ) | |
| // ParsePlacement parses a unit placement directive, as | |
| // specified in the To clause of an application entry in the | |
| // applications section of a bundle. | |
| func ParsePlacement(p string) (*UnitPlacement, error) { | |
| m := validPlacement.FindStringSubmatch(p) | |
| if m == nil { | |
| return nil, fmt.Errorf("invalid placement syntax %q", p) | |
| } | |
| up := UnitPlacement{ | |
| ContainerType: m[1], | |
| Application: m[2], | |
| Machine: m[4], | |
| } | |
| if unitStr := m[3]; unitStr != "" { | |
| // We know that unitStr must be a valid integer because | |
| // it's specified as such in the regexp. | |
| up.Unit, _ = strconv.Atoi(unitStr) | |
| } else { | |
| up.Unit = -1 | |
| } | |
| if up.Application == "new" { | |
| if up.Unit != -1 { | |
| return nil, fmt.Errorf("invalid placement syntax %q", p) | |
| } | |
| up.Machine, up.Application = "new", "" | |
| } | |
| return &up, nil | |
| } | |
| // inferEndpoints infers missing relation names from the given endpoint | |
| // specifications, using the given get function to retrieve charm | |
| // data if necessary. It returns the fully specified endpoints. | |
| func inferEndpoints(epSpec0, epSpec1 endpoint, get func(svc string) (*Meta, error)) (endpoint, endpoint, error) { | |
| if epSpec0.relation != "" && epSpec1.relation != "" { | |
| // The endpoints are already specified explicitly so | |
| // there is no need to fetch any charm data to infer | |
| // them. | |
| return epSpec0, epSpec1, nil | |
| } | |
| eps0, err := possibleEndpoints(epSpec0, get) | |
| if err != nil { | |
| return endpoint{}, endpoint{}, err | |
| } | |
| eps1, err := possibleEndpoints(epSpec1, get) | |
| if err != nil { | |
| return endpoint{}, endpoint{}, err | |
| } | |
| var candidates [][]endpointInfo | |
| for _, ep0 := range eps0 { | |
| for _, ep1 := range eps1 { | |
| if ep0.canRelateTo(ep1) { | |
| candidates = append(candidates, []endpointInfo{ep0, ep1}) | |
| } | |
| } | |
| } | |
| switch len(candidates) { | |
| case 0: | |
| return endpoint{}, endpoint{}, fmt.Errorf("no relations found") | |
| case 1: | |
| return candidates[0][0].endpoint(), candidates[0][1].endpoint(), nil | |
| } | |
| // There's ambiguity; try discarding implicit relations. | |
| filtered := discardImplicitRelations(candidates) | |
| if len(filtered) == 1 { | |
| return filtered[0][0].endpoint(), filtered[0][1].endpoint(), nil | |
| } | |
| // The ambiguity cannot be resolved, so return an error. | |
| var keys []string | |
| for _, cand := range candidates { | |
| keys = append(keys, fmt.Sprintf("%q", relationKey(cand))) | |
| } | |
| sort.Strings(keys) | |
| return endpoint{}, endpoint{}, fmt.Errorf("ambiguous relation: %s %s could refer to %s", | |
| epSpec0, epSpec1, strings.Join(keys, "; ")) | |
| } | |
| func discardImplicitRelations(candidates [][]endpointInfo) [][]endpointInfo { | |
| var filtered [][]endpointInfo | |
| outer: | |
| for _, cand := range candidates { | |
| for _, ep := range cand { | |
| if ep.IsImplicit() { | |
| continue outer | |
| } | |
| } | |
| filtered = append(filtered, cand) | |
| } | |
| return filtered | |
| } | |
| // relationKey returns a string describing the relation defined by | |
| // endpoints, for use in various contexts (including error messages). | |
| func relationKey(endpoints []endpointInfo) string { | |
| var names []string | |
| for _, ep := range endpoints { | |
| names = append(names, ep.String()) | |
| } | |
| sort.Strings(names) | |
| return strings.Join(names, " ") | |
| } | |
| // possibleEndpoints returns all the endpoints that the given endpoint spec | |
| // could refer to. | |
| func possibleEndpoints(epSpec endpoint, get func(svc string) (*Meta, error)) ([]endpointInfo, error) { | |
| meta, err := get(epSpec.application) | |
| if err != nil { | |
| return nil, err | |
| } | |
| var eps []endpointInfo | |
| add := func(r Relation) { | |
| if epSpec.relation == "" || epSpec.relation == r.Name { | |
| eps = append(eps, endpointInfo{ | |
| applicationName: epSpec.application, | |
| Relation: r, | |
| }) | |
| } | |
| } | |
| for _, r := range meta.Provides { | |
| add(r) | |
| } | |
| for _, r := range meta.Requires { | |
| add(r) | |
| } | |
| // Every application implicitly provides a juju-info relation. | |
| add(Relation{ | |
| Name: "juju-info", | |
| Role: RoleProvider, | |
| Interface: "juju-info", | |
| Scope: ScopeGlobal, | |
| }) | |
| return eps, nil | |
| } |