Skip to content

Commit

Permalink
Merge pull request juju#17668 from SimonRichardson/charm-state-part-deux
Browse files Browse the repository at this point in the history
juju#17668

Adds state get implementations, the set charm is going to be in subsequent PR, as I believe that it will take a lot of time to correctly implement. Manifest, actions, config, and LXD profiles are row-based structs, which is the normal form, unlike charm metadata. The manifest bases are defined as an array and an index was added to the state schema to ensure correct ordering was maintained (although we do run into another sqlair bug[1]). Architectures will not be guaranteed to be in order, because we're storing the base array and the architectures as one flat row. If we did want to do that, we should create another table that kept track of that one as well. For now, the order of architectures shouldn't matter as we don't pick one over the other.

 1. canonical/sqlair#155

<!-- 
The PR title should match: <type>(optional <scope>): <description>.

Please also ensure all commits in this PR comply with our conventional commits specification:
https://docs.google.com/document/d/1SYUo9G7qZ_jdoVXpUVamS5VCgHmtZ0QA-wZxKoMS-C0 
-->

<!-- Why this change is needed and what it does. -->

## Checklist

<!-- If an item is not applicable, use `~strikethrough~`. -->

- [x] Code style: imports ordered, good names, simple structure, etc
- [x] Comments saying why design decisions were made
- [x] Go unit tests, with comments saying what you're testing

## QA steps

The state tests should pass. Once the `SetCharm` is implemented in subsequent PRs then we should start to see full end to end tests, which would better validate all the state implementation.


## Links


