Skip to content

Commit

Permalink
Merge pull request #147 from SimonRichardson/charm-metadata-manifest
Browse files Browse the repository at this point in the history
#147

When migrating 3.6 to 4.0 we need to have the charm metadata and manifest upfront when importing an application. This is a requirement in order to keep RI (referential integrity) for the new SQL schema. The metadata and manifest are treated as an amorphous type, not as individual leaf types for every type of the metadata. This means when we want to rev the metadata it will be done at the root level (metadata and manifest only have a version).

Otherwise, the code is a replication of https://github.com/juju/juju/tree/main/internal/charm. Versioning of the charm will now impact model import/export. Changing the charm metadata is no longer free (probably never was, but always treated as such).

----

JIRA: [JUJU-6411](https://warthogs.atlassian.net/browse/JUJU-6411)


[JUJU-6411]: https://warthogs.atlassian.net/browse/JUJU-6411?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
  • Loading branch information
jujubot authored Jul 26, 2024
2 parents bb4ae56 + f768240 commit 45f57a3
Show file tree
Hide file tree
Showing 9 changed files with 1,953 additions and 7 deletions.
70 changes: 70 additions & 0 deletions application.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ type Application interface {
CharmOrigin() CharmOrigin
SetCharmOrigin(CharmOriginArgs)

CharmMetadata() CharmMetadata
SetCharmMetadata(CharmMetadataArgs)

CharmManifest() CharmManifest
SetCharmManifest(CharmManifestArgs)

Tools() AgentTools
SetTools(AgentToolsArgs)

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -639,6 +676,7 @@ var applicationDeserializationFuncs = map[int]applicationDeserializationFunc{
10: importApplicationV10,
11: importApplicationV11,
12: importApplicationV12,
13: importApplicationV13,
}

func applicationV1Fields() (schema.Fields, schema.Defaults) {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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 {
Expand Down
20 changes: 18 additions & 2 deletions application_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@ func minimalApplicationMap() map[interface{}]interface{} {
minimalUnitMap(),
},
},
"charm-origin": minimalCharmOriginMap(),
"charm-origin": minimalCharmOriginMap(),
"charm-metadata": minimalCharmMetadataMap(),
"charm-manifest": minimalCharmManifestMap(),
}
}

Expand Down Expand Up @@ -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
}

Expand All @@ -124,6 +128,8 @@ func minimalApplication(args ...ApplicationArgs) *application {
u.SetTools(minimalAgentToolsArgs())
}
a.SetCharmOrigin(minimalCharmOriginArgs())
a.SetCharmMetadata(minimalCharmMetadataArgs())
a.SetCharmManifest(minimalCharmManifestArgs())
return a
}

Expand Down Expand Up @@ -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) {
Expand All @@ -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_ = ""
Expand All @@ -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_ = ""
Expand All @@ -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_ = ""
Expand All @@ -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_ = ""
Expand All @@ -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_ = ""
Expand Down
161 changes: 161 additions & 0 deletions charmmanifest.go
Original file line number Diff line number Diff line change
@@ -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_
}
Loading

0 comments on commit 45f57a3

Please sign in to comment.