From 9a3c8100855d0f1b2a4cdd7bf7d30d5e95a70f0f Mon Sep 17 00:00:00 2001 From: Simon Richardson Date: Mon, 29 Jul 2024 17:38:40 +0100 Subject: [PATCH 1/3] feat: import/export charm actions Charm actions can be used to ensure RI (referential integrity) when constructing a charm/application in Juju 4.0. --- application.go | 25 +++++++ application_test.go | 8 ++ charmactions.go | 160 ++++++++++++++++++++++++++++++++++++++++ charmactions_test.go | 171 +++++++++++++++++++++++++++++++++++++++++++ charmmanifest.go | 4 +- interfaces.go | 13 ++++ 6 files changed, 379 insertions(+), 2 deletions(-) create mode 100644 charmactions.go create mode 100644 charmactions_test.go diff --git a/application.go b/application.go index 6d3cf7a..4cc7620 100644 --- a/application.go +++ b/application.go @@ -156,6 +156,7 @@ type application struct { // CharmMetadata and CharmManifest fields CharmMetadata_ *charmMetadata `yaml:"charm-metadata,omitempty"` CharmManifest_ *charmManifest `yaml:"charm-manifest,omitempty"` + CharmActions_ *charmActions `yaml:"charm-actions,omitempty"` } // ApplicationArgs is an argument struct used to add an application to the Model. @@ -554,6 +555,20 @@ func (a *application) SetCharmManifest(args CharmManifestArgs) { a.CharmManifest_ = newCharmManifest(args) } +// CharmActions implements Application. +func (a *application) CharmActions() CharmActions { + // To avoid a typed nil, check before returning. + if a.CharmActions_ == nil { + return nil + } + return a.CharmActions_ +} + +// SetCharmActions implements Application. +func (a *application) SetCharmActions(args CharmActionsArgs) { + a.CharmActions_ = newCharmActions(args) +} + // Offers implements Application. func (a *application) Offers() []ApplicationOffer { if a.Offers_ == nil || len(a.Offers_.Offers) == 0 { @@ -811,8 +826,10 @@ func applicationV13Fields() (schema.Fields, schema.Defaults) { fields, defaults := applicationV12Fields() fields["charm-metadata"] = schema.StringMap(schema.Any()) fields["charm-manifest"] = schema.StringMap(schema.Any()) + fields["charm-actions"] = schema.StringMap(schema.Any()) defaults["charm-metadata"] = schema.Omit defaults["charm-manifest"] = schema.Omit + defaults["charm-actions"] = schema.Omit return fields, defaults } @@ -1006,6 +1023,14 @@ func importApplication(fields schema.Fields, defaults schema.Defaults, importVer } result.CharmManifest_ = charmManifest } + + if charmActionsMap, ok := valid["charm-actions"]; ok { + charmActions, err := importCharmActions(charmActionsMap.(map[string]interface{})) + if err != nil { + return nil, errors.Trace(err) + } + result.CharmActions_ = charmActions + } } result.importAnnotations(valid) diff --git a/application_test.go b/application_test.go index cd17945..49613e3 100644 --- a/application_test.go +++ b/application_test.go @@ -67,6 +67,7 @@ func minimalApplicationMap() map[interface{}]interface{} { "charm-origin": minimalCharmOriginMap(), "charm-metadata": minimalCharmMetadataMap(), "charm-manifest": minimalCharmManifestMap(), + "charm-actions": minimalCharmActionsMap(), } } @@ -108,6 +109,7 @@ func minimalApplicationMapCAAS() map[interface{}]interface{} { result["charm-origin"] = minimalCharmOriginMap() result["charm-metadata"] = minimalCharmMetadataMap() result["charm-manifest"] = minimalCharmManifestMap() + result["charm-actions"] = minimalCharmActionsMap() return result } @@ -130,6 +132,7 @@ func minimalApplication(args ...ApplicationArgs) *application { a.SetCharmOrigin(minimalCharmOriginArgs()) a.SetCharmMetadata(minimalCharmMetadataArgs()) a.SetCharmManifest(minimalCharmManifestArgs()) + a.SetCharmActions(minimalCharmActionsArgs()) return a } @@ -391,6 +394,7 @@ func (s *ApplicationSerializationSuite) TestV1ParsingReturnsLatest(c *gc.C) { appLatest.CharmOrigin_ = nil appLatest.CharmMetadata_ = nil appLatest.CharmManifest_ = nil + appLatest.CharmActions_ = nil appResult := s.exportImportVersion(c, appV1, 1) appLatest.Series_ = "" @@ -416,6 +420,7 @@ func (s *ApplicationSerializationSuite) TestV2ParsingReturnsLatest(c *gc.C) { appLatest.CharmOrigin_ = nil appLatest.CharmMetadata_ = nil appLatest.CharmManifest_ = nil + appLatest.CharmActions_ = nil appResult := s.exportImportVersion(c, appV1, 2) appLatest.Series_ = "" @@ -437,6 +442,7 @@ func (s *ApplicationSerializationSuite) TestV3ParsingReturnsLatest(c *gc.C) { appLatest.CharmOrigin_ = nil appLatest.CharmMetadata_ = nil appLatest.CharmManifest_ = nil + appLatest.CharmActions_ = nil appResult := s.exportImportVersion(c, appV2, 3) appLatest.Series_ = "" @@ -454,6 +460,7 @@ func (s *ApplicationSerializationSuite) TestV5ParsingReturnsLatest(c *gc.C) { appLatest.CharmOrigin_ = nil appLatest.CharmMetadata_ = nil appLatest.CharmManifest_ = nil + appLatest.CharmActions_ = nil appResult := s.exportImportVersion(c, appV5, 5) appLatest.Series_ = "" @@ -470,6 +477,7 @@ func (s *ApplicationSerializationSuite) TestV6ParsingReturnsLatest(c *gc.C) { appLatest.CharmOrigin_ = nil appLatest.CharmMetadata_ = nil appLatest.CharmManifest_ = nil + appLatest.CharmActions_ = nil appResult := s.exportImportVersion(c, appV6, 6) appLatest.Series_ = "" diff --git a/charmactions.go b/charmactions.go new file mode 100644 index 0000000..79a1a5a --- /dev/null +++ b/charmactions.go @@ -0,0 +1,160 @@ +// Copyright 2024 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +package description + +import ( + "github.com/juju/errors" + "github.com/juju/schema" +) + +type CharmActionsArgs struct { + Actions map[string]CharmAction +} + +func newCharmActions(args CharmActionsArgs) *charmActions { + actions := make(map[string]charmAction) + if args.Actions != nil { + for name, a := range args.Actions { + actions[name] = charmAction{ + Description_: a.Description(), + Parallel_: a.Parallel(), + ExecutionGroup_: a.ExecutionGroup(), + Parameters_: a.Parameters(), + } + } + } + + return &charmActions{ + Version_: 1, + Actions_: actions, + } +} + +type charmActions struct { + Version_ int `yaml:"version"` + Actions_ map[string]charmAction `yaml:"actions"` +} + +func (a *charmActions) Actions() map[string]CharmAction { + actions := make(map[string]CharmAction) + for i, b := range a.Actions_ { + actions[i] = b + } + return actions +} + +func importCharmActions(source map[string]interface{}) (*charmActions, error) { + version, err := getVersion(source) + if err != nil { + return nil, errors.Annotate(err, "charmActions version schema check failed") + } + + importFunc, ok := charmActionsDeserializationFuncs[version] + if !ok { + return nil, errors.NotValidf("version %d", version) + } + return importFunc(source) +} + +var charmActionsDeserializationFuncs = map[int]func(map[string]interface{}) (*charmActions, error){ + 1: importCharmActionsV1, +} + +func importCharmActionsV1(source map[string]interface{}) (*charmActions, error) { + return importCharmActionsVersion(source, 1) +} + +func importCharmActionsVersion(source map[string]interface{}, version int) (*charmActions, error) { + fields := schema.Fields{ + "actions": schema.StringMap(schema.Any()), + } + defaults := schema.Defaults{ + "actions": schema.Omit, + } + checker := schema.FieldMap(fields, defaults) + + coerced, err := checker.Coerce(source, nil) + if err != nil { + return nil, errors.Annotatef(err, "charmActions v1 schema check failed") + } + valid := coerced.(map[string]interface{}) + + var actions map[string]charmAction + if valid["actions"] != nil { + actions = make(map[string]charmAction, len(valid["actions"].(map[string]interface{}))) + for name, v := range valid["actions"].(map[string]interface{}) { + var err error + actions[name], err = importCharmAction(v) + if err != nil { + return nil, errors.Annotate(err, "charmActions actions schema check failed") + } + } + } + + return &charmActions{ + Version_: 1, + Actions_: actions, + }, nil +} + +func importCharmAction(source interface{}) (charmAction, error) { + fields := schema.Fields{ + "description": schema.String(), + "parallel": schema.Bool(), + "execution-group": schema.String(), + "parameters": schema.StringMap(schema.Any()), + } + defaults := schema.Defaults{ + "description": schema.Omit, + "parallel": false, + "execution-group": schema.Omit, + "parameters": schema.Omit, + } + checker := schema.FieldMap(fields, defaults) + + coerced, err := checker.Coerce(source, nil) + if err != nil { + return charmAction{}, errors.Annotatef(err, "charmAction schema check failed") + } + valid := coerced.(map[string]interface{}) + + var parameters map[string]interface{} + if valid["parameters"] != nil { + parameters = valid["parameters"].(map[string]interface{}) + } + + return charmAction{ + Description_: valid["description"].(string), + Parallel_: valid["parallel"].(bool), + ExecutionGroup_: valid["execution-group"].(string), + Parameters_: parameters, + }, nil +} + +type charmAction struct { + Description_ string `yaml:"description"` + Parallel_ bool `yaml:"parallel"` + ExecutionGroup_ string `yaml:"execution-group"` + Parameters_ map[string]interface{} `yaml:"parameters"` +} + +// Description returns the description of the action. +func (a charmAction) Description() string { + return a.Description_ +} + +// Parallel returns whether the action can be run in parallel. +func (a charmAction) Parallel() bool { + return a.Parallel_ +} + +// ExecutionGroup returns the execution group of the action. +func (a charmAction) ExecutionGroup() string { + return a.ExecutionGroup_ +} + +// Parameters returns the parameters of the action. +func (a charmAction) Parameters() map[string]interface{} { + return a.Parameters_ +} diff --git a/charmactions_test.go b/charmactions_test.go new file mode 100644 index 0000000..39e2988 --- /dev/null +++ b/charmactions_test.go @@ -0,0 +1,171 @@ +// 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 CharmActionsSerializationSuite struct { + SerializationSuite +} + +var _ = gc.Suite(&CharmActionsSerializationSuite{}) + +func (s *CharmActionsSerializationSuite) SetUpTest(c *gc.C) { + s.importName = "charmActions" + s.importFunc = func(m map[string]interface{}) (interface{}, error) { + return importCharmActions(m) + } +} + +func (s *CharmActionsSerializationSuite) TestNewCharmActions(c *gc.C) { + args := CharmActionsArgs{ + Actions: map[string]CharmAction{ + "echo": charmAction{ + Description_: "echo description", + Parallel_: true, + ExecutionGroup_: "group1", + Parameters_: map[string]interface{}{ + "message": "string", + }, + }, + }, + } + metadata := newCharmActions(args) + + c.Assert(metadata.Actions(), gc.DeepEquals, map[string]CharmAction{ + "echo": charmAction{ + Description_: "echo description", + Parallel_: true, + ExecutionGroup_: "group1", + Parameters_: map[string]interface{}{ + "message": "string", + }, + }, + }) +} + +func minimalCharmActionsMap() map[interface{}]interface{} { + return map[interface{}]interface{}{ + "version": 1, + "actions": map[interface{}]interface{}{}, + } +} + +func minimalCharmActionsArgs() CharmActionsArgs { + return CharmActionsArgs{} +} + +func minimalCharmActions() *charmActions { + return newCharmActions(minimalCharmActionsArgs()) +} + +func maximalCharmActionsMap() map[interface{}]interface{} { + return map[interface{}]interface{}{ + "version": 1, + "actions": map[interface{}]interface{}{ + "echo": map[interface{}]interface{}{ + "description": "echo description", + "parallel": true, + "execution-group": "group1", + "parameters": map[interface{}]interface{}{ + "message": "string", + }, + }, + }, + } +} + +func maximalCharmActionsArgs() CharmActionsArgs { + return CharmActionsArgs{ + Actions: map[string]CharmAction{ + "echo": charmAction{ + Description_: "echo description", + Parallel_: true, + ExecutionGroup_: "group1", + Parameters_: map[string]interface{}{ + "message": "string", + }, + }, + }, + } +} + +func maximalCharmActions() *charmActions { + return newCharmActions(maximalCharmActionsArgs()) +} + +func (s *CharmActionsSerializationSuite) TestMinimalMatches(c *gc.C) { + bytes, err := yaml.Marshal(minimalCharmActions()) + 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, minimalCharmActionsMap()) +} + +func (s *CharmActionsSerializationSuite) TestMaximalMatches(c *gc.C) { + bytes, err := yaml.Marshal(maximalCharmActions()) + 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, maximalCharmActionsMap()) +} + +func (s *CharmActionsSerializationSuite) TestMinimalParsingSerializedData(c *gc.C) { + initial := minimalCharmActions() + 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 := importCharmActions(source) + c.Assert(err, jc.ErrorIsNil) + c.Assert(instance, jc.DeepEquals, initial) +} + +func (s *CharmActionsSerializationSuite) TestMaximalParsingSerializedData(c *gc.C) { + initial := maximalCharmActions() + 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 := importCharmActions(source) + c.Assert(err, jc.ErrorIsNil) + c.Assert(instance, jc.DeepEquals, initial) +} + +func (s *CharmActionsSerializationSuite) exportImportVersion(c *gc.C, origin_ *charmActions, version int) *charmActions { + 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 := importCharmActions(source) + c.Assert(err, jc.ErrorIsNil) + return origin +} + +func (s *CharmActionsSerializationSuite) TestV1ParsingReturnsLatest(c *gc.C) { + args := maximalCharmActionsArgs() + originV1 := newCharmActions(args) + + originLatest := *originV1 + originResult := s.exportImportVersion(c, originV1, 1) + c.Assert(*originResult, jc.DeepEquals, originLatest) +} diff --git a/charmmanifest.go b/charmmanifest.go index bb89552..d0db57d 100644 --- a/charmmanifest.go +++ b/charmmanifest.go @@ -1,4 +1,4 @@ -// Copyright 2020 Canonical Ltd. +// Copyright 2024 Canonical Ltd. // Licensed under the LGPLv3, see LICENCE file for details. package description @@ -83,7 +83,7 @@ func importCharmManifestVersion(source map[string]interface{}, importVersion int coerced, err := checker.Coerce(source, nil) if err != nil { - return nil, errors.Annotatef(err, "charmOrigin v1 schema check failed") + return nil, errors.Annotatef(err, "charmManifest v1 schema check failed") } valid := coerced.(map[string]interface{}) diff --git a/interfaces.go b/interfaces.go index 987884f..7017d45 100644 --- a/interfaces.go +++ b/interfaces.go @@ -332,3 +332,16 @@ type CharmManifestBase interface { Channel() string Architectures() []string } + +// CharmActions represents the actions of a charm. +type CharmActions interface { + Actions() map[string]CharmAction +} + +// CharmAction represents an action in the metadata of a charm. +type CharmAction interface { + Description() string + Parallel() bool + ExecutionGroup() string + Parameters() map[string]interface{} +} From a7d504e55b1624616ec917437498c4d964f7c021 Mon Sep 17 00:00:00 2001 From: Simon Richardson Date: Tue, 30 Jul 2024 10:05:44 +0100 Subject: [PATCH 2/3] feat: charm configs This adds charm configs to the description package. It should be noted that there already exists a CharmConfig in the application already, but that is the current values of the config. What this is representing is the actual charm config. It should be noted that CharmConfig was originally called settings, which doesn't help either. --- application.go | 37 +++++++++- application_test.go | 8 +++ charmactions.go | 1 + charmconfigs.go | 147 +++++++++++++++++++++++++++++++++++++++ charmconfigs_test.go | 159 +++++++++++++++++++++++++++++++++++++++++++ interfaces.go | 12 ++++ 6 files changed, 363 insertions(+), 1 deletion(-) create mode 100644 charmconfigs.go create mode 100644 charmconfigs_test.go diff --git a/application.go b/application.go index 4cc7620..03a0199 100644 --- a/application.go +++ b/application.go @@ -117,6 +117,9 @@ type application struct { EndpointBindings_ map[string]string `yaml:"endpoint-bindings,omitempty"` + // CharmConfig_ and ApplicationConfig_ are the actual configuration values + // for the charm and application, respectively. These are the values that + // have been set by the user or the charm itself. CharmConfig_ map[string]interface{} `yaml:"settings"` ApplicationConfig_ map[string]interface{} `yaml:"application-config,omitempty"` @@ -153,10 +156,14 @@ type application struct { // CharmOrigin fields CharmOrigin_ *charmOrigin `yaml:"charm-origin,omitempty"` - // CharmMetadata and CharmManifest fields + + // The following fields represent the actual charm data for the + // application. These are the immutable parts of the application, either + // provided by the charm itself. CharmMetadata_ *charmMetadata `yaml:"charm-metadata,omitempty"` CharmManifest_ *charmManifest `yaml:"charm-manifest,omitempty"` CharmActions_ *charmActions `yaml:"charm-actions,omitempty"` + CharmConfigs_ *charmConfigs `yaml:"charm-configs,omitempty"` } // ApplicationArgs is an argument struct used to add an application to the Model. @@ -569,6 +576,20 @@ func (a *application) SetCharmActions(args CharmActionsArgs) { a.CharmActions_ = newCharmActions(args) } +// CharmConfigs implements Application. +func (a *application) CharmConfigs() CharmConfigs { + // To avoid a typed nil, check before returning. + if a.CharmConfigs_ == nil { + return nil + } + return a.CharmConfigs_ +} + +// SetCharmConfigs implements Application. +func (a *application) SetCharmConfigs(args CharmConfigsArgs) { + a.CharmConfigs_ = newCharmConfigs(args) +} + // Offers implements Application. func (a *application) Offers() []ApplicationOffer { if a.Offers_ == nil || len(a.Offers_.Offers) == 0 { @@ -827,9 +848,11 @@ func applicationV13Fields() (schema.Fields, schema.Defaults) { fields["charm-metadata"] = schema.StringMap(schema.Any()) fields["charm-manifest"] = schema.StringMap(schema.Any()) fields["charm-actions"] = schema.StringMap(schema.Any()) + fields["charm-configs"] = schema.StringMap(schema.Any()) defaults["charm-metadata"] = schema.Omit defaults["charm-manifest"] = schema.Omit defaults["charm-actions"] = schema.Omit + defaults["charm-configs"] = schema.Omit return fields, defaults } @@ -1008,6 +1031,10 @@ func importApplication(fields schema.Fields, defaults schema.Defaults, importVer } if importVersion >= 13 { + // These fields are used to populate the charm data for the application. + // This ensures that correct RI is maintained for the charm data + // when migrating between models. + if charmMetadataMap, ok := valid["charm-metadata"]; ok { charmMetadata, err := importCharmMetadata(charmMetadataMap.(map[string]interface{})) if err != nil { @@ -1031,6 +1058,14 @@ func importApplication(fields schema.Fields, defaults schema.Defaults, importVer } result.CharmActions_ = charmActions } + + if charmConfigMap, ok := valid["charm-configs"]; ok { + charmConfig, err := importCharmConfigs(charmConfigMap.(map[string]interface{})) + if err != nil { + return nil, errors.Trace(err) + } + result.CharmConfigs_ = charmConfig + } } result.importAnnotations(valid) diff --git a/application_test.go b/application_test.go index 49613e3..a94e22d 100644 --- a/application_test.go +++ b/application_test.go @@ -68,6 +68,7 @@ func minimalApplicationMap() map[interface{}]interface{} { "charm-metadata": minimalCharmMetadataMap(), "charm-manifest": minimalCharmManifestMap(), "charm-actions": minimalCharmActionsMap(), + "charm-configs": minimalCharmConfigsMap(), } } @@ -110,6 +111,7 @@ func minimalApplicationMapCAAS() map[interface{}]interface{} { result["charm-metadata"] = minimalCharmMetadataMap() result["charm-manifest"] = minimalCharmManifestMap() result["charm-actions"] = minimalCharmActionsMap() + result["charm-configs"] = minimalCharmConfigsMap() return result } @@ -133,6 +135,7 @@ func minimalApplication(args ...ApplicationArgs) *application { a.SetCharmMetadata(minimalCharmMetadataArgs()) a.SetCharmManifest(minimalCharmManifestArgs()) a.SetCharmActions(minimalCharmActionsArgs()) + a.SetCharmConfigs(minimalCharmConfigsArgs()) return a } @@ -395,6 +398,7 @@ func (s *ApplicationSerializationSuite) TestV1ParsingReturnsLatest(c *gc.C) { appLatest.CharmMetadata_ = nil appLatest.CharmManifest_ = nil appLatest.CharmActions_ = nil + appLatest.CharmConfigs_ = nil appResult := s.exportImportVersion(c, appV1, 1) appLatest.Series_ = "" @@ -421,6 +425,7 @@ func (s *ApplicationSerializationSuite) TestV2ParsingReturnsLatest(c *gc.C) { appLatest.CharmMetadata_ = nil appLatest.CharmManifest_ = nil appLatest.CharmActions_ = nil + appLatest.CharmConfigs_ = nil appResult := s.exportImportVersion(c, appV1, 2) appLatest.Series_ = "" @@ -443,6 +448,7 @@ func (s *ApplicationSerializationSuite) TestV3ParsingReturnsLatest(c *gc.C) { appLatest.CharmMetadata_ = nil appLatest.CharmManifest_ = nil appLatest.CharmActions_ = nil + appLatest.CharmConfigs_ = nil appResult := s.exportImportVersion(c, appV2, 3) appLatest.Series_ = "" @@ -461,6 +467,7 @@ func (s *ApplicationSerializationSuite) TestV5ParsingReturnsLatest(c *gc.C) { appLatest.CharmMetadata_ = nil appLatest.CharmManifest_ = nil appLatest.CharmActions_ = nil + appLatest.CharmConfigs_ = nil appResult := s.exportImportVersion(c, appV5, 5) appLatest.Series_ = "" @@ -478,6 +485,7 @@ func (s *ApplicationSerializationSuite) TestV6ParsingReturnsLatest(c *gc.C) { appLatest.CharmMetadata_ = nil appLatest.CharmManifest_ = nil appLatest.CharmActions_ = nil + appLatest.CharmConfigs_ = nil appResult := s.exportImportVersion(c, appV6, 6) appLatest.Series_ = "" diff --git a/charmactions.go b/charmactions.go index 79a1a5a..020f5f7 100644 --- a/charmactions.go +++ b/charmactions.go @@ -36,6 +36,7 @@ type charmActions struct { Actions_ map[string]charmAction `yaml:"actions"` } +// Actions returns the actions of the charm. func (a *charmActions) Actions() map[string]CharmAction { actions := make(map[string]CharmAction) for i, b := range a.Actions_ { diff --git a/charmconfigs.go b/charmconfigs.go new file mode 100644 index 0000000..bd970cc --- /dev/null +++ b/charmconfigs.go @@ -0,0 +1,147 @@ +// Copyright 2024 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +package description + +import ( + "github.com/juju/errors" + "github.com/juju/schema" +) + +type CharmConfigsArgs struct { + Configs map[string]CharmConfig +} + +func newCharmConfigs(args CharmConfigsArgs) *charmConfigs { + config := make(map[string]charmConfig) + if args.Configs != nil { + for name, c := range args.Configs { + config[name] = charmConfig{ + Type_: c.Type(), + Default_: c.Default(), + Description_: c.Description(), + } + } + } + + return &charmConfigs{ + Version_: 1, + Configs_: config, + } +} + +type charmConfigs struct { + Version_ int `yaml:"version"` + Configs_ map[string]charmConfig `yaml:"configs"` +} + +// Configs returns the configs of the charm. +func (c *charmConfigs) Configs() map[string]CharmConfig { + configs := make(map[string]CharmConfig) + for i, b := range c.Configs_ { + configs[i] = b + } + return configs +} + +func importCharmConfigs(source map[string]interface{}) (*charmConfigs, error) { + version, err := getVersion(source) + if err != nil { + return nil, errors.Annotate(err, "charmConfigs version schema check failed") + } + + importFunc, ok := charmConfigsDeserializationFuncs[version] + if !ok { + return nil, errors.NotValidf("version %d", version) + } + return importFunc(source) +} + +var charmConfigsDeserializationFuncs = map[int]func(map[string]interface{}) (*charmConfigs, error){ + 1: importCharmConfigsV1, +} + +func importCharmConfigsV1(source map[string]interface{}) (*charmConfigs, error) { + return importCharmConfigsVersion(source, 1) +} + +func importCharmConfigsVersion(source map[string]interface{}, version int) (*charmConfigs, error) { + fields := schema.Fields{ + "configs": schema.StringMap(schema.Any()), + } + defaults := schema.Defaults{ + "configs": schema.Omit, + } + checker := schema.FieldMap(fields, defaults) + + coerced, err := checker.Coerce(source, nil) + if err != nil { + return nil, errors.Annotatef(err, "charmConfigs v1 schema check failed") + } + valid := coerced.(map[string]interface{}) + + var configs map[string]charmConfig + if valid["configs"] != nil { + configs = make(map[string]charmConfig) + for name, raw := range valid["configs"].(map[string]interface{}) { + var err error + configs[name], err = importCharmConfig(raw) + if err != nil { + return nil, errors.Annotatef(err, "charmConfig %q schema check failed", name) + } + } + } + + return &charmConfigs{ + Version_: 1, + Configs_: configs, + }, nil +} + +func importCharmConfig(source interface{}) (charmConfig, error) { + fields := schema.Fields{ + "type": schema.String(), + "default": schema.Any(), + "description": schema.String(), + } + defaults := schema.Defaults{ + "type": schema.Omit, + "default": schema.Omit, + "description": schema.Omit, + } + checker := schema.FieldMap(fields, defaults) + + coerced, err := checker.Coerce(source, nil) + if err != nil { + return charmConfig{}, errors.Annotate(err, "charmConfig schema check failed") + } + valid := coerced.(map[string]interface{}) + + config := charmConfig{ + Type_: valid["type"].(string), + Default_: valid["default"], + Description_: valid["description"].(string), + } + return config, nil +} + +type charmConfig struct { + Type_ string `yaml:"type"` + Default_ interface{} `yaml:"default"` + Description_ string `yaml:"description"` +} + +// Type returns the type of the config. +func (c charmConfig) Type() string { + return c.Type_ +} + +// Default returns the default value of the config. +func (c charmConfig) Default() interface{} { + return c.Default_ +} + +// Description returns the description of the config. +func (c charmConfig) Description() string { + return c.Description_ +} diff --git a/charmconfigs_test.go b/charmconfigs_test.go new file mode 100644 index 0000000..3765ae3 --- /dev/null +++ b/charmconfigs_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 CharmConfigsSerializationSuite struct { + SerializationSuite +} + +var _ = gc.Suite(&CharmConfigsSerializationSuite{}) + +func (s *CharmConfigsSerializationSuite) SetUpTest(c *gc.C) { + s.importName = "charmConfigs" + s.importFunc = func(m map[string]interface{}) (interface{}, error) { + return importCharmConfigs(m) + } +} + +func (s *CharmConfigsSerializationSuite) TestNewCharmConfigs(c *gc.C) { + args := CharmConfigsArgs{ + Configs: map[string]CharmConfig{ + "foo": charmConfig{ + Description_: "description", + Type_: "string", + Default_: "default", + }, + }, + } + metadata := newCharmConfigs(args) + + c.Assert(metadata.Configs(), gc.DeepEquals, map[string]CharmConfig{ + "foo": charmConfig{ + Description_: "description", + Type_: "string", + Default_: "default", + }, + }) +} + +func minimalCharmConfigsMap() map[interface{}]interface{} { + return map[interface{}]interface{}{ + "version": 1, + "configs": map[interface{}]interface{}{}, + } +} + +func minimalCharmConfigsArgs() CharmConfigsArgs { + return CharmConfigsArgs{} +} + +func minimalCharmConfigs() *charmConfigs { + return newCharmConfigs(minimalCharmConfigsArgs()) +} + +func maximalCharmConfigsMap() map[interface{}]interface{} { + return map[interface{}]interface{}{ + "version": 1, + "configs": map[interface{}]interface{}{ + "foo": map[interface{}]interface{}{ + "description": "description", + "type": "string", + "default": "default", + }, + }, + } +} + +func maximalCharmConfigsArgs() CharmConfigsArgs { + return CharmConfigsArgs{ + Configs: map[string]CharmConfig{ + "foo": charmConfig{ + Description_: "description", + Type_: "string", + Default_: "default", + }, + }, + } +} + +func maximalCharmConfigs() *charmConfigs { + return newCharmConfigs(maximalCharmConfigsArgs()) +} + +func (s *CharmConfigsSerializationSuite) TestMinimalMatches(c *gc.C) { + bytes, err := yaml.Marshal(minimalCharmConfigs()) + 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, minimalCharmConfigsMap()) +} + +func (s *CharmConfigsSerializationSuite) TestMaximalMatches(c *gc.C) { + bytes, err := yaml.Marshal(maximalCharmConfigs()) + 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, maximalCharmConfigsMap()) +} + +func (s *CharmConfigsSerializationSuite) TestMinimalParsingSerializedData(c *gc.C) { + initial := minimalCharmConfigs() + 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 := importCharmConfigs(source) + c.Assert(err, jc.ErrorIsNil) + c.Assert(instance, jc.DeepEquals, initial) +} + +func (s *CharmConfigsSerializationSuite) TestMaximalParsingSerializedData(c *gc.C) { + initial := maximalCharmConfigs() + 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 := importCharmConfigs(source) + c.Assert(err, jc.ErrorIsNil) + c.Assert(instance, jc.DeepEquals, initial) +} + +func (s *CharmConfigsSerializationSuite) exportImportVersion(c *gc.C, origin_ *charmConfigs, version int) *charmConfigs { + 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 := importCharmConfigs(source) + c.Assert(err, jc.ErrorIsNil) + return origin +} + +func (s *CharmConfigsSerializationSuite) TestV1ParsingReturnsLatest(c *gc.C) { + args := maximalCharmConfigsArgs() + originV1 := newCharmConfigs(args) + + originLatest := *originV1 + originResult := s.exportImportVersion(c, originV1, 1) + c.Assert(*originResult, jc.DeepEquals, originLatest) +} diff --git a/interfaces.go b/interfaces.go index 7017d45..22f0c5e 100644 --- a/interfaces.go +++ b/interfaces.go @@ -345,3 +345,15 @@ type CharmAction interface { ExecutionGroup() string Parameters() map[string]interface{} } + +// CharmConfigs represents the configuration of a charm. +type CharmConfigs interface { + Configs() map[string]CharmConfig +} + +// CharmConfig represents the configuration of a charm. +type CharmConfig interface { + Type() string + Default() interface{} + Description() string +} From dfac62caf6de2268a18f54a8111cd0513354cbbb Mon Sep 17 00:00:00 2001 From: Simon Richardson Date: Tue, 30 Jul 2024 10:18:46 +0100 Subject: [PATCH 3/3] feat: lxd profile This also adds the LXD profile to the charm metadata. Adding this to charm metadata as a blob allows us to pass it through without it being incorrectly interpreted as a yaml. --- charmmetadata.go | 15 +++++++++++++++ charmmetadata_test.go | 2 ++ 2 files changed, 17 insertions(+) diff --git a/charmmetadata.go b/charmmetadata.go index 81c4505..ef3dc62 100644 --- a/charmmetadata.go +++ b/charmmetadata.go @@ -30,6 +30,7 @@ type CharmMetadataArgs struct { Resources map[string]CharmMetadataResource Terms []string Containers map[string]CharmMetadataContainer + LXDProfile string } func newCharmMetadata(args CharmMetadataArgs) *charmMetadata { @@ -176,6 +177,7 @@ func newCharmMetadata(args CharmMetadataArgs) *charmMetadata { Resources_: resources, Terms_: args.Terms, Containers_: containers, + LXDProfile_: args.LXDProfile, } } @@ -201,6 +203,7 @@ type charmMetadata struct { Resources_ map[string]charmMetadataResource `yaml:"resources,omitempty"` Terms_ []string `yaml:"terms,omitempty"` Containers_ map[string]charmMetadataContainer `yaml:"containers,omitempty"` + LXDProfile_ string `yaml:"lxd-profile,omitempty"` } // Name returns the name of the charm. @@ -330,6 +333,11 @@ func (m *charmMetadata) Containers() map[string]CharmMetadataContainer { return containers } +// LXDPofile returns the LXD profile of the charm. +func (m *charmMetadata) LXDProfile() string { + return m.LXDProfile_ +} + func importCharmMetadata(source map[string]interface{}) (*charmMetadata, error) { version, err := getVersion(source) if err != nil { @@ -375,6 +383,7 @@ func importCharmMetadataVersion(source map[string]interface{}, importVersion int "resources": schema.StringMap(schema.Any()), "terms": schema.List(schema.String()), "containers": schema.StringMap(schema.Any()), + "lxd-profile": schema.String(), } defaults := schema.Defaults{ "summary": schema.Omit, @@ -395,6 +404,7 @@ func importCharmMetadataVersion(source map[string]interface{}, importVersion int "resources": schema.Omit, "terms": schema.Omit, "containers": schema.Omit, + "lxd-profile": schema.Omit, } checker := schema.FieldMap(fields, defaults) @@ -531,6 +541,7 @@ func importCharmMetadataVersion(source map[string]interface{}, importVersion int minJujuVersion string runAs string assumes string + lxdProfile string ) if valid["summary"] != nil { @@ -551,6 +562,9 @@ func importCharmMetadataVersion(source map[string]interface{}, importVersion int if valid["assumes"] != nil { assumes = valid["assumes"].(string) } + if valid["lxd-profile"] != nil { + lxdProfile = valid["lxd-profile"].(string) + } return &charmMetadata{ Version_: 1, @@ -573,6 +587,7 @@ func importCharmMetadataVersion(source map[string]interface{}, importVersion int Payloads_: payloads, Containers_: containers, Terms_: terms, + LXDProfile_: lxdProfile, }, nil } diff --git a/charmmetadata_test.go b/charmmetadata_test.go index f445422..91dbb43 100644 --- a/charmmetadata_test.go +++ b/charmmetadata_test.go @@ -58,6 +58,7 @@ func maximalCharmMetadataMap() map[interface{}]interface{} { "run-as": "root", "assumes": "{}", "min-juju-version": "4.0.0", + "lxd-profile": "{}", "categories": []interface{}{"test", "testing"}, "tags": []interface{}{"foo", "bar"}, "terms": []interface{}{"baz", "qux"}, @@ -154,6 +155,7 @@ func maximalCharmMetadataArgs() CharmMetadataArgs { RunAs: "root", Assumes: "{}", MinJujuVersion: "4.0.0", + LXDProfile: "{}", Categories: []string{"test", "testing"}, Tags: []string{"foo", "bar"}, Terms: []string{"baz", "qux"},