**Jira card:** [JUJU-6015](https://warthogs.atlassian.net/browse/JUJU-6015)



[JUJU-6015]: https://warthogs.atlassian.net/browse/JUJU-6015?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
  • Loading branch information
jujubot authored Jul 11, 2024
2 parents 4afec8b + bbfd224 commit ec8a836
Show file tree
Hide file tree
Showing 10 changed files with 1,077 additions and 68 deletions.
23 changes: 23 additions & 0 deletions domain/charm/state/actions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Copyright 2024 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package state

import (
"github.com/juju/juju/domain/charm"
)

func decodeActions(actions []charmAction) charm.Actions {
result := charm.Actions{
Actions: make(map[string]charm.Action),
}
for _, action := range actions {
result.Actions[action.Key] = charm.Action{
Description: action.Description,
Parallel: action.Parallel,
ExecutionGroup: action.ExecutionGroup,
Params: action.Params,
}
}
return result
}
62 changes: 62 additions & 0 deletions domain/charm/state/actions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright 2024 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package state

import (
gc "gopkg.in/check.v1"

"github.com/juju/juju/domain/charm"
schematesting "github.com/juju/juju/domain/schema/testing"
)

type actionsSuite struct {
schematesting.ModelSuite
}

var _ = gc.Suite(&actionsSuite{})

var actionsTestCases = [...]struct {
name string
input []charmAction
output charm.Actions
}{
{
name: "empty",
input: []charmAction{},
output: charm.Actions{
Actions: make(map[string]charm.Action),
},
},
{
name: "single",
input: []charmAction{
{
Key: "action",
Description: "description",
Parallel: true,
ExecutionGroup: "group",
Params: []byte("{}"),
},
},
output: charm.Actions{
Actions: map[string]charm.Action{
"action": {
Description: "description",
Parallel: true,
ExecutionGroup: "group",
Params: []byte("{}"),
},
},
},
},
}

func (s *actionsSuite) TestConvertActions(c *gc.C) {
for _, tc := range actionsTestCases {
c.Logf("Running test case %q", tc.name)

result := decodeActions(tc.input)
c.Check(result, gc.DeepEquals, tc.output)
}
}
67 changes: 67 additions & 0 deletions domain/charm/state/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright 2024 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package state

import (
"fmt"
"strconv"

"github.com/juju/juju/domain/charm"
)

func decodeConfig(configs []charmConfig) (charm.Config, error) {
result := charm.Config{
Options: make(map[string]charm.Option),
}
for _, config := range configs {
optionType, err := decodeConfigType(config.Type)
if err != nil {
return charm.Config{}, fmt.Errorf("cannot decode config type %q: %w", config.Type, err)
}

defaultValue, err := decodeConfigDefaultValue(optionType, config.DefaultValue)
if err != nil {
return charm.Config{}, fmt.Errorf("cannot decode config default value %q: %w", config.DefaultValue, err)
}

result.Options[config.Key] = charm.Option{
Type: optionType,
Description: config.Description,
Default: defaultValue,
}
}
return result, nil
}

func decodeConfigType(t string) (charm.OptionType, error) {
switch t {
case "string":
return charm.OptionString, nil
case "int":
return charm.OptionInt, nil
case "float":
return charm.OptionFloat, nil
case "boolean":
return charm.OptionBool, nil
case "secret":
return charm.OptionSecret, nil
default:
return "", fmt.Errorf("unknown config type %q", t)
}
}

func decodeConfigDefaultValue(t charm.OptionType, value string) (any, error) {
switch t {
case charm.OptionString, charm.OptionSecret:
return value, nil
case charm.OptionInt:
return strconv.Atoi(value)
case charm.OptionFloat:
return strconv.ParseFloat(value, 64)
case charm.OptionBool:
return strconv.ParseBool(value)
default:
return nil, fmt.Errorf("unknown config type %q", t)
}
}
142 changes: 142 additions & 0 deletions domain/charm/state/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// Copyright 2024 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package state

import (
jc "github.com/juju/testing/checkers"
gc "gopkg.in/check.v1"

"github.com/juju/juju/domain/charm"
schematesting "github.com/juju/juju/domain/schema/testing"
)

type configSuite struct {
schematesting.ModelSuite
}

var _ = gc.Suite(&configSuite{})

var configTestCases = [...]struct {
name string
input []charmConfig
output charm.Config
}{
{
name: "empty",
input: []charmConfig{},
output: charm.Config{
Options: make(map[string]charm.Option),
},
},
{
name: "string",
input: []charmConfig{
{
Key: "string",
Type: "string",
Description: "description",
DefaultValue: "default",
},
},
output: charm.Config{
Options: map[string]charm.Option{
"string": {
Type: charm.OptionString,
Description: "description",
Default: "default",
},
},
},
},
{
name: "secret",
input: []charmConfig{
{
Key: "secret",
Type: "secret",
Description: "description",
DefaultValue: "default",
},
},
output: charm.Config{
Options: map[string]charm.Option{
"secret": {
Type: charm.OptionSecret,
Description: "description",
Default: "default",
},
},
},
},
{
name: "int",
input: []charmConfig{
{
Key: "int",
Type: "int",
Description: "description",
DefaultValue: "1",
},
},
output: charm.Config{
Options: map[string]charm.Option{
"int": {
Type: charm.OptionInt,
Description: "description",
Default: 1,
},
},
},
},
{
name: "float",
input: []charmConfig{
{
Key: "float",
Type: "float",
Description: "description",
DefaultValue: "4.2",
},
},
output: charm.Config{
Options: map[string]charm.Option{
"float": {
Type: charm.OptionFloat,
Description: "description",
Default: 4.2,
},
},
},
},
{
name: "boolean",
input: []charmConfig{
{
Key: "boolean",
Type: "boolean",
Description: "description",
DefaultValue: "true",
},
},
output: charm.Config{
Options: map[string]charm.Option{
"boolean": {
Type: charm.OptionBool,
Description: "description",
Default: true,
},
},
},
},
}

func (s *configSuite) TestConvertConfig(c *gc.C) {
for _, tc := range configTestCases {
c.Logf("Running test case %q", tc.name)

result, err := decodeConfig(tc.input)
c.Assert(err, jc.ErrorIsNil)
c.Check(result, gc.DeepEquals, tc.output)
}
}
89 changes: 89 additions & 0 deletions domain/charm/state/manifest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright 2024 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.

package state

import (
"fmt"

"github.com/juju/juju/domain/charm"
)

// decodeManifest decodes the given manifests into a charm.Manifest.
// It should respect the order of the bases in the manifest. Although the
// order of architectures within a base is not guaranteed.
func decodeManifest(manifests []charmManifest) (charm.Manifest, error) {
bases := make(map[int]charm.Base)

var largestIndex int
for _, base := range manifests {
channel, err := decodeManifestChannel(base)
if err != nil {
return charm.Manifest{}, fmt.Errorf("cannot decode channel: %w", err)
}

if b, ok := bases[base.Index]; ok {
b.Architectures = append(b.Architectures, base.Architecture)
bases[base.Index] = b

continue
}

var architectures []string
if base.Architecture != "" {
architectures = append(architectures, base.Architecture)
}

bases[base.Index] = charm.Base{
Name: base.OS,
Channel: channel,
Architectures: architectures,
}

if base.Index > largestIndex {
largestIndex = base.Index
}
}

// Convert the map into a slice using the largest index as the length. This
// means that we preserved the order of the bases even faced with holes in
// the array.
result := make([]charm.Base, largestIndex+1)
for index, base := range bases {
result[index] = base
}

return charm.Manifest{
Bases: result,
}, nil
}

// decodeManifestChannel decodes the given base into a charm.Channel.
func decodeManifestChannel(base charmManifest) (charm.Channel, error) {
risk, err := decodeManifestChannelRisk(base.Risk)
if err != nil {
return charm.Channel{}, fmt.Errorf("cannot decode risk: %w", err)
}

return charm.Channel{
Track: base.Track,
Risk: risk,
Branch: base.Branch,
}, nil
}

// decodeManifestChannelRisk decodes the given risk into a charm.ChannelRisk.
func decodeManifestChannelRisk(risk string) (charm.ChannelRisk, error) {
switch risk {
case "stable":
return charm.RiskStable, nil
case "candidate":
return charm.RiskCandidate, nil
case "beta":
return charm.RiskBeta, nil
case "edge":
return charm.RiskEdge, nil
default:
return "", fmt.Errorf("unknown risk %q", risk)
}
}
Loading

0 comments on commit ec8a836

Please sign in to comment.