diff --git a/application.go b/application.go index f3cf838..142a195 100644 --- a/application.go +++ b/application.go @@ -71,6 +71,8 @@ type Application interface { OpenedPortRanges() PortRanges AddOpenedPortRange(OpenedPortRangeArgs) + + ProvisioningState() ProvisioningState } // ExposedEndpoint encapsulates the details about the CIDRs and/or spaces that @@ -128,14 +130,15 @@ type application struct { StorageConstraints_ map[string]*storageconstraint `yaml:"storage-constraints,omitempty"` // CAAS application fields. - PasswordHash_ string `yaml:"password-hash,omitempty"` - PodSpec_ string `yaml:"pod-spec,omitempty"` - Placement_ string `yaml:"placement,omitempty"` - HasResources_ bool `yaml:"has-resources,omitempty"` - DesiredScale_ int `yaml:"desired-scale,omitempty"` - CloudService_ *cloudService `yaml:"cloud-service,omitempty"` - Tools_ *agentTools `yaml:"tools,omitempty"` - OperatorStatus_ *status `yaml:"operator-status,omitempty"` + PasswordHash_ string `yaml:"password-hash,omitempty"` + PodSpec_ string `yaml:"pod-spec,omitempty"` + Placement_ string `yaml:"placement,omitempty"` + HasResources_ bool `yaml:"has-resources,omitempty"` + DesiredScale_ int `yaml:"desired-scale,omitempty"` + CloudService_ *cloudService `yaml:"cloud-service,omitempty"` + Tools_ *agentTools `yaml:"tools,omitempty"` + OperatorStatus_ *status `yaml:"operator-status,omitempty"` + ProvisioningState_ *provisioningState `yaml:"provisioning-state,omitempty"` OpenedPortRanges_ *deployedPortRanges `yaml:"opened-port-ranges,omitempty"` @@ -173,6 +176,7 @@ type ApplicationArgs struct { LeadershipSettings map[string]interface{} StorageConstraints map[string]StorageConstraintArgs MetricsCredentials []byte + ProvisioningState *ProvisioningStateArgs } func newApplication(args ApplicationArgs) *application { @@ -201,6 +205,7 @@ func newApplication(args ApplicationArgs) *application { LeadershipSettings_: args.LeadershipSettings, MetricsCredentials_: creds, StatusHistory_: newStatusHistory(), + ProvisioningState_: newProvisioningState(args.ProvisioningState), } app.setUnits(nil) app.setResources(nil) @@ -578,6 +583,14 @@ func (a *application) Validate() error { return nil } +// ProvisioningState implements Application. +func (a *application) ProvisioningState() ProvisioningState { + if a.ProvisioningState_ == nil { + return nil + } + return a.ProvisioningState_ +} + func importApplications(source map[string]interface{}) ([]*application, error) { checker := versionedChecker("applications") coerced, err := checker.Coerce(source, nil) @@ -624,6 +637,7 @@ var applicationDeserializationFuncs = map[int]applicationDeserializationFunc{ 8: importApplicationV8, 9: importApplicationV9, 10: importApplicationV10, + 11: importApplicationV11, } func applicationV1Fields() (schema.Fields, schema.Defaults) { @@ -738,6 +752,13 @@ func applicationV10Fields() (schema.Fields, schema.Defaults) { return fields, defaults } +func applicationV11Fields() (schema.Fields, schema.Defaults) { + fields, defaults := applicationV10Fields() + fields["provisioning-state"] = schema.StringMap(schema.Any()) + defaults["provisioning-state"] = schema.Omit + return fields, defaults +} + func importApplicationV1(source map[string]interface{}) (*application, error) { fields, defaults := applicationV1Fields() return importApplication(fields, defaults, 1, source) @@ -788,6 +809,11 @@ func importApplicationV10(source map[string]interface{}) (*application, error) { return importApplication(fields, defaults, 10, source) } +func importApplicationV11(source map[string]interface{}) (*application, error) { + fields, defaults := applicationV11Fields() + return importApplication(fields, defaults, 11, source) +} + func importApplication(fields schema.Fields, defaults schema.Defaults, importVersion int, source map[string]interface{}) (*application, error) { checker := schema.FieldMap(fields, defaults) @@ -865,6 +891,14 @@ func importApplication(fields schema.Fields, defaults schema.Defaults, importVer } } + if importVersion >= 11 { + if provisioningState, ok := valid["provisioning-state"].(map[string]interface{}); ok { + if result.ProvisioningState_, err = importProvisioningState(provisioningState); err != nil { + return nil, errors.Trace(err) + } + } + } + series, hasSeries := valid["series"].(string) if importVersion <= 9 && importVersion >= 7 && hasSeries { // If we have a series but no platform defined lets make a platform from the series diff --git a/application_test.go b/application_test.go index 6986f2f..65e02d9 100644 --- a/application_test.go +++ b/application_test.go @@ -362,7 +362,7 @@ func (s *ApplicationSerializationSuite) exportImportVersion(c *gc.C, application } func (s *ApplicationSerializationSuite) exportImportLatest(c *gc.C, application_ *application) *application { - return s.exportImportVersion(c, application_, 10) + return s.exportImportVersion(c, application_, 11) } func (s *ApplicationSerializationSuite) TestV1ParsingReturnsLatest(c *gc.C) { @@ -587,6 +587,19 @@ func (s *ApplicationSerializationSuite) TestDesiredScale(c *gc.C) { c.Assert(application.DesiredScale(), gc.Equals, 3) } +func (s *ApplicationSerializationSuite) TestProvisioningState(c *gc.C) { + args := minimalApplicationArgs(CAAS) + args.ProvisioningState = &ProvisioningStateArgs{ + Scaling: true, + ScaleTarget: 10, + } + initial := minimalApplication(args) + + application := s.exportImportLatest(c, initial) + c.Assert(application.ProvisioningState().Scaling(), jc.IsTrue) + c.Assert(application.ProvisioningState().ScaleTarget(), gc.Equals, 10) +} + func (s *ApplicationSerializationSuite) TestCloudService(c *gc.C) { args := minimalApplicationArgs(CAAS) initial := minimalApplication(args) diff --git a/model.go b/model.go index addb11d..9f901ad 100644 --- a/model.go +++ b/model.go @@ -453,7 +453,7 @@ func (m *model) AddApplication(args ApplicationArgs) Application { func (m *model) setApplications(applicationList []*application) { m.Applications_ = applications{ - Version: 10, + Version: 11, Applications_: applicationList, } } diff --git a/provisioningstate.go b/provisioningstate.go new file mode 100644 index 0000000..01b7b57 --- /dev/null +++ b/provisioningstate.go @@ -0,0 +1,96 @@ +// Copyright 2023 Canonical Ltd. +// Licensed under the LGPLv3, see LICENCE file for details. + +package description + +import ( + "github.com/juju/errors" + "github.com/juju/schema" +) + +type ProvisioningState interface { + Scaling() bool + ScaleTarget() int +} + +type provisioningState struct { + Version_ int `yaml:"version"` + Scaling_ bool `yaml:"scaling"` + ScaleTarget_ int `yaml:"scale-target"` +} + +func (i *provisioningState) Scaling() bool { + return i.Scaling_ +} + +func (i *provisioningState) ScaleTarget() int { + return i.ScaleTarget_ +} + +// ProvisioningStateArgs is an argument struct used to create a +// new internal provisioningState type that supports the ProvisioningState interface. +type ProvisioningStateArgs struct { + Scaling bool + ScaleTarget int +} + +func newProvisioningState(args *ProvisioningStateArgs) *provisioningState { + if args == nil { + return nil + } + return &provisioningState{ + Version_: 1, + Scaling_: args.Scaling, + ScaleTarget_: args.ScaleTarget, + } +} + +func importProvisioningState(source map[string]interface{}) (*provisioningState, error) { + version, err := getVersion(source) + if err != nil { + return nil, errors.Annotate(err, "provisioning-state version schema check failed") + } + importFunc, ok := provisioningStateDeserializationFuncs[version] + if !ok { + return nil, errors.NotValidf("version %d", version) + } + return importFunc(source) +} + +type provisioningStateDeserializationFunc func(map[string]interface{}) (*provisioningState, error) + +var provisioningStateDeserializationFuncs = map[int]provisioningStateDeserializationFunc{ + 1: importProvisioningStateV1, +} + +func importProvisioningStateV1(source map[string]interface{}) (*provisioningState, error) { + fields, defaults := provisioningStateV1Schema() + checker := schema.FieldMap(fields, defaults) + + coerced, err := checker.Coerce(source, nil) + if err != nil { + return nil, errors.Annotatef(err, "provisioning-state v1 schema check failed") + } + + return provisioningStateV1(coerced.(map[string]interface{})), nil +} + +func provisioningStateV1Schema() (schema.Fields, schema.Defaults) { + fields := schema.Fields{ + "scaling": schema.Bool(), + "scale-target": schema.Int(), + } + defaults := schema.Defaults{ + "scaling": false, + "scale-target": 0, + } + return fields, defaults +} + +func provisioningStateV1(valid map[string]interface{}) *provisioningState { + return &provisioningState{ + Version_: 1, + Scaling_: valid["scaling"].(bool), + ScaleTarget_: int(valid["scale-target"].(int64)), + } +} diff --git a/provisioningstate_test.go b/provisioningstate_test.go new file mode 100644 index 0000000..735877b --- /dev/null +++ b/provisioningstate_test.go @@ -0,0 +1,119 @@ +// Copyright 2023 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 ProvisioningStateSerializationSuite struct { + SerializationSuite +} + +var _ = gc.Suite(&ProvisioningStateSerializationSuite{}) + +func (s *ProvisioningStateSerializationSuite) SetUpTest(c *gc.C) { + s.importName = "provisioning-state" + s.importFunc = func(m map[string]interface{}) (interface{}, error) { + return importProvisioningState(m) + } +} + +func (s *ProvisioningStateSerializationSuite) TestNewProvisioningState(c *gc.C) { + args := ProvisioningStateArgs{ + Scaling: true, + ScaleTarget: 10, + } + instance := newProvisioningState(&args) + c.Assert(instance.Scaling(), jc.IsTrue) + c.Assert(instance.ScaleTarget(), gc.Equals, 10) +} + +func minimalProvisioningStateMap() map[interface{}]interface{} { + return map[interface{}]interface{}{ + "version": 1, + "scaling": true, + "scale-target": 10, + } +} + +func minimalProvisioningStateArgs() *ProvisioningStateArgs { + return &ProvisioningStateArgs{ + Scaling: true, + ScaleTarget: 10, + } +} + +func minimalProvisioningState() *provisioningState { + return newProvisioningState(minimalProvisioningStateArgs()) +} + +func maximalProvisioningStateMap() map[interface{}]interface{} { + return map[interface{}]interface{}{ + "version": 1, + "scaling": true, + "scale-target": 10, + } +} + +func maximalProvisioningStateArgs() *ProvisioningStateArgs { + return &ProvisioningStateArgs{ + Scaling: true, + ScaleTarget: 10, + } +} + +func maximalProvisioningState() *provisioningState { + return newProvisioningState(maximalProvisioningStateArgs()) +} + +func (s *ProvisioningStateSerializationSuite) TestMinimalMatches(c *gc.C) { + bytes, err := yaml.Marshal(minimalProvisioningState()) + 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, minimalProvisioningStateMap()) +} + +func (s *ProvisioningStateSerializationSuite) TestMaximalMatches(c *gc.C) { + bytes, err := yaml.Marshal(maximalProvisioningState()) + 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, maximalProvisioningStateMap()) +} + +func (s *ProvisioningStateSerializationSuite) TestParsingSerializedData(c *gc.C) { + initial := maximalProvisioningState() + 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 := importProvisioningState(source) + c.Assert(err, jc.ErrorIsNil) + c.Assert(instance, jc.DeepEquals, initial) +} + +func (s *ProvisioningStateSerializationSuite) exportImportVersion(c *gc.C, origin_ *provisioningState, version int) *provisioningState { + 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 := importProvisioningState(source) + c.Assert(err, jc.ErrorIsNil) + return origin +}