diff --git a/application.go b/application.go index 3086d7b..6d3cf7a 100644 --- a/application.go +++ b/application.go @@ -61,6 +61,12 @@ type Application interface { CharmOrigin() CharmOrigin SetCharmOrigin(CharmOriginArgs) + CharmMetadata() CharmMetadata + SetCharmMetadata(CharmMetadataArgs) + + CharmManifest() CharmManifest + SetCharmManifest(CharmManifestArgs) + Tools() AgentTools SetTools(AgentToolsArgs) @@ -147,6 +153,9 @@ type application struct { // CharmOrigin fields CharmOrigin_ *charmOrigin `yaml:"charm-origin,omitempty"` + // 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. @@ -517,6 +526,34 @@ 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) +} + +// 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 { @@ -639,6 +676,7 @@ var applicationDeserializationFuncs = map[int]applicationDeserializationFunc{ 10: importApplicationV10, 11: importApplicationV11, 12: importApplicationV12, + 13: importApplicationV13, } func applicationV1Fields() (schema.Fields, schema.Defaults) { @@ -769,6 +807,15 @@ 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()) + fields["charm-manifest"] = schema.StringMap(schema.Any()) + defaults["charm-metadata"] = schema.Omit + defaults["charm-manifest"] = schema.Omit + return fields, defaults +} + func importApplicationV1(source map[string]interface{}) (*application, error) { fields, defaults := applicationV1Fields() return importApplication(fields, defaults, 1, source) @@ -829,6 +876,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 +990,24 @@ 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 + } + + 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) if err := result.importStatusHistory(valid); err != nil { diff --git a/application_test.go b/application_test.go index 208b9b2..cd17945 100644 --- a/application_test.go +++ b/application_test.go @@ -64,7 +64,9 @@ func minimalApplicationMap() map[interface{}]interface{} { minimalUnitMap(), }, }, - "charm-origin": minimalCharmOriginMap(), + "charm-origin": minimalCharmOriginMap(), + "charm-metadata": minimalCharmMetadataMap(), + "charm-manifest": minimalCharmManifestMap(), } } @@ -104,6 +106,8 @@ func minimalApplicationMapCAAS() map[interface{}]interface{} { result["tools"] = minimalAgentToolsMap() result["operator-status"] = minimalStatusMap() result["charm-origin"] = minimalCharmOriginMap() + result["charm-metadata"] = minimalCharmMetadataMap() + result["charm-manifest"] = minimalCharmManifestMap() return result } @@ -124,6 +128,8 @@ func minimalApplication(args ...ApplicationArgs) *application { u.SetTools(minimalAgentToolsArgs()) } a.SetCharmOrigin(minimalCharmOriginArgs()) + a.SetCharmMetadata(minimalCharmMetadataArgs()) + a.SetCharmManifest(minimalCharmManifestArgs()) return a } @@ -362,7 +368,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 +389,8 @@ func (s *ApplicationSerializationSuite) TestV1ParsingReturnsLatest(c *gc.C) { appLatest.OperatorStatus_ = nil appLatest.Offers_ = nil appLatest.CharmOrigin_ = nil + appLatest.CharmMetadata_ = nil + appLatest.CharmManifest_ = nil appResult := s.exportImportVersion(c, appV1, 1) appLatest.Series_ = "" @@ -406,6 +414,8 @@ func (s *ApplicationSerializationSuite) TestV2ParsingReturnsLatest(c *gc.C) { appLatest.OperatorStatus_ = nil appLatest.Offers_ = nil appLatest.CharmOrigin_ = nil + appLatest.CharmMetadata_ = nil + appLatest.CharmManifest_ = nil appResult := s.exportImportVersion(c, appV1, 2) appLatest.Series_ = "" @@ -425,6 +435,8 @@ func (s *ApplicationSerializationSuite) TestV3ParsingReturnsLatest(c *gc.C) { appLatest.OperatorStatus_ = nil appLatest.Offers_ = nil appLatest.CharmOrigin_ = nil + appLatest.CharmMetadata_ = nil + appLatest.CharmManifest_ = nil appResult := s.exportImportVersion(c, appV2, 3) appLatest.Series_ = "" @@ -440,6 +452,8 @@ func (s *ApplicationSerializationSuite) TestV5ParsingReturnsLatest(c *gc.C) { appLatest := appV5 appLatest.HasResources_ = false appLatest.CharmOrigin_ = nil + appLatest.CharmMetadata_ = nil + appLatest.CharmManifest_ = nil appResult := s.exportImportVersion(c, appV5, 5) appLatest.Series_ = "" @@ -454,6 +468,8 @@ func (s *ApplicationSerializationSuite) TestV6ParsingReturnsLatest(c *gc.C) { // Make an app with fields not in v6 removed. 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 new file mode 100644 index 0000000..81c4505 --- /dev/null +++ b/charmmetadata.go @@ -0,0 +1,1031 @@ +// 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 + Provides map[string]CharmMetadataRelation + Peers map[string]CharmMetadataRelation + Requires 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 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(), + 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, + Provides_: provides, + Requires_: requires, + Peers_: peers, + 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"` + 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"` + 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_ +} + +// 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 +} + +// 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(), + "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()), + "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, + "provides": schema.Omit, + "requires": schema.Omit, + "peers": 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 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 peers[k], err = importCharmMetadataRelation(v, importVersion); err != nil { + return nil, errors.Annotatef(err, "peers 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, + Provides_: provides, + Requires_: requires, + Peers_: peers, + 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 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"` +} + +// 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_ +} + +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"` +} + +// 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_ +} + +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"` +} + +// 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_ +} + +type charmMetadataPayload struct { + Name_ string `yaml:"name"` + 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_ +} + +type charmMetadataResource struct { + Name_ string `yaml:"name"` + Type_ string `yaml:"type"` + Path_ string `yaml:"path"` + 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_ +} + +type charmMetadataContainer struct { + Resource_ string `yaml:"resource"` + Mounts_ []charmMetadataContainerMount `yaml:"mounts"` + Uid_ *int `yaml:"uid,omitempty"` + 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_ { + mounts[i] = m + } + 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_ +} + +type charmMetadataContainerMount struct { + Storage_ string `yaml:"storage"` + 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/charmmetadata_test.go b/charmmetadata_test.go new file mode 100644 index 0000000..f445422 --- /dev/null +++ b/charmmetadata_test.go @@ -0,0 +1,414 @@ +// 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", + }, + "provides": map[interface{}]interface{}{ + "db": map[interface{}]interface{}{ + "name": "db", + "role": "provider", + "interface": "mysql", + "optional": true, + "limit": 1, + "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", + "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", + }, + Provides: map[string]CharmMetadataRelation{ + "db": charmMetadataRelation{ + Name_: "db", + Role_: "provider", + Interface_: "mysql", + Optional_: true, + Limit_: 1, + 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", + 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", + }, + Provides: map[string]CharmMetadataRelation{ + "db": charmMetadataRelation{ + Name_: "db", + Role_: "provider", + Interface_: "mysql", + 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", + 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 721d651..b66b341 100644 --- a/interfaces.go +++ b/interfaces.go @@ -238,3 +238,100 @@ 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 + Provides() map[string]CharmMetadataRelation + Requires() map[string]CharmMetadataRelation + Peers() 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 +} + +// 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 +} diff --git a/model.go b/model.go index 9258a1d..820deb3 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, } }