From e3f29b6b7b02a412fae71bea0681b3afa6882547 Mon Sep 17 00:00:00 2001 From: Simon Richardson Date: Wed, 24 Jul 2024 17:40:40 +0100 Subject: [PATCH 1/3] feat: add charm metadata to application description To correctly have RI (referential integrity) when adding a application in 4.0, we need to know the charm metadata. This essential metadata is a requirement for things to be able to function correctly. We don't need actions, config and lxd profiles, they can be supplied when we upload the actual binary blob. As this a deeply nested struct, I'm treating it as one amorphous blob, as apposed to creating and setting each nested entity as it's own thing. The version will be the same for the whole of the metadata in it's first implementation. Although each nested type has it's own schema, so future iterations can update it independently from each other. --- application.go | 42 ++ application_test.go | 12 +- charmmetadata.go | 916 ++++++++++++++++++++++++++++++++++++++++++ charmmetadata_test.go | 358 +++++++++++++++++ charmorigin.go | 6 +- interfaces.go | 83 ++++ model.go | 2 +- 7 files changed, 1412 insertions(+), 7 deletions(-) create mode 100644 charmmetadata.go create mode 100644 charmmetadata_test.go diff --git a/application.go b/application.go index 3086d7b..0f7a320 100644 --- a/application.go +++ b/application.go @@ -61,6 +61,9 @@ type Application interface { CharmOrigin() CharmOrigin SetCharmOrigin(CharmOriginArgs) + CharmMetadata() CharmMetadata + SetCharmMetadata(CharmMetadataArgs) + Tools() AgentTools SetTools(AgentToolsArgs) @@ -147,6 +150,8 @@ type application struct { // CharmOrigin fields CharmOrigin_ *charmOrigin `yaml:"charm-origin,omitempty"` + // CharmMetadata fields + CharmMetadata_ *charmMetadata `yaml:"charm-metadata,omitempty"` } // ApplicationArgs is an argument struct used to add an application to the Model. @@ -517,6 +522,20 @@ func (a *application) SetCharmOrigin(args CharmOriginArgs) { a.CharmOrigin_ = newCharmOrigin(args) } +// CharmMetadata implements Application. +func (a *application) CharmMetadata() CharmMetadata { + // To avoid a typed nil, check before returning. + if a.CharmMetadata_ == nil { + return nil + } + return a.CharmMetadata_ +} + +// SetCharmMetadata implements Application. +func (a *application) SetCharmMetadata(args CharmMetadataArgs) { + a.CharmMetadata_ = newCharmMetadata(args) +} + // Offers implements Application. func (a *application) Offers() []ApplicationOffer { if a.Offers_ == nil || len(a.Offers_.Offers) == 0 { @@ -639,6 +658,7 @@ var applicationDeserializationFuncs = map[int]applicationDeserializationFunc{ 10: importApplicationV10, 11: importApplicationV11, 12: importApplicationV12, + 13: importApplicationV13, } func applicationV1Fields() (schema.Fields, schema.Defaults) { @@ -769,6 +789,13 @@ func applicationV12Fields() (schema.Fields, schema.Defaults) { return fields, defaults } +func applicationV13Fields() (schema.Fields, schema.Defaults) { + fields, defaults := applicationV12Fields() + fields["charm-metadata"] = schema.StringMap(schema.Any()) + defaults["charm-metadata"] = schema.Omit + return fields, defaults +} + func importApplicationV1(source map[string]interface{}) (*application, error) { fields, defaults := applicationV1Fields() return importApplication(fields, defaults, 1, source) @@ -829,6 +856,11 @@ func importApplicationV12(source map[string]interface{}) (*application, error) { return importApplication(fields, defaults, 12, source) } +func importApplicationV13(source map[string]interface{}) (*application, error) { + fields, defaults := applicationV13Fields() + return importApplication(fields, defaults, 13, source) +} + func importApplication(fields schema.Fields, defaults schema.Defaults, importVersion int, source map[string]interface{}) (*application, error) { checker := schema.FieldMap(fields, defaults) @@ -938,6 +970,16 @@ func importApplication(fields schema.Fields, defaults schema.Defaults, importVer } + if importVersion >= 13 { + if charmMetadataMap, ok := valid["charm-metadata"]; ok { + charmMetadata, err := importCharmMetadata(charmMetadataMap.(map[string]interface{})) + if err != nil { + return nil, errors.Trace(err) + } + result.CharmMetadata_ = charmMetadata + } + } + result.importAnnotations(valid) if err := result.importStatusHistory(valid); err != nil { diff --git a/application_test.go b/application_test.go index 208b9b2..aded121 100644 --- a/application_test.go +++ b/application_test.go @@ -64,7 +64,8 @@ func minimalApplicationMap() map[interface{}]interface{} { minimalUnitMap(), }, }, - "charm-origin": minimalCharmOriginMap(), + "charm-origin": minimalCharmOriginMap(), + "charm-metadata": minimalCharmMetadataMap(), } } @@ -104,6 +105,7 @@ func minimalApplicationMapCAAS() map[interface{}]interface{} { result["tools"] = minimalAgentToolsMap() result["operator-status"] = minimalStatusMap() result["charm-origin"] = minimalCharmOriginMap() + result["charm-metadata"] = minimalCharmMetadataMap() return result } @@ -124,6 +126,7 @@ func minimalApplication(args ...ApplicationArgs) *application { u.SetTools(minimalAgentToolsArgs()) } a.SetCharmOrigin(minimalCharmOriginArgs()) + a.SetCharmMetadata(minimalCharmMetadataArgs()) return a } @@ -362,7 +365,7 @@ func (s *ApplicationSerializationSuite) exportImportVersion(c *gc.C, application } func (s *ApplicationSerializationSuite) exportImportLatest(c *gc.C, application_ *application) *application { - return s.exportImportVersion(c, application_, 12) + return s.exportImportVersion(c, application_, 13) } func (s *ApplicationSerializationSuite) TestV1ParsingReturnsLatest(c *gc.C) { @@ -383,6 +386,7 @@ func (s *ApplicationSerializationSuite) TestV1ParsingReturnsLatest(c *gc.C) { appLatest.OperatorStatus_ = nil appLatest.Offers_ = nil appLatest.CharmOrigin_ = nil + appLatest.CharmMetadata_ = nil appResult := s.exportImportVersion(c, appV1, 1) appLatest.Series_ = "" @@ -406,6 +410,7 @@ func (s *ApplicationSerializationSuite) TestV2ParsingReturnsLatest(c *gc.C) { appLatest.OperatorStatus_ = nil appLatest.Offers_ = nil appLatest.CharmOrigin_ = nil + appLatest.CharmMetadata_ = nil appResult := s.exportImportVersion(c, appV1, 2) appLatest.Series_ = "" @@ -425,6 +430,7 @@ func (s *ApplicationSerializationSuite) TestV3ParsingReturnsLatest(c *gc.C) { appLatest.OperatorStatus_ = nil appLatest.Offers_ = nil appLatest.CharmOrigin_ = nil + appLatest.CharmMetadata_ = nil appResult := s.exportImportVersion(c, appV2, 3) appLatest.Series_ = "" @@ -440,6 +446,7 @@ func (s *ApplicationSerializationSuite) TestV5ParsingReturnsLatest(c *gc.C) { appLatest := appV5 appLatest.HasResources_ = false appLatest.CharmOrigin_ = nil + appLatest.CharmMetadata_ = nil appResult := s.exportImportVersion(c, appV5, 5) appLatest.Series_ = "" @@ -454,6 +461,7 @@ func (s *ApplicationSerializationSuite) TestV6ParsingReturnsLatest(c *gc.C) { // Make an app with fields not in v6 removed. appLatest := appV6 appLatest.CharmOrigin_ = nil + appLatest.CharmMetadata_ = nil appResult := s.exportImportVersion(c, appV6, 6) appLatest.Series_ = "" diff --git a/charmmetadata.go b/charmmetadata.go new file mode 100644 index 0000000..af69686 --- /dev/null +++ b/charmmetadata.go @@ -0,0 +1,916 @@ +// Copyright 2020 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +package description + +import ( + "github.com/juju/errors" + "github.com/juju/schema" +) + +// CharmMetadataArgs is an argument struct used to create a new +// CharmMetadata. +type CharmMetadataArgs struct { + Name string + Summary string + Description string + Subordinate bool + MinJujuVersion string + RunAs string + Assumes string + Relations map[string]CharmMetadataRelation + ExtraBindings map[string]string + Categories []string + Tags []string + Storage map[string]CharmMetadataStorage + Devices map[string]CharmMetadataDevice + Payloads map[string]CharmMetadataPayload + Resources map[string]CharmMetadataResource + Terms []string + Containers map[string]CharmMetadataContainer +} + +func newCharmMetadata(args CharmMetadataArgs) *charmMetadata { + var relations map[string]charmMetadataRelation + if args.Relations != nil { + relations = make(map[string]charmMetadataRelation, len(args.Relations)) + for k, v := range args.Relations { + relations[k] = charmMetadataRelation{ + Name_: v.Name(), + Role_: v.Role(), + Interface_: v.Interface(), + Optional_: v.Optional(), + Limit_: v.Limit(), + Scope_: v.Scope(), + } + } + } + + var storage map[string]charmMetadataStorage + if args.Storage != nil { + storage = make(map[string]charmMetadataStorage, len(args.Storage)) + for k, v := range args.Storage { + storage[k] = charmMetadataStorage{ + Name_: v.Name(), + Description_: v.Description(), + Type_: v.Type(), + Shared_: v.Shared(), + Readonly_: v.Readonly(), + CountMin_: v.CountMin(), + CountMax_: v.CountMax(), + MinimumSize_: v.MinimumSize(), + Location_: v.Location(), + Properties_: v.Properties(), + } + } + } + + var devices map[string]charmMetadataDevice + if args.Devices != nil { + devices = make(map[string]charmMetadataDevice, len(args.Devices)) + for k, v := range args.Devices { + devices[k] = charmMetadataDevice{ + Name_: v.Name(), + Description_: v.Description(), + Type_: v.Type(), + CountMin_: v.CountMin(), + CountMax_: v.CountMax(), + } + } + } + + var payloads map[string]charmMetadataPayload + if args.Payloads != nil { + payloads = make(map[string]charmMetadataPayload, len(args.Payloads)) + for k, v := range args.Payloads { + payloads[k] = charmMetadataPayload{ + Name_: v.Name(), + Type_: v.Type(), + } + } + } + + var resources map[string]charmMetadataResource + if args.Resources != nil { + resources = make(map[string]charmMetadataResource, len(args.Resources)) + for k, v := range args.Resources { + resources[k] = charmMetadataResource{ + Name_: v.Name(), + Type_: v.Type(), + Path_: v.Path(), + Description_: v.Description(), + } + } + } + + var containers map[string]charmMetadataContainer + if args.Containers != nil { + containers = make(map[string]charmMetadataContainer, len(args.Containers)) + for k, v := range args.Containers { + mounts := make([]charmMetadataContainerMount, len(v.Mounts())) + for i, m := range v.Mounts() { + mounts[i] = charmMetadataContainerMount{ + Storage_: m.Storage(), + Location_: m.Location(), + } + } + containers[k] = charmMetadataContainer{ + Resource_: v.Resource(), + Mounts_: mounts, + Uid_: v.Uid(), + Gid_: v.Gid(), + } + } + } + + return &charmMetadata{ + Version_: 1, + Name_: args.Name, + Summary_: args.Summary, + Description_: args.Description, + Subordinate_: args.Subordinate, + MinJujuVersion_: args.MinJujuVersion, + RunAs_: args.RunAs, + Assumes_: args.Assumes, + Relations_: relations, + ExtraBindings_: args.ExtraBindings, + Categories_: args.Categories, + Tags_: args.Tags, + Storage_: storage, + Devices_: devices, + Payloads_: payloads, + Resources_: resources, + Terms_: args.Terms, + Containers_: containers, + } +} + +// charmMetadata represents the metadata of a charm. +type charmMetadata struct { + Version_ int `yaml:"version"` + Name_ string `yaml:"name,omitempty"` + Summary_ string `yaml:"summary,omitempty"` + Description_ string `yaml:"description,omitempty"` + Subordinate_ bool `yaml:"subordinate,omitempty"` + MinJujuVersion_ string `yaml:"min-juju-version,omitempty"` + RunAs_ string `yaml:"run-as,omitempty"` + Assumes_ string `yaml:"assumes,omitempty"` + Relations_ map[string]charmMetadataRelation `yaml:"relations,omitempty"` + ExtraBindings_ map[string]string `yaml:"extra-bindings,omitempty"` + Categories_ []string `yaml:"categories,omitempty"` + Tags_ []string `yaml:"tags,omitempty"` + Storage_ map[string]charmMetadataStorage `yaml:"storage,omitempty"` + Devices_ map[string]charmMetadataDevice `yaml:"devices,omitempty"` + Payloads_ map[string]charmMetadataPayload `yaml:"payloads,omitempty"` + Resources_ map[string]charmMetadataResource `yaml:"resources,omitempty"` + Terms_ []string `yaml:"terms,omitempty"` + Containers_ map[string]charmMetadataContainer `yaml:"containers,omitempty"` +} + +// Name returns the name of the charm. +func (m *charmMetadata) Name() string { + return m.Name_ +} + +// Summary returns the summary of the charm. +func (m *charmMetadata) Summary() string { + return m.Summary_ +} + +// Description returns the description of the charm. +func (m *charmMetadata) Description() string { + return m.Description_ +} + +// Subordinate returns whether the charm is a subordinate charm. +func (m *charmMetadata) Subordinate() bool { + return m.Subordinate_ +} + +// MinJujuVersion returns the minimum Juju version required by the charm. +func (m *charmMetadata) MinJujuVersion() string { + return m.MinJujuVersion_ +} + +// RunAs returns the user the charm should run as. +func (m *charmMetadata) RunAs() string { + return m.RunAs_ +} + +// Assumes returns the charm the charm assumes. +func (m *charmMetadata) Assumes() string { + return m.Assumes_ +} + +// Relations returns the relations of the charm. +func (m *charmMetadata) Relations() map[string]CharmMetadataRelation { + relations := make(map[string]CharmMetadataRelation, len(m.Relations_)) + for k, v := range m.Relations_ { + relations[k] = v + } + return relations +} + +// ExtraBindings returns the extra bindings of the charm. +func (m *charmMetadata) ExtraBindings() map[string]string { + return m.ExtraBindings_ +} + +// Categories returns the categories of the charm. +func (m *charmMetadata) Categories() []string { + return m.Categories_ +} + +// Tags returns the tags of the charm. +func (m *charmMetadata) Tags() []string { + return m.Tags_ +} + +// Storage returns the storage of the charm. +func (m *charmMetadata) Storage() map[string]CharmMetadataStorage { + storage := make(map[string]CharmMetadataStorage, len(m.Storage_)) + for k, v := range m.Storage_ { + storage[k] = v + } + return storage +} + +// Devices returns the devices of the charm. +func (m *charmMetadata) Devices() map[string]CharmMetadataDevice { + devices := make(map[string]CharmMetadataDevice, len(m.Devices_)) + for k, v := range m.Devices_ { + devices[k] = v + } + return devices +} + +// Payloads returns the payloads of the charm. +func (m *charmMetadata) Payloads() map[string]CharmMetadataPayload { + payloads := make(map[string]CharmMetadataPayload, len(m.Payloads_)) + for k, v := range m.Payloads_ { + payloads[k] = v + } + return payloads +} + +// Resources returns the resources of the charm. +func (m *charmMetadata) Resources() map[string]CharmMetadataResource { + resources := make(map[string]CharmMetadataResource, len(m.Resources_)) + for k, v := range m.Resources_ { + resources[k] = v + } + return resources +} + +// Terms returns the terms of the charm. +func (m *charmMetadata) Terms() []string { + return m.Terms_ +} + +// Containers returns the containers of the charm. +func (m *charmMetadata) Containers() map[string]CharmMetadataContainer { + containers := make(map[string]CharmMetadataContainer, len(m.Containers_)) + for k, v := range m.Containers_ { + containers[k] = v + } + return containers +} + +func importCharmMetadata(source map[string]interface{}) (*charmMetadata, error) { + version, err := getVersion(source) + if err != nil { + return nil, errors.Annotate(err, "charmMetadata version schema check failed") + } + + importFunc, ok := charmMetadataDeserializationFuncs[version] + if !ok { + return nil, errors.NotValidf("version %d", version) + } + + return importFunc(source) +} + +type charmMetadataDeserializationFunc func(map[string]interface{}) (*charmMetadata, error) + +var charmMetadataDeserializationFuncs = map[int]charmMetadataDeserializationFunc{ + 1: importCharmMetadataV1, +} + +func importCharmMetadataV1(source map[string]interface{}) (*charmMetadata, error) { + return importCharmMetadataVersion(source, 1) +} + +func importCharmMetadataVersion(source map[string]interface{}, importVersion int) (*charmMetadata, error) { + fields := schema.Fields{ + "name": schema.String(), + "summary": schema.String(), + "description": schema.String(), + "subordinate": schema.Bool(), + "min-juju-version": schema.String(), + "run-as": schema.String(), + "assumes": schema.String(), + "relations": schema.StringMap(schema.Any()), + "extra-bindings": schema.StringMap(schema.String()), + "categories": schema.List(schema.String()), + "tags": schema.List(schema.String()), + "storage": schema.StringMap(schema.Any()), + "devices": schema.StringMap(schema.Any()), + "payloads": schema.StringMap(schema.Any()), + "resources": schema.StringMap(schema.Any()), + "terms": schema.List(schema.String()), + "containers": schema.StringMap(schema.Any()), + } + defaults := schema.Defaults{ + "summary": schema.Omit, + "description": schema.Omit, + "subordinate": schema.Omit, + "min-juju-version": schema.Omit, + "run-as": schema.Omit, + "assumes": schema.Omit, + "relations": schema.Omit, + "extra-bindings": schema.Omit, + "categories": schema.Omit, + "tags": schema.Omit, + "storage": schema.Omit, + "devices": schema.Omit, + "payloads": schema.Omit, + "resources": schema.Omit, + "terms": schema.Omit, + "containers": schema.Omit, + } + checker := schema.FieldMap(fields, defaults) + + coerced, err := checker.Coerce(source, nil) + if err != nil { + return nil, errors.Annotatef(err, "charmOrigin v1 schema check failed") + } + valid := coerced.(map[string]interface{}) + + var relations map[string]charmMetadataRelation + if valid["relations"] != nil { + relations = make(map[string]charmMetadataRelation) + for k, v := range valid["relations"].(map[string]interface{}) { + var err error + if relations[k], err = importCharmMetadataRelation(v, importVersion); err != nil { + return nil, errors.Annotatef(err, "relation %q", k) + } + } + } + + var extraBindings map[string]string + if valid["extra-bindings"] != nil { + extraBindings = make(map[string]string) + for k, v := range valid["extra-bindings"].(map[string]interface{}) { + extraBindings[k] = v.(string) + } + } + + var categories []string + if valid["categories"] != nil { + categories = make([]string, 0) + for _, v := range valid["categories"].([]interface{}) { + categories = append(categories, v.(string)) + } + } + + var tags []string + if valid["tags"] != nil { + tags = make([]string, 0) + for _, v := range valid["tags"].([]interface{}) { + tags = append(tags, v.(string)) + } + } + + var storage map[string]charmMetadataStorage + if valid["storage"] != nil { + storage = make(map[string]charmMetadataStorage) + for k, v := range valid["storage"].(map[string]interface{}) { + var err error + if storage[k], err = importCharmMetadataStorage(v, importVersion); err != nil { + return nil, errors.Annotatef(err, "storage %q", k) + } + } + } + + var devices map[string]charmMetadataDevice + if valid["devices"] != nil { + devices = make(map[string]charmMetadataDevice) + for k, v := range valid["devices"].(map[string]interface{}) { + var err error + if devices[k], err = importCharmMetadataDevice(v, importVersion); err != nil { + return nil, errors.Annotatef(err, "device %q", k) + } + } + } + + var payloads map[string]charmMetadataPayload + if valid["payloads"] != nil { + payloads = make(map[string]charmMetadataPayload) + for k, v := range valid["payloads"].(map[string]interface{}) { + var err error + if payloads[k], err = importCharmMetadataPayload(v, importVersion); err != nil { + return nil, errors.Annotatef(err, "payload %q", k) + } + } + } + + var resources map[string]charmMetadataResource + if valid["resources"] != nil { + resources = make(map[string]charmMetadataResource) + for k, v := range valid["resources"].(map[string]interface{}) { + var err error + if resources[k], err = importCharmMetadataResource(v, importVersion); err != nil { + return nil, errors.Annotatef(err, "resource %q", k) + } + } + } + + var containers map[string]charmMetadataContainer + if valid["containers"] != nil { + containers = make(map[string]charmMetadataContainer) + for k, v := range valid["containers"].(map[string]interface{}) { + var err error + if containers[k], err = importCharmMetadataContainer(v, importVersion); err != nil { + return nil, errors.Annotatef(err, "container %q", k) + } + } + } + + var terms []string + if valid["terms"] != nil { + terms = make([]string, 0) + for _, v := range valid["terms"].([]interface{}) { + terms = append(terms, v.(string)) + } + } + + var ( + summary string + description string + subordinate bool + minJujuVersion string + runAs string + assumes string + ) + + if valid["summary"] != nil { + summary = valid["summary"].(string) + } + if valid["description"] != nil { + description = valid["description"].(string) + } + if valid["subordinate"] != nil { + subordinate = valid["subordinate"].(bool) + } + if valid["min-juju-version"] != nil { + minJujuVersion = valid["min-juju-version"].(string) + } + if valid["run-as"] != nil { + runAs = valid["run-as"].(string) + } + if valid["assumes"] != nil { + assumes = valid["assumes"].(string) + } + + return &charmMetadata{ + Version_: 1, + Name_: valid["name"].(string), + Summary_: summary, + Description_: description, + Subordinate_: subordinate, + MinJujuVersion_: minJujuVersion, + RunAs_: runAs, + Assumes_: assumes, + Relations_: relations, + ExtraBindings_: extraBindings, + Categories_: categories, + Tags_: tags, + Storage_: storage, + Devices_: devices, + Resources_: resources, + Payloads_: payloads, + Containers_: containers, + Terms_: terms, + }, nil +} + +func importCharmMetadataRelation(source interface{}, importVersion int) (charmMetadataRelation, error) { + fields := schema.Fields{ + "name": schema.String(), + "role": schema.String(), + "interface": schema.String(), + "optional": schema.Bool(), + "limit": schema.Int(), + "scope": schema.String(), + } + defaults := schema.Defaults{ + "optional": schema.Omit, + "limit": schema.Omit, + "scope": schema.Omit, + } + checker := schema.FieldMap(fields, defaults) + + coerced, err := checker.Coerce(source, nil) + if err != nil { + return charmMetadataRelation{}, errors.Annotate(err, "charmMetadataRelation schema check failed") + } + valid := coerced.(map[string]interface{}) + + return charmMetadataRelation{ + Name_: valid["name"].(string), + Role_: valid["role"].(string), + Interface_: valid["interface"].(string), + Optional_: valid["optional"].(bool), + Limit_: int(valid["limit"].(int64)), + Scope_: valid["scope"].(string), + }, nil +} + +func importCharmMetadataStorage(source interface{}, importVersion int) (charmMetadataStorage, error) { + fields := schema.Fields{ + "name": schema.String(), + "description": schema.String(), + "type": schema.String(), + "shared": schema.Bool(), + "readonly": schema.Bool(), + "count-min": schema.Int(), + "count-max": schema.Int(), + "minimum-size": schema.Int(), + "location": schema.String(), + "properties": schema.List(schema.String()), + } + defaults := schema.Defaults{ + "description": schema.Omit, + "shared": schema.Omit, + "readonly": schema.Omit, + "count-min": schema.Omit, + "count-max": schema.Omit, + "minimum-size": schema.Omit, + "location": schema.Omit, + "properties": schema.Omit, + } + checker := schema.FieldMap(fields, defaults) + + coerced, err := checker.Coerce(source, nil) + if err != nil { + return charmMetadataStorage{}, errors.Annotate(err, "charmMetadataStorage schema check failed") + } + valid := coerced.(map[string]interface{}) + + properties := make([]string, 0) + for _, v := range valid["properties"].([]interface{}) { + properties = append(properties, v.(string)) + } + + return charmMetadataStorage{ + Name_: valid["name"].(string), + Description_: valid["description"].(string), + Type_: valid["type"].(string), + Shared_: valid["shared"].(bool), + Readonly_: valid["readonly"].(bool), + CountMin_: int(valid["count-min"].(int64)), + CountMax_: int(valid["count-max"].(int64)), + MinimumSize_: int(valid["minimum-size"].(int64)), + Location_: valid["location"].(string), + Properties_: properties, + }, nil +} + +func importCharmMetadataDevice(source interface{}, importVersion int) (charmMetadataDevice, error) { + fields := schema.Fields{ + "name": schema.String(), + "description": schema.String(), + "type": schema.String(), + "count-min": schema.Int(), + "count-max": schema.Int(), + } + defaults := schema.Defaults{ + "description": schema.Omit, + "count-min": schema.Omit, + "count-max": schema.Omit, + } + checker := schema.FieldMap(fields, defaults) + + coerced, err := checker.Coerce(source, nil) + if err != nil { + return charmMetadataDevice{}, errors.Annotate(err, "charmMetadataDevice schema check failed") + } + valid := coerced.(map[string]interface{}) + + return charmMetadataDevice{ + Name_: valid["name"].(string), + Description_: valid["description"].(string), + Type_: valid["type"].(string), + CountMin_: int(valid["count-min"].(int64)), + CountMax_: int(valid["count-max"].(int64)), + }, nil +} + +func importCharmMetadataPayload(source interface{}, importVersion int) (charmMetadataPayload, error) { + fields := schema.Fields{ + "name": schema.String(), + "type": schema.String(), + } + defaults := schema.Defaults{} + checker := schema.FieldMap(fields, defaults) + + coerced, err := checker.Coerce(source, nil) + if err != nil { + return charmMetadataPayload{}, errors.Annotate(err, "charmMetadataPayload schema check failed") + } + valid := coerced.(map[string]interface{}) + + return charmMetadataPayload{ + Name_: valid["name"].(string), + Type_: valid["type"].(string), + }, nil +} + +func importCharmMetadataResource(source interface{}, importVersion int) (charmMetadataResource, error) { + fields := schema.Fields{ + "name": schema.String(), + "type": schema.String(), + "path": schema.String(), + "description": schema.String(), + } + defaults := schema.Defaults{ + "description": schema.Omit, + } + checker := schema.FieldMap(fields, defaults) + + coerced, err := checker.Coerce(source, nil) + if err != nil { + return charmMetadataResource{}, errors.Annotate(err, "charmMetadataResource schema check failed") + } + valid := coerced.(map[string]interface{}) + + return charmMetadataResource{ + Name_: valid["name"].(string), + Type_: valid["type"].(string), + Path_: valid["path"].(string), + Description_: valid["description"].(string), + }, nil +} + +func importCharmMetadataContainer(source interface{}, importVersion int) (charmMetadataContainer, error) { + fields := schema.Fields{ + "resource": schema.String(), + "mounts": schema.List(schema.Any()), + "uid": schema.Int(), + "gid": schema.Int(), + } + defaults := schema.Defaults{ + "uid": schema.Omit, + "gid": schema.Omit, + } + checker := schema.FieldMap(fields, defaults) + + coerced, err := checker.Coerce(source, nil) + if err != nil { + return charmMetadataContainer{}, errors.Annotate(err, "charmMetadataContainer schema check failed") + } + valid := coerced.(map[string]interface{}) + + mounts := make([]charmMetadataContainerMount, 0) + for _, v := range valid["mounts"].([]interface{}) { + mount, err := importCharmMetadataContainerMount(v, importVersion) + if err != nil { + return charmMetadataContainer{}, errors.Annotate(err, "mount") + } + mounts = append(mounts, mount) + } + + var uid *int + if valid["uid"] != nil { + uid = int64ToIntPtr(valid["uid"].(*int64)) + } + var gid *int + if valid["gid"] != nil { + uid = int64ToIntPtr(valid["gid"].(*int64)) + } + + return charmMetadataContainer{ + Resource_: valid["resource"].(string), + Mounts_: mounts, + Uid_: uid, + Gid_: gid, + }, nil +} + +func int64ToIntPtr(i *int64) *int { + if i == nil { + return nil + } + p := int(*i) + return &p +} + +func importCharmMetadataContainerMount(source interface{}, importVersion int) (charmMetadataContainerMount, error) { + fields := schema.Fields{ + "storage": schema.String(), + "location": schema.String(), + } + defaults := schema.Defaults{} + checker := schema.FieldMap(fields, defaults) + + coerced, err := checker.Coerce(source, nil) + if err != nil { + return charmMetadataContainerMount{}, errors.Annotate(err, "charmMetadataContainerMount schema check failed") + } + valid := coerced.(map[string]interface{}) + + return charmMetadataContainerMount{ + Storage_: valid["storage"].(string), + Location_: valid["location"].(string), + }, nil +} + +type charmMetadataRelation struct { + Name_ string `yaml:"name"` + Role_ string `yaml:"role"` + Interface_ string `yaml:"interface"` + Optional_ bool `yaml:"optional"` + Limit_ int `yaml:"limit"` + Scope_ string `yaml:"scope"` +} + +func (r charmMetadataRelation) Name() string { + return r.Name_ +} + +func (r charmMetadataRelation) Role() string { + return r.Role_ +} + +func (r charmMetadataRelation) Interface() string { + return r.Interface_ +} + +func (r charmMetadataRelation) Optional() bool { + return r.Optional_ +} + +func (r charmMetadataRelation) Limit() int { + return r.Limit_ +} + +func (r charmMetadataRelation) Scope() string { + return r.Scope_ +} + +type charmMetadataStorage struct { + Name_ string `yaml:"name"` + Description_ string `yaml:"description"` + Type_ string `yaml:"type"` + Shared_ bool `yaml:"shared"` + Readonly_ bool `yaml:"readonly"` + CountMin_ int `yaml:"count-min"` + CountMax_ int `yaml:"count-max"` + MinimumSize_ int `yaml:"minimum-size"` + Location_ string `yaml:"location"` + Properties_ []string `yaml:"properties"` +} + +func (s charmMetadataStorage) Name() string { + return s.Name_ +} + +func (s charmMetadataStorage) Description() string { + return s.Description_ +} + +func (s charmMetadataStorage) Type() string { + return s.Type_ +} + +func (s charmMetadataStorage) Shared() bool { + return s.Shared_ +} + +func (s charmMetadataStorage) Readonly() bool { + return s.Readonly_ +} + +func (s charmMetadataStorage) CountMin() int { + return s.CountMin_ +} + +func (s charmMetadataStorage) CountMax() int { + return s.CountMax_ +} + +func (s charmMetadataStorage) MinimumSize() int { + return s.MinimumSize_ +} + +func (s charmMetadataStorage) Location() string { + return s.Location_ +} + +func (s charmMetadataStorage) Properties() []string { + return s.Properties_ +} + +type charmMetadataDevice struct { + Name_ string `yaml:"name"` + Description_ string `yaml:"description"` + Type_ string `yaml:"type"` + CountMin_ int `yaml:"count-min"` + CountMax_ int `yaml:"count-max"` +} + +func (d charmMetadataDevice) Name() string { + return d.Name_ +} + +func (d charmMetadataDevice) Description() string { + return d.Description_ +} + +func (d charmMetadataDevice) Type() string { + return d.Type_ +} + +func (d charmMetadataDevice) CountMin() int { + return d.CountMin_ +} + +func (d charmMetadataDevice) CountMax() int { + return d.CountMax_ +} + +type charmMetadataPayload struct { + Name_ string `yaml:"name"` + Type_ string `yaml:"type"` +} + +func (p charmMetadataPayload) Name() string { + return p.Name_ +} + +func (p charmMetadataPayload) Type() string { + return p.Type_ +} + +type charmMetadataResource struct { + Name_ string `yaml:"name"` + Type_ string `yaml:"type"` + Path_ string `yaml:"path"` + Description_ string `yaml:"description"` +} + +func (r charmMetadataResource) Name() string { + return r.Name_ +} + +func (r charmMetadataResource) Type() string { + return r.Type_ +} + +func (r charmMetadataResource) Path() string { + return r.Path_ +} + +func (r charmMetadataResource) Description() string { + return r.Description_ +} + +type charmMetadataContainer struct { + Resource_ string `yaml:"resource"` + Mounts_ []charmMetadataContainerMount `yaml:"mounts"` + Uid_ *int `yaml:"uid,omitempty"` + Gid_ *int `yaml:"gid,omitempty"` +} + +func (c charmMetadataContainer) Resource() string { + return c.Resource_ +} + +func (c charmMetadataContainer) Mounts() []CharmMetadataContainerMount { + mounts := make([]CharmMetadataContainerMount, len(c.Mounts_)) + for i, m := range c.Mounts_ { + mounts[i] = m + } + return mounts +} + +func (c charmMetadataContainer) Uid() *int { + return c.Uid_ +} + +func (c charmMetadataContainer) Gid() *int { + return c.Gid_ +} + +type charmMetadataContainerMount struct { + Storage_ string `yaml:"storage"` + Location_ string `yaml:"location"` +} + +func (m charmMetadataContainerMount) Storage() string { + return m.Storage_ +} + +func (m charmMetadataContainerMount) Location() string { + return m.Location_ +} diff --git a/charmmetadata_test.go b/charmmetadata_test.go new file mode 100644 index 0000000..ae42060 --- /dev/null +++ b/charmmetadata_test.go @@ -0,0 +1,358 @@ +// Copyright 2024 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +package description + +import ( + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + "gopkg.in/yaml.v2" +) + +type CharmMetadataSerializationSuite struct { + SerializationSuite +} + +var _ = gc.Suite(&CharmMetadataSerializationSuite{}) + +func (s *CharmMetadataSerializationSuite) SetUpTest(c *gc.C) { + s.importName = "charmMetadata" + s.importFunc = func(m map[string]interface{}) (interface{}, error) { + return importCharmMetadata(m) + } +} + +func (s *CharmMetadataSerializationSuite) TestNewCharmMetadata(c *gc.C) { + args := CharmMetadataArgs{ + Name: "test-charm", + } + metadata := newCharmMetadata(args) + + c.Assert(metadata.Name(), gc.Equals, args.Name) +} + +func minimalCharmMetadataMap() map[interface{}]interface{} { + return map[interface{}]interface{}{ + "version": 1, + "name": "test-charm", + } +} + +func minimalCharmMetadataArgs() CharmMetadataArgs { + return CharmMetadataArgs{ + Name: "test-charm", + } +} + +func minimalCharmMetadata() *charmMetadata { + return newCharmMetadata(minimalCharmMetadataArgs()) +} + +func maximalCharmMetadataMap() map[interface{}]interface{} { + return map[interface{}]interface{}{ + "version": 1, + "name": "test-charm", + "summary": "A test charm", + "description": "A test charm for testing", + "subordinate": true, + "run-as": "root", + "assumes": "{}", + "min-juju-version": "4.0.0", + "categories": []interface{}{"test", "testing"}, + "tags": []interface{}{"foo", "bar"}, + "terms": []interface{}{"baz", "qux"}, + "extra-bindings": map[interface{}]interface{}{ + "db": "mysql", + }, + "relations": map[interface{}]interface{}{ + "db": map[interface{}]interface{}{ + "name": "db", + "role": "provider", + "interface": "mysql", + "optional": true, + "limit": 1, + "scope": "global", + }, + }, + "storage": map[interface{}]interface{}{ + "tmp": map[interface{}]interface{}{ + "name": "tmp", + "description": "Temporary storage", + "type": "filesystem", + "shared": true, + "readonly": true, + "count-min": 1, + "count-max": 2, + "minimum-size": 1024, + "location": "/tmp", + "properties": []interface{}{"foo", "bar"}, + }, + }, + "devices": map[interface{}]interface{}{ + "gpu": map[interface{}]interface{}{ + "name": "gpu", + "description": "Graphics card", + "type": "gpu", + "count-min": 1, + "count-max": 2, + }, + }, + "payloads": map[interface{}]interface{}{ + "logs": map[interface{}]interface{}{ + "name": "logs", + "type": "log", + }, + }, + "resources": map[interface{}]interface{}{ + "database": map[interface{}]interface{}{ + "name": "database", + "type": "file", + "description": "Database dump", + "path": "/var/lib/sqlite", + }, + }, + "containers": map[interface{}]interface{}{ + "postgres": map[interface{}]interface{}{ + "resource": "database", + "mounts": []interface{}{ + map[interface{}]interface{}{ + "storage": "tmp", + "location": "/var/lib/postgres", + }, + }, + }, + }, + } +} + +func maximalCharmMetadataArgs() CharmMetadataArgs { + return CharmMetadataArgs{ + Name: "test-charm", + Summary: "A test charm", + Description: "A test charm for testing", + Subordinate: true, + RunAs: "root", + Assumes: "{}", + MinJujuVersion: "4.0.0", + Categories: []string{"test", "testing"}, + Tags: []string{"foo", "bar"}, + Terms: []string{"baz", "qux"}, + ExtraBindings: map[string]string{ + "db": "mysql", + }, + Relations: map[string]CharmMetadataRelation{ + "db": charmMetadataRelation{ + Name_: "db", + Role_: "provider", + Interface_: "mysql", + Optional_: true, + Limit_: 1, + Scope_: "global", + }, + }, + Storage: map[string]CharmMetadataStorage{ + "tmp": charmMetadataStorage{ + Name_: "tmp", + Description_: "Temporary storage", + Type_: "filesystem", + Shared_: true, + Readonly_: true, + CountMin_: 1, + CountMax_: 2, + MinimumSize_: 1024, + Location_: "/tmp", + Properties_: []string{"foo", "bar"}, + }, + }, + Devices: map[string]CharmMetadataDevice{ + "gpu": charmMetadataDevice{ + Name_: "gpu", + Description_: "Graphics card", + Type_: "gpu", + CountMin_: 1, + CountMax_: 2, + }, + }, + Payloads: map[string]CharmMetadataPayload{ + "logs": charmMetadataPayload{ + Name_: "logs", + Type_: "log", + }, + }, + Resources: map[string]CharmMetadataResource{ + "database": charmMetadataResource{ + Name_: "database", + Type_: "file", + Description_: "Database dump", + Path_: "/var/lib/sqlite", + }, + }, + Containers: map[string]CharmMetadataContainer{ + "postgres": charmMetadataContainer{ + Resource_: "database", + Mounts_: []charmMetadataContainerMount{ + { + Storage_: "tmp", + Location_: "/var/lib/postgres", + }, + }, + }, + }, + } +} + +func maximalCharmMetadata() *charmMetadata { + return newCharmMetadata(maximalCharmMetadataArgs()) +} + +func partialCharmMetadataArgs() CharmMetadataArgs { + return CharmMetadataArgs{ + Name: "test-charm", + Summary: "A test charm", + Description: "A test charm for testing", + Subordinate: true, + RunAs: "root", + Assumes: "{}", + Tags: []string{"foo", "bar"}, + Terms: []string{"baz", "qux"}, + ExtraBindings: map[string]string{ + "db": "mysql", + }, + Relations: map[string]CharmMetadataRelation{ + "db": charmMetadataRelation{ + Name_: "db", + Role_: "provider", + Interface_: "mysql", + Scope_: "global", + }, + }, + Storage: map[string]CharmMetadataStorage{ + "tmp": charmMetadataStorage{ + Name_: "tmp", + Type_: "filesystem", + Shared_: true, + Readonly_: true, + CountMin_: 1, + Location_: "/tmp", + Properties_: []string{"foo", "bar"}, + }, + }, + Devices: map[string]CharmMetadataDevice{ + "gpu": charmMetadataDevice{ + Name_: "gpu", + Description_: "Graphics card", + CountMax_: 2, + }, + }, + Payloads: map[string]CharmMetadataPayload{ + "logs": charmMetadataPayload{ + Name_: "logs", + }, + }, + Resources: map[string]CharmMetadataResource{ + "database": charmMetadataResource{ + Name_: "database", + Type_: "file", + }, + }, + Containers: map[string]CharmMetadataContainer{ + "postgres": charmMetadataContainer{ + Resource_: "database", + Mounts_: []charmMetadataContainerMount{ + { + Storage_: "tmp", + }, + }, + }, + }, + } +} + +func partialCharmMetadata() *charmMetadata { + return newCharmMetadata(partialCharmMetadataArgs()) +} + +func (s *CharmMetadataSerializationSuite) TestMinimalMatches(c *gc.C) { + bytes, err := yaml.Marshal(minimalCharmMetadata()) + c.Assert(err, jc.ErrorIsNil) + + var source map[interface{}]interface{} + err = yaml.Unmarshal(bytes, &source) + c.Assert(err, jc.ErrorIsNil) + c.Assert(source, jc.DeepEquals, minimalCharmMetadataMap()) +} + +func (s *CharmMetadataSerializationSuite) TestMaximalMatches(c *gc.C) { + bytes, err := yaml.Marshal(maximalCharmMetadata()) + c.Assert(err, jc.ErrorIsNil) + + var source map[interface{}]interface{} + err = yaml.Unmarshal(bytes, &source) + c.Assert(err, jc.ErrorIsNil) + c.Assert(source, jc.DeepEquals, maximalCharmMetadataMap()) +} + +func (s *CharmMetadataSerializationSuite) TestMinimalParsingSerializedData(c *gc.C) { + initial := minimalCharmMetadata() + bytes, err := yaml.Marshal(initial) + c.Assert(err, jc.ErrorIsNil) + + var source map[string]interface{} + err = yaml.Unmarshal(bytes, &source) + c.Assert(err, jc.ErrorIsNil) + + instance, err := importCharmMetadata(source) + c.Assert(err, jc.ErrorIsNil) + c.Assert(instance, jc.DeepEquals, initial) +} + +func (s *CharmMetadataSerializationSuite) TestMaximalParsingSerializedData(c *gc.C) { + initial := maximalCharmMetadata() + bytes, err := yaml.Marshal(initial) + c.Assert(err, jc.ErrorIsNil) + + var source map[string]interface{} + err = yaml.Unmarshal(bytes, &source) + c.Assert(err, jc.ErrorIsNil) + + instance, err := importCharmMetadata(source) + c.Assert(err, jc.ErrorIsNil) + c.Assert(instance, jc.DeepEquals, initial) +} + +func (s *CharmMetadataSerializationSuite) TestPartialParsingSerializedData(c *gc.C) { + initial := partialCharmMetadata() + bytes, err := yaml.Marshal(initial) + c.Assert(err, jc.ErrorIsNil) + + var source map[string]interface{} + err = yaml.Unmarshal(bytes, &source) + c.Assert(err, jc.ErrorIsNil) + + instance, err := importCharmMetadata(source) + c.Assert(err, jc.ErrorIsNil) + c.Assert(instance, jc.DeepEquals, initial) +} + +func (s *CharmMetadataSerializationSuite) exportImportVersion(c *gc.C, origin_ *charmMetadata, version int) *charmMetadata { + origin_.Version_ = version + bytes, err := yaml.Marshal(origin_) + c.Assert(err, jc.ErrorIsNil) + + var source map[string]interface{} + err = yaml.Unmarshal(bytes, &source) + c.Assert(err, jc.ErrorIsNil) + + origin, err := importCharmMetadata(source) + c.Assert(err, jc.ErrorIsNil) + return origin +} + +func (s *CharmMetadataSerializationSuite) TestV1ParsingReturnsLatest(c *gc.C) { + args := maximalCharmMetadataArgs() + originV1 := newCharmMetadata(args) + + originLatest := *originV1 + originResult := s.exportImportVersion(c, originV1, 1) + c.Assert(*originResult, jc.DeepEquals, originLatest) +} diff --git a/charmorigin.go b/charmorigin.go index aff6164..130df90 100644 --- a/charmorigin.go +++ b/charmorigin.go @@ -13,7 +13,7 @@ import ( ) // CharmOriginArgs is an argument struct used to add information about the -// tools the agent is using to a Machine. +// charm origin. type CharmOriginArgs struct { Source string ID string @@ -35,9 +35,7 @@ func newCharmOrigin(args CharmOriginArgs) *charmOrigin { } } -// Keeping the charmOrigin with the machine code, because we hope -// that one day we will succeed in merging the unit agents with the -// machine agents. +// charmOrigin represents the origin of a charm. type charmOrigin struct { Version_ int `yaml:"version"` Source_ string `yaml:"source"` diff --git a/interfaces.go b/interfaces.go index 5dd6e0f..4b0787a 100644 --- a/interfaces.go +++ b/interfaces.go @@ -235,3 +235,86 @@ type CharmOrigin interface { Channel() string Platform() string } + +// CharmMetadata represents the metadata of a charm. +type CharmMetadata interface { + Name() string + Summary() string + Description() string + Subordinate() bool + MinJujuVersion() string + RunAs() string + Assumes() string + Relations() map[string]CharmMetadataRelation + ExtraBindings() map[string]string + Categories() []string + Tags() []string + Storage() map[string]CharmMetadataStorage + Devices() map[string]CharmMetadataDevice + Payloads() map[string]CharmMetadataPayload + Resources() map[string]CharmMetadataResource + Terms() []string + Containers() map[string]CharmMetadataContainer +} + +// CharmMetadataRelation represents a relation in the metadata of a charm. +type CharmMetadataRelation interface { + Name() string + Role() string + Interface() string + Optional() bool + Limit() int + Scope() string +} + +// CharmMetadataStorage represents a storage in the metadata of a charm. +type CharmMetadataStorage interface { + Name() string + Description() string + Type() string + Shared() bool + Readonly() bool + CountMin() int + CountMax() int + MinimumSize() int + Location() string + Properties() []string +} + +// CharmMetadataDevice represents a device in the metadata of a charm. +type CharmMetadataDevice interface { + Name() string + Description() string + Type() string + CountMin() int + CountMax() int +} + +// CharmMetadataPayload represents a payload in the metadata of a charm. +type CharmMetadataPayload interface { + Name() string + Type() string +} + +// CharmMetadataResource represents a resource in the metadata of a charm. +type CharmMetadataResource interface { + Name() string + Type() string + Path() string + Description() string +} + +// CharmMetadataContainer represents a container in the metadata of a charm. +type CharmMetadataContainer interface { + Resource() string + Mounts() []CharmMetadataContainerMount + Uid() *int + Gid() *int +} + +// CharmMetadataContainerMount represents a mount in the metadata of a charm +// container. +type CharmMetadataContainerMount interface { + Storage() string + Location() string +} diff --git a/model.go b/model.go index 312a96c..64ab53e 100644 --- a/model.go +++ b/model.go @@ -488,7 +488,7 @@ func (m *model) AddApplication(args ApplicationArgs) Application { func (m *model) setApplications(applicationList []*application) { m.Applications_ = applications{ - Version: 12, + Version: 13, Applications_: applicationList, } } From 943167a686781e4182b24bfe82de616bcf46f147 Mon Sep 17 00:00:00 2001 From: Simon Richardson Date: Thu, 25 Jul 2024 15:11:21 +0100 Subject: [PATCH 2/3] feat: add charm manifest Following on with metadata, this adds the manifest as well to help send information between the 3.6 model and a 4.0 model. --- application.go | 30 +++++++- application_test.go | 8 +++ charmmanifest.go | 161 ++++++++++++++++++++++++++++++++++++++++++ charmmanifest_test.go | 159 +++++++++++++++++++++++++++++++++++++++++ charmmetadata.go | 49 ++++++++++--- interfaces.go | 12 ++++ 6 files changed, 410 insertions(+), 9 deletions(-) create mode 100644 charmmanifest.go create mode 100644 charmmanifest_test.go diff --git a/application.go b/application.go index 0f7a320..6d3cf7a 100644 --- a/application.go +++ b/application.go @@ -64,6 +64,9 @@ type Application interface { CharmMetadata() CharmMetadata SetCharmMetadata(CharmMetadataArgs) + CharmManifest() CharmManifest + SetCharmManifest(CharmManifestArgs) + Tools() AgentTools SetTools(AgentToolsArgs) @@ -150,8 +153,9 @@ type application struct { // CharmOrigin fields CharmOrigin_ *charmOrigin `yaml:"charm-origin,omitempty"` - // CharmMetadata fields + // CharmMetadata and CharmManifest fields CharmMetadata_ *charmMetadata `yaml:"charm-metadata,omitempty"` + CharmManifest_ *charmManifest `yaml:"charm-manifest,omitempty"` } // ApplicationArgs is an argument struct used to add an application to the Model. @@ -536,6 +540,20 @@ func (a *application) SetCharmMetadata(args CharmMetadataArgs) { a.CharmMetadata_ = newCharmMetadata(args) } +// CharmManifest implements Application. +func (a *application) CharmManifest() CharmManifest { + // To avoid a typed nil, check before returning. + if a.CharmManifest_ == nil { + return nil + } + return a.CharmManifest_ +} + +// SetCharmManifest implements Application. +func (a *application) SetCharmManifest(args CharmManifestArgs) { + a.CharmManifest_ = newCharmManifest(args) +} + // Offers implements Application. func (a *application) Offers() []ApplicationOffer { if a.Offers_ == nil || len(a.Offers_.Offers) == 0 { @@ -792,7 +810,9 @@ func applicationV12Fields() (schema.Fields, schema.Defaults) { func applicationV13Fields() (schema.Fields, schema.Defaults) { fields, defaults := applicationV12Fields() fields["charm-metadata"] = schema.StringMap(schema.Any()) + fields["charm-manifest"] = schema.StringMap(schema.Any()) defaults["charm-metadata"] = schema.Omit + defaults["charm-manifest"] = schema.Omit return fields, defaults } @@ -978,6 +998,14 @@ func importApplication(fields schema.Fields, defaults schema.Defaults, importVer } result.CharmMetadata_ = charmMetadata } + + if charmManifestMap, ok := valid["charm-manifest"]; ok { + charmManifest, err := importCharmManifest(charmManifestMap.(map[string]interface{})) + if err != nil { + return nil, errors.Trace(err) + } + result.CharmManifest_ = charmManifest + } } result.importAnnotations(valid) diff --git a/application_test.go b/application_test.go index aded121..cd17945 100644 --- a/application_test.go +++ b/application_test.go @@ -66,6 +66,7 @@ func minimalApplicationMap() map[interface{}]interface{} { }, "charm-origin": minimalCharmOriginMap(), "charm-metadata": minimalCharmMetadataMap(), + "charm-manifest": minimalCharmManifestMap(), } } @@ -106,6 +107,7 @@ func minimalApplicationMapCAAS() map[interface{}]interface{} { result["operator-status"] = minimalStatusMap() result["charm-origin"] = minimalCharmOriginMap() result["charm-metadata"] = minimalCharmMetadataMap() + result["charm-manifest"] = minimalCharmManifestMap() return result } @@ -127,6 +129,7 @@ func minimalApplication(args ...ApplicationArgs) *application { } a.SetCharmOrigin(minimalCharmOriginArgs()) a.SetCharmMetadata(minimalCharmMetadataArgs()) + a.SetCharmManifest(minimalCharmManifestArgs()) return a } @@ -387,6 +390,7 @@ func (s *ApplicationSerializationSuite) TestV1ParsingReturnsLatest(c *gc.C) { appLatest.Offers_ = nil appLatest.CharmOrigin_ = nil appLatest.CharmMetadata_ = nil + appLatest.CharmManifest_ = nil appResult := s.exportImportVersion(c, appV1, 1) appLatest.Series_ = "" @@ -411,6 +415,7 @@ func (s *ApplicationSerializationSuite) TestV2ParsingReturnsLatest(c *gc.C) { appLatest.Offers_ = nil appLatest.CharmOrigin_ = nil appLatest.CharmMetadata_ = nil + appLatest.CharmManifest_ = nil appResult := s.exportImportVersion(c, appV1, 2) appLatest.Series_ = "" @@ -431,6 +436,7 @@ func (s *ApplicationSerializationSuite) TestV3ParsingReturnsLatest(c *gc.C) { appLatest.Offers_ = nil appLatest.CharmOrigin_ = nil appLatest.CharmMetadata_ = nil + appLatest.CharmManifest_ = nil appResult := s.exportImportVersion(c, appV2, 3) appLatest.Series_ = "" @@ -447,6 +453,7 @@ func (s *ApplicationSerializationSuite) TestV5ParsingReturnsLatest(c *gc.C) { appLatest.HasResources_ = false appLatest.CharmOrigin_ = nil appLatest.CharmMetadata_ = nil + appLatest.CharmManifest_ = nil appResult := s.exportImportVersion(c, appV5, 5) appLatest.Series_ = "" @@ -462,6 +469,7 @@ func (s *ApplicationSerializationSuite) TestV6ParsingReturnsLatest(c *gc.C) { appLatest := appV6 appLatest.CharmOrigin_ = nil appLatest.CharmMetadata_ = nil + appLatest.CharmManifest_ = nil appResult := s.exportImportVersion(c, appV6, 6) appLatest.Series_ = "" diff --git a/charmmanifest.go b/charmmanifest.go new file mode 100644 index 0000000..bb89552 --- /dev/null +++ b/charmmanifest.go @@ -0,0 +1,161 @@ +// Copyright 2020 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +package description + +import ( + "github.com/juju/errors" + "github.com/juju/schema" +) + +// CharmManifestArgs is an argument struct used to create a new +// CharmManifest. +type CharmManifestArgs struct { + Bases []CharmManifestBase +} + +func newCharmManifest(args CharmManifestArgs) *charmManifest { + var bases []charmManifestBase + if args.Bases != nil { + bases = make([]charmManifestBase, len(args.Bases)) + for i, b := range args.Bases { + bases[i] = charmManifestBase{ + Name_: b.Name(), + Channel_: b.Channel(), + Architectures_: b.Architectures(), + } + } + } + + return &charmManifest{ + Version_: 1, + Bases_: bases, + } +} + +// charmManifest represents the metadata of a charm. +type charmManifest struct { + Version_ int `yaml:"version"` + Bases_ []charmManifestBase `yaml:"bases"` +} + +// Bases returns the list of the base the charm supports. +func (m *charmManifest) Bases() []CharmManifestBase { + bases := make([]CharmManifestBase, len(m.Bases_)) + for i, b := range m.Bases_ { + bases[i] = b + } + return bases +} + +func importCharmManifest(source map[string]interface{}) (*charmManifest, error) { + version, err := getVersion(source) + if err != nil { + return nil, errors.Annotate(err, "charmManifest version schema check failed") + } + + importFunc, ok := charmManifestDeserializationFuncs[version] + if !ok { + return nil, errors.NotValidf("version %d", version) + } + + return importFunc(source) +} + +type charmManifestDeserializationFunc func(map[string]interface{}) (*charmManifest, error) + +var charmManifestDeserializationFuncs = map[int]charmManifestDeserializationFunc{ + 1: importCharmManifestV1, +} + +func importCharmManifestV1(source map[string]interface{}) (*charmManifest, error) { + return importCharmManifestVersion(source, 1) +} + +func importCharmManifestVersion(source map[string]interface{}, importVersion int) (*charmManifest, error) { + fields := schema.Fields{ + "bases": schema.List(schema.Any()), + } + defaults := schema.Defaults{ + "bases": schema.Omit, + } + checker := schema.FieldMap(fields, defaults) + + coerced, err := checker.Coerce(source, nil) + if err != nil { + return nil, errors.Annotatef(err, "charmOrigin v1 schema check failed") + } + valid := coerced.(map[string]interface{}) + + var bases []charmManifestBase + if valid["bases"] != nil { + bases = make([]charmManifestBase, len(valid["bases"].([]interface{}))) + for k, v := range valid["bases"].([]interface{}) { + var err error + bases[k], err = importCharmManifestBase(v, importVersion) + if err != nil { + return nil, errors.Annotate(err, "charmManifest bases schema check failed") + } + } + } + + return &charmManifest{ + Version_: 1, + Bases_: bases, + }, nil +} + +func importCharmManifestBase(source interface{}, importVersion int) (charmManifestBase, error) { + fields := schema.Fields{ + "name": schema.String(), + "channel": schema.String(), + "architectures": schema.List(schema.String()), + } + defaults := schema.Defaults{ + "name": schema.Omit, + "channel": schema.Omit, + "architectures": schema.Omit, + } + checker := schema.FieldMap(fields, defaults) + + coerced, err := checker.Coerce(source, nil) + if err != nil { + return charmManifestBase{}, errors.Annotate(err, "charmManifestBase schema check failed") + } + valid := coerced.(map[string]interface{}) + + var architectures []string + if valid["architectures"] != nil { + architectures = make([]string, len(valid["architectures"].([]interface{}))) + for i, a := range valid["architectures"].([]interface{}) { + architectures[i] = a.(string) + } + } + + return charmManifestBase{ + Name_: valid["name"].(string), + Channel_: valid["channel"].(string), + Architectures_: architectures, + }, nil +} + +type charmManifestBase struct { + Name_ string `yaml:"name"` + Channel_ string `yaml:"channel"` + Architectures_ []string `yaml:"architectures"` +} + +// Name returns the name of the base. +func (r charmManifestBase) Name() string { + return r.Name_ +} + +// Channel returns the channel of the base. +func (r charmManifestBase) Channel() string { + return r.Channel_ +} + +// Architectures returns the architectures of the base. +func (r charmManifestBase) Architectures() []string { + return r.Architectures_ +} diff --git a/charmmanifest_test.go b/charmmanifest_test.go new file mode 100644 index 0000000..ff3a839 --- /dev/null +++ b/charmmanifest_test.go @@ -0,0 +1,159 @@ +// Copyright 2024 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +package description + +import ( + jc "github.com/juju/testing/checkers" + gc "gopkg.in/check.v1" + "gopkg.in/yaml.v2" +) + +type CharmManifestSerializationSuite struct { + SerializationSuite +} + +var _ = gc.Suite(&CharmManifestSerializationSuite{}) + +func (s *CharmManifestSerializationSuite) SetUpTest(c *gc.C) { + s.importName = "charmManifest" + s.importFunc = func(m map[string]interface{}) (interface{}, error) { + return importCharmManifest(m) + } +} + +func (s *CharmManifestSerializationSuite) TestNewCharmManifest(c *gc.C) { + args := CharmManifestArgs{ + Bases: []CharmManifestBase{ + charmManifestBase{ + Name_: "ubuntu", + Channel_: "22.04", + Architectures_: []string{"amd64"}, + }, + }, + } + metadata := newCharmManifest(args) + + c.Assert(metadata.Bases(), gc.DeepEquals, []CharmManifestBase{ + charmManifestBase{ + Name_: "ubuntu", + Channel_: "22.04", + Architectures_: []string{"amd64"}, + }, + }) +} + +func minimalCharmManifestMap() map[interface{}]interface{} { + return map[interface{}]interface{}{ + "version": 1, + "bases": []interface{}{}, + } +} + +func minimalCharmManifestArgs() CharmManifestArgs { + return CharmManifestArgs{} +} + +func minimalCharmManifest() *charmManifest { + return newCharmManifest(minimalCharmManifestArgs()) +} + +func maximalCharmManifestMap() map[interface{}]interface{} { + return map[interface{}]interface{}{ + "version": 1, + "bases": []interface{}{ + map[interface{}]interface{}{ + "name": "ubuntu", + "channel": "22.04", + "architectures": []interface{}{"amd64"}, + }, + }, + } +} + +func maximalCharmManifestArgs() CharmManifestArgs { + return CharmManifestArgs{ + Bases: []CharmManifestBase{ + charmManifestBase{ + Name_: "ubuntu", + Channel_: "22.04", + Architectures_: []string{"amd64"}, + }, + }, + } +} + +func maximalCharmManifest() *charmManifest { + return newCharmManifest(maximalCharmManifestArgs()) +} + +func (s *CharmManifestSerializationSuite) TestMinimalMatches(c *gc.C) { + bytes, err := yaml.Marshal(minimalCharmManifest()) + c.Assert(err, jc.ErrorIsNil) + + var source map[interface{}]interface{} + err = yaml.Unmarshal(bytes, &source) + c.Assert(err, jc.ErrorIsNil) + c.Assert(source, jc.DeepEquals, minimalCharmManifestMap()) +} + +func (s *CharmManifestSerializationSuite) TestMaximalMatches(c *gc.C) { + bytes, err := yaml.Marshal(maximalCharmManifest()) + c.Assert(err, jc.ErrorIsNil) + + var source map[interface{}]interface{} + err = yaml.Unmarshal(bytes, &source) + c.Assert(err, jc.ErrorIsNil) + c.Assert(source, jc.DeepEquals, maximalCharmManifestMap()) +} + +func (s *CharmManifestSerializationSuite) TestMinimalParsingSerializedData(c *gc.C) { + initial := minimalCharmManifest() + bytes, err := yaml.Marshal(initial) + c.Assert(err, jc.ErrorIsNil) + + var source map[string]interface{} + err = yaml.Unmarshal(bytes, &source) + c.Assert(err, jc.ErrorIsNil) + + instance, err := importCharmManifest(source) + c.Assert(err, jc.ErrorIsNil) + c.Assert(instance, jc.DeepEquals, initial) +} + +func (s *CharmManifestSerializationSuite) TestMaximalParsingSerializedData(c *gc.C) { + initial := maximalCharmManifest() + bytes, err := yaml.Marshal(initial) + c.Assert(err, jc.ErrorIsNil) + + var source map[string]interface{} + err = yaml.Unmarshal(bytes, &source) + c.Assert(err, jc.ErrorIsNil) + + instance, err := importCharmManifest(source) + c.Assert(err, jc.ErrorIsNil) + c.Assert(instance, jc.DeepEquals, initial) +} + +func (s *CharmManifestSerializationSuite) exportImportVersion(c *gc.C, origin_ *charmManifest, version int) *charmManifest { + origin_.Version_ = version + bytes, err := yaml.Marshal(origin_) + c.Assert(err, jc.ErrorIsNil) + + var source map[string]interface{} + err = yaml.Unmarshal(bytes, &source) + c.Assert(err, jc.ErrorIsNil) + + origin, err := importCharmManifest(source) + c.Assert(err, jc.ErrorIsNil) + return origin +} + +func (s *CharmManifestSerializationSuite) TestV1ParsingReturnsLatest(c *gc.C) { + args := maximalCharmManifestArgs() + originV1 := newCharmManifest(args) + + originLatest := *originV1 + originResult := s.exportImportVersion(c, originV1, 1) + c.Assert(*originResult, jc.DeepEquals, originLatest) +} diff --git a/charmmetadata.go b/charmmetadata.go index af69686..1ba9ac7 100644 --- a/charmmetadata.go +++ b/charmmetadata.go @@ -697,14 +697,6 @@ func importCharmMetadataContainer(source interface{}, importVersion int) (charmM }, nil } -func int64ToIntPtr(i *int64) *int { - if i == nil { - return nil - } - p := int(*i) - return &p -} - func importCharmMetadataContainerMount(source interface{}, importVersion int) (charmMetadataContainerMount, error) { fields := schema.Fields{ "storage": schema.String(), @@ -734,26 +726,32 @@ type charmMetadataRelation struct { Scope_ string `yaml:"scope"` } +// Name returns the name of the relation. func (r charmMetadataRelation) Name() string { return r.Name_ } +// Role returns the role of the relation. func (r charmMetadataRelation) Role() string { return r.Role_ } +// Interface returns the interface of the relation. func (r charmMetadataRelation) Interface() string { return r.Interface_ } +// Optional returns whether the relation is optional. func (r charmMetadataRelation) Optional() bool { return r.Optional_ } +// Limit returns the limit of the relation. func (r charmMetadataRelation) Limit() int { return r.Limit_ } +// Scope returns the scope of the relation. func (r charmMetadataRelation) Scope() string { return r.Scope_ } @@ -771,42 +769,52 @@ type charmMetadataStorage struct { Properties_ []string `yaml:"properties"` } +// Name returns the name of the storage. func (s charmMetadataStorage) Name() string { return s.Name_ } +// Description returns the description of the storage. func (s charmMetadataStorage) Description() string { return s.Description_ } +// Type returns the type of the storage. func (s charmMetadataStorage) Type() string { return s.Type_ } +// Shared returns whether the storage is shared. func (s charmMetadataStorage) Shared() bool { return s.Shared_ } +// Readonly returns whether the storage is readonly. func (s charmMetadataStorage) Readonly() bool { return s.Readonly_ } +// CountMin returns the minimum count of the storage. func (s charmMetadataStorage) CountMin() int { return s.CountMin_ } +// CountMax returns the maximum count of the storage. func (s charmMetadataStorage) CountMax() int { return s.CountMax_ } +// MinimumSize returns the minimum size of the storage. func (s charmMetadataStorage) MinimumSize() int { return s.MinimumSize_ } +// Location returns the location of the storage. func (s charmMetadataStorage) Location() string { return s.Location_ } +// Properties returns the properties of the storage. func (s charmMetadataStorage) Properties() []string { return s.Properties_ } @@ -819,22 +827,27 @@ type charmMetadataDevice struct { CountMax_ int `yaml:"count-max"` } +// Name returns the name of the device. func (d charmMetadataDevice) Name() string { return d.Name_ } +// Description returns the description of the device. func (d charmMetadataDevice) Description() string { return d.Description_ } +// Type returns the type of the device. func (d charmMetadataDevice) Type() string { return d.Type_ } +// CountMin returns the minimum count of the device. func (d charmMetadataDevice) CountMin() int { return d.CountMin_ } +// CountMax returns the maximum count of the device. func (d charmMetadataDevice) CountMax() int { return d.CountMax_ } @@ -844,10 +857,12 @@ type charmMetadataPayload struct { Type_ string `yaml:"type"` } +// Name returns the name of the payload. func (p charmMetadataPayload) Name() string { return p.Name_ } +// Type returns the type of the payload. func (p charmMetadataPayload) Type() string { return p.Type_ } @@ -859,18 +874,22 @@ type charmMetadataResource struct { Description_ string `yaml:"description"` } +// Name returns the name of the resource. func (r charmMetadataResource) Name() string { return r.Name_ } +// Type returns the type of the resource. func (r charmMetadataResource) Type() string { return r.Type_ } +// Path returns the path of the resource. func (r charmMetadataResource) Path() string { return r.Path_ } +// Description returns the description of the resource. func (r charmMetadataResource) Description() string { return r.Description_ } @@ -882,10 +901,12 @@ type charmMetadataContainer struct { Gid_ *int `yaml:"gid,omitempty"` } +// Resource returns the resource of the container. func (c charmMetadataContainer) Resource() string { return c.Resource_ } +// Mounts returns the mounts of the container. func (c charmMetadataContainer) Mounts() []CharmMetadataContainerMount { mounts := make([]CharmMetadataContainerMount, len(c.Mounts_)) for i, m := range c.Mounts_ { @@ -894,10 +915,12 @@ func (c charmMetadataContainer) Mounts() []CharmMetadataContainerMount { return mounts } +// Uid returns the uid of the container. func (c charmMetadataContainer) Uid() *int { return c.Uid_ } +// Gid returns the gid of the container. func (c charmMetadataContainer) Gid() *int { return c.Gid_ } @@ -907,10 +930,20 @@ type charmMetadataContainerMount struct { Location_ string `yaml:"location"` } +// Storage returns the storage of the mount. func (m charmMetadataContainerMount) Storage() string { return m.Storage_ } +// Location returns the location of the mount. func (m charmMetadataContainerMount) Location() string { return m.Location_ } + +func int64ToIntPtr(i *int64) *int { + if i == nil { + return nil + } + p := int(*i) + return &p +} diff --git a/interfaces.go b/interfaces.go index 4b0787a..c08884c 100644 --- a/interfaces.go +++ b/interfaces.go @@ -318,3 +318,15 @@ type CharmMetadataContainerMount interface { Storage() string Location() string } + +// CharmManifest represents the manifest of a charm. +type CharmManifest interface { + Bases() []CharmManifestBase +} + +// CharmManifestBase represents the metadata of a charm base. +type CharmManifestBase interface { + Name() string + Channel() string + Architectures() []string +} From 003ba4789030523a7e69120142e293263dfb48ab Mon Sep 17 00:00:00 2001 From: Simon Richardson Date: Fri, 26 Jul 2024 12:12:22 +0100 Subject: [PATCH 3/3] fix: expose each relation type To ensure that we don't get name collisions on the map, expose all values. We don't need to optimise this path. --- charmmetadata.go | 124 +++++++++++++++++++++++++++++++++++------- charmmetadata_test.go | 62 ++++++++++++++++++++- interfaces.go | 4 +- 3 files changed, 165 insertions(+), 25 deletions(-) diff --git a/charmmetadata.go b/charmmetadata.go index 1ba9ac7..81c4505 100644 --- a/charmmetadata.go +++ b/charmmetadata.go @@ -18,7 +18,9 @@ type CharmMetadataArgs struct { MinJujuVersion string RunAs string Assumes string - Relations map[string]CharmMetadataRelation + Provides map[string]CharmMetadataRelation + Peers map[string]CharmMetadataRelation + Requires map[string]CharmMetadataRelation ExtraBindings map[string]string Categories []string Tags []string @@ -31,11 +33,41 @@ type CharmMetadataArgs struct { } func newCharmMetadata(args CharmMetadataArgs) *charmMetadata { - var relations map[string]charmMetadataRelation - if args.Relations != nil { - relations = make(map[string]charmMetadataRelation, len(args.Relations)) - for k, v := range args.Relations { - relations[k] = charmMetadataRelation{ + var provides map[string]charmMetadataRelation + if args.Provides != nil { + provides = make(map[string]charmMetadataRelation, len(args.Provides)) + for k, v := range args.Provides { + provides[k] = charmMetadataRelation{ + Name_: v.Name(), + Role_: v.Role(), + Interface_: v.Interface(), + Optional_: v.Optional(), + Limit_: v.Limit(), + Scope_: v.Scope(), + } + } + } + + var peers map[string]charmMetadataRelation + if args.Peers != nil { + peers = make(map[string]charmMetadataRelation, len(args.Peers)) + for k, v := range args.Peers { + peers[k] = charmMetadataRelation{ + Name_: v.Name(), + Role_: v.Role(), + Interface_: v.Interface(), + Optional_: v.Optional(), + Limit_: v.Limit(), + Scope_: v.Scope(), + } + } + } + + var requires map[string]charmMetadataRelation + if args.Requires != nil { + requires = make(map[string]charmMetadataRelation, len(args.Requires)) + for k, v := range args.Requires { + requires[k] = charmMetadataRelation{ Name_: v.Name(), Role_: v.Role(), Interface_: v.Interface(), @@ -132,7 +164,9 @@ func newCharmMetadata(args CharmMetadataArgs) *charmMetadata { MinJujuVersion_: args.MinJujuVersion, RunAs_: args.RunAs, Assumes_: args.Assumes, - Relations_: relations, + Provides_: provides, + Requires_: requires, + Peers_: peers, ExtraBindings_: args.ExtraBindings, Categories_: args.Categories, Tags_: args.Tags, @@ -155,7 +189,9 @@ type charmMetadata struct { MinJujuVersion_ string `yaml:"min-juju-version,omitempty"` RunAs_ string `yaml:"run-as,omitempty"` Assumes_ string `yaml:"assumes,omitempty"` - Relations_ map[string]charmMetadataRelation `yaml:"relations,omitempty"` + Provides_ map[string]charmMetadataRelation `yaml:"provides,omitempty"` + Requires_ map[string]charmMetadataRelation `yaml:"requires,omitempty"` + Peers_ map[string]charmMetadataRelation `yaml:"peers,omitempty"` ExtraBindings_ map[string]string `yaml:"extra-bindings,omitempty"` Categories_ []string `yaml:"categories,omitempty"` Tags_ []string `yaml:"tags,omitempty"` @@ -202,10 +238,28 @@ func (m *charmMetadata) Assumes() string { return m.Assumes_ } -// Relations returns the relations of the charm. -func (m *charmMetadata) Relations() map[string]CharmMetadataRelation { - relations := make(map[string]CharmMetadataRelation, len(m.Relations_)) - for k, v := range m.Relations_ { +// Provides returns the relations of the charm. +func (m *charmMetadata) Provides() map[string]CharmMetadataRelation { + relations := make(map[string]CharmMetadataRelation, len(m.Provides_)) + for k, v := range m.Provides_ { + relations[k] = v + } + return relations +} + +// Requires returns the relations of the charm. +func (m *charmMetadata) Requires() map[string]CharmMetadataRelation { + relations := make(map[string]CharmMetadataRelation, len(m.Requires_)) + for k, v := range m.Requires_ { + relations[k] = v + } + return relations +} + +// Peers returns the relations of the charm. +func (m *charmMetadata) Peers() map[string]CharmMetadataRelation { + relations := make(map[string]CharmMetadataRelation, len(m.Peers_)) + for k, v := range m.Peers_ { relations[k] = v } return relations @@ -309,7 +363,9 @@ func importCharmMetadataVersion(source map[string]interface{}, importVersion int "min-juju-version": schema.String(), "run-as": schema.String(), "assumes": schema.String(), - "relations": schema.StringMap(schema.Any()), + "provides": schema.StringMap(schema.Any()), + "requires": schema.StringMap(schema.Any()), + "peers": schema.StringMap(schema.Any()), "extra-bindings": schema.StringMap(schema.String()), "categories": schema.List(schema.String()), "tags": schema.List(schema.String()), @@ -327,7 +383,9 @@ func importCharmMetadataVersion(source map[string]interface{}, importVersion int "min-juju-version": schema.Omit, "run-as": schema.Omit, "assumes": schema.Omit, - "relations": schema.Omit, + "provides": schema.Omit, + "requires": schema.Omit, + "peers": schema.Omit, "extra-bindings": schema.Omit, "categories": schema.Omit, "tags": schema.Omit, @@ -346,13 +404,35 @@ func importCharmMetadataVersion(source map[string]interface{}, importVersion int } valid := coerced.(map[string]interface{}) - var relations map[string]charmMetadataRelation - if valid["relations"] != nil { - relations = make(map[string]charmMetadataRelation) - for k, v := range valid["relations"].(map[string]interface{}) { + var provides map[string]charmMetadataRelation + if valid["provides"] != nil { + provides = make(map[string]charmMetadataRelation) + for k, v := range valid["provides"].(map[string]interface{}) { + var err error + if provides[k], err = importCharmMetadataRelation(v, importVersion); err != nil { + return nil, errors.Annotatef(err, "provides relation %q", k) + } + } + } + + var requires map[string]charmMetadataRelation + if valid["requires"] != nil { + requires = make(map[string]charmMetadataRelation) + for k, v := range valid["requires"].(map[string]interface{}) { + var err error + if requires[k], err = importCharmMetadataRelation(v, importVersion); err != nil { + return nil, errors.Annotatef(err, "requires relation %q", k) + } + } + } + + var peers map[string]charmMetadataRelation + if valid["peers"] != nil { + peers = make(map[string]charmMetadataRelation) + for k, v := range valid["peers"].(map[string]interface{}) { var err error - if relations[k], err = importCharmMetadataRelation(v, importVersion); err != nil { - return nil, errors.Annotatef(err, "relation %q", k) + if peers[k], err = importCharmMetadataRelation(v, importVersion); err != nil { + return nil, errors.Annotatef(err, "peers relation %q", k) } } } @@ -481,7 +561,9 @@ func importCharmMetadataVersion(source map[string]interface{}, importVersion int MinJujuVersion_: minJujuVersion, RunAs_: runAs, Assumes_: assumes, - Relations_: relations, + Provides_: provides, + Requires_: requires, + Peers_: peers, ExtraBindings_: extraBindings, Categories_: categories, Tags_: tags, diff --git a/charmmetadata_test.go b/charmmetadata_test.go index ae42060..f445422 100644 --- a/charmmetadata_test.go +++ b/charmmetadata_test.go @@ -64,7 +64,7 @@ func maximalCharmMetadataMap() map[interface{}]interface{} { "extra-bindings": map[interface{}]interface{}{ "db": "mysql", }, - "relations": map[interface{}]interface{}{ + "provides": map[interface{}]interface{}{ "db": map[interface{}]interface{}{ "name": "db", "role": "provider", @@ -74,6 +74,26 @@ func maximalCharmMetadataMap() map[interface{}]interface{} { "scope": "global", }, }, + "requires": map[interface{}]interface{}{ + "db": map[interface{}]interface{}{ + "name": "db", + "role": "require", + "interface": "mysql", + "optional": true, + "limit": 1, + "scope": "global", + }, + }, + "peers": map[interface{}]interface{}{ + "db": map[interface{}]interface{}{ + "name": "db", + "role": "peer", + "interface": "mysql", + "optional": true, + "limit": 1, + "scope": "global", + }, + }, "storage": map[interface{}]interface{}{ "tmp": map[interface{}]interface{}{ "name": "tmp", @@ -140,7 +160,7 @@ func maximalCharmMetadataArgs() CharmMetadataArgs { ExtraBindings: map[string]string{ "db": "mysql", }, - Relations: map[string]CharmMetadataRelation{ + Provides: map[string]CharmMetadataRelation{ "db": charmMetadataRelation{ Name_: "db", Role_: "provider", @@ -150,6 +170,26 @@ func maximalCharmMetadataArgs() CharmMetadataArgs { Scope_: "global", }, }, + Requires: map[string]CharmMetadataRelation{ + "db": charmMetadataRelation{ + Name_: "db", + Role_: "require", + Interface_: "mysql", + Optional_: true, + Limit_: 1, + Scope_: "global", + }, + }, + Peers: map[string]CharmMetadataRelation{ + "db": charmMetadataRelation{ + Name_: "db", + Role_: "peer", + Interface_: "mysql", + Optional_: true, + Limit_: 1, + Scope_: "global", + }, + }, Storage: map[string]CharmMetadataStorage{ "tmp": charmMetadataStorage{ Name_: "tmp", @@ -218,7 +258,7 @@ func partialCharmMetadataArgs() CharmMetadataArgs { ExtraBindings: map[string]string{ "db": "mysql", }, - Relations: map[string]CharmMetadataRelation{ + Provides: map[string]CharmMetadataRelation{ "db": charmMetadataRelation{ Name_: "db", Role_: "provider", @@ -226,6 +266,22 @@ func partialCharmMetadataArgs() CharmMetadataArgs { Scope_: "global", }, }, + Requires: map[string]CharmMetadataRelation{ + "db": charmMetadataRelation{ + Name_: "db", + Role_: "require", + Interface_: "mysql", + Scope_: "global", + }, + }, + Peers: map[string]CharmMetadataRelation{ + "db": charmMetadataRelation{ + Name_: "db", + Role_: "peer", + Interface_: "mysql", + Scope_: "global", + }, + }, Storage: map[string]CharmMetadataStorage{ "tmp": charmMetadataStorage{ Name_: "tmp", diff --git a/interfaces.go b/interfaces.go index c08884c..987884f 100644 --- a/interfaces.go +++ b/interfaces.go @@ -245,7 +245,9 @@ type CharmMetadata interface { MinJujuVersion() string RunAs() string Assumes() string - Relations() map[string]CharmMetadataRelation + Provides() map[string]CharmMetadataRelation + Requires() map[string]CharmMetadataRelation + Peers() map[string]CharmMetadataRelation ExtraBindings() map[string]string Categories() []string Tags() []string