diff --git a/Makefile b/Makefile index 14fbb36..e3bbce7 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -PROJECT := github.com/juju/description/v5 +PROJECT := github.com/juju/description/v6 .PHONY: check-licence check-go check diff --git a/go.mod b/go.mod index 4145d0c..ebfe2cd 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/juju/description/v5 +module github.com/juju/description/v6 go 1.21 diff --git a/interfaces.go b/interfaces.go index 5dd6e0f..721d651 100644 --- a/interfaces.go +++ b/interfaces.go @@ -22,6 +22,7 @@ type AgentTools interface { // Space represents a network space, which is a named collection of subnets. type Space interface { Id() string + UUID() string Name() string Public() bool ProviderID() string @@ -204,6 +205,7 @@ type StorageInstanceConstraints struct { // Subnet represents a network subnet. type Subnet interface { ID() string + UUID() string ProviderId() string ProviderNetworkId() string ProviderSpaceId() string @@ -212,6 +214,7 @@ type Subnet interface { AvailabilityZones() []string IsPublic() bool SpaceID() string + SpaceUUID() string SpaceName() string FanLocalUnderlay() string FanOverlay() string diff --git a/model.go b/model.go index 5729d19..74a178f 100644 --- a/model.go +++ b/model.go @@ -565,7 +565,7 @@ func (m *model) AddSpace(args SpaceArgs) Space { func (m *model) setSpaces(spaceList []*space) { m.Spaces_ = spaces{ - Version: 2, + Version: 3, Spaces_: spaceList, } } @@ -1234,18 +1234,18 @@ func (m *model) validateStorage(validationCtx *validationContext) error { // validateSubnets makes sure that any spaces referenced by subnets exist. func (m *model) validateSubnets() error { - spaceIDs := set.NewStrings() + spaceUUIDs := set.NewStrings() for _, space := range m.Spaces_.Spaces_ { - spaceIDs.Add(space.Id()) + spaceUUIDs.Add(space.UUID()) } for _, subnet := range m.Subnets_.Subnets_ { // space "0" is the new, in juju 2.7, default space, // created with each new model. - if subnet.SpaceID() == "" || subnet.SpaceID() == "0" { + if subnet.SpaceUUID() == "" || subnet.SpaceUUID() == "0" { continue } - if !spaceIDs.Contains(subnet.SpaceID()) { - return errors.Errorf("subnet %q references non-existent space %q", subnet.CIDR(), subnet.SpaceID()) + if !spaceUUIDs.Contains(subnet.SpaceUUID()) { + return errors.Errorf("subnet %q references non-existent space %q", subnet.CIDR(), subnet.SpaceUUID()) } } diff --git a/model_test.go b/model_test.go index 241b827..b3d09b6 100644 --- a/model_test.go +++ b/model_test.go @@ -150,7 +150,7 @@ func (s *ModelSerializationSuite) TestVersions(c *gc.C) { c.Assert(initial.Relations_.Version, gc.Equals, len(relationFieldsFuncs)) c.Assert(initial.RemoteEntities_.Version, gc.Equals, len(remoteEntityFieldsFuncs)) c.Assert(initial.RemoteApplications_.Version, gc.Equals, len(remoteApplicationFieldsFuncs)) - c.Assert(initial.Spaces_.Version, gc.Equals, len(spaceDeserializationFuncs)) + c.Assert(initial.Spaces_.Version, gc.Equals, len(spaceFieldsFuncs)) c.Assert(initial.Volumes_.Version, gc.Equals, len(volumeDeserializationFuncs)) c.Assert(initial.FirewallRules_.Version, gc.Equals, len(firewallRuleFieldsFuncs)) c.Assert(initial.OfferConnections_.Version, gc.Equals, len(offerConnectionDeserializationFuncs)) @@ -594,11 +594,11 @@ func (s *ModelSerializationSuite) TestModelSerializationWithRelationNetworks(c * func (s *ModelSerializationSuite) TestModelValidationChecksSubnets(c *gc.C) { model := s.newModel(ModelArgs{Owner: names.NewUserTag("owner")}) - model.AddSubnet(SubnetArgs{CIDR: "10.0.0.0/24", SpaceID: "3"}) + model.AddSubnet(SubnetArgs{CIDR: "10.0.0.0/24", SpaceUUID: "deadbeef-1bad-500d-9000-4b1d0d06f00d"}) model.AddSubnet(SubnetArgs{CIDR: "10.0.1.0/24"}) err := model.Validate() - c.Assert(err, gc.ErrorMatches, `subnet "10.0.0.0/24" references non-existent space "3"`) - model.AddSpace(SpaceArgs{Id: "3"}) + c.Assert(err, gc.ErrorMatches, `subnet "10.0.0.0/24" references non-existent space "deadbeef-1bad-500d-9000-4b1d0d06f00d"`) + model.AddSpace(SpaceArgs{UUID: "deadbeef-1bad-500d-9000-4b1d0d06f00d"}) err = model.Validate() c.Assert(err, jc.ErrorIsNil) } @@ -1206,9 +1206,9 @@ func (s *ModelSerializationSuite) TestVersion1IgnoresRemoteApplications(c *gc.C) func (s *ModelSerializationSuite) TestSpaces(c *gc.C) { initial := s.newModel(ModelArgs{Owner: names.NewUserTag("owner")}) - space := initial.AddSpace(SpaceArgs{Id: "1", Name: "special"}) + space := initial.AddSpace(SpaceArgs{UUID: "deadbeef-1bad-500d-9000-4b1d0d06f00d", Name: "special"}) c.Assert(space.Name(), gc.Equals, "special") - c.Assert(space.Id(), gc.Equals, "1") + c.Assert(space.UUID(), gc.Equals, "deadbeef-1bad-500d-9000-4b1d0d06f00d") spaces := initial.Spaces() c.Assert(spaces, gc.HasLen, 1) diff --git a/package_test.go b/package_test.go index cbdb19b..a4c29fb 100644 --- a/package_test.go +++ b/package_test.go @@ -25,7 +25,7 @@ var _ = gc.Suite(&ImportTest{}) func (*ImportTest) TestImports(c *gc.C) { imps, err := jtesting.FindImports( - "github.com/juju/description/v5", + "github.com/juju/description/v6", "github.com/juju/juju/") c.Assert(err, jc.ErrorIsNil) // This package brings in nothing else from juju/juju diff --git a/remoteapplication_test.go b/remoteapplication_test.go index 9b5b137..4ad95c8 100644 --- a/remoteapplication_test.go +++ b/remoteapplication_test.go @@ -77,8 +77,10 @@ func minimalRemoteApplicationMapWithoutStatus() map[interface{}]interface{} { "subnets": []interface{}{map[interface{}]interface{}{ "cidr": "2.3.4.0/24", "subnet-id": "", + "uuid": "", "is-public": false, "space-id": "", + "space-uuid": "", "space-name": "", "vlan-tag": 0, "provider-id": "juju-subnet-1", diff --git a/remotespace_test.go b/remotespace_test.go index bccff8d..8de0155 100644 --- a/remotespace_test.go +++ b/remotespace_test.go @@ -40,8 +40,10 @@ func minimalRemoteSpaceMap() map[interface{}]interface{} { "subnets": []interface{}{map[interface{}]interface{}{ "cidr": "2.3.4.0/24", "subnet-id": "", + "uuid": "", "is-public": false, "space-id": "", + "space-uuid": "", "space-name": "a-space", "vlan-tag": 64, "provider-id": "juju-subnet-1", diff --git a/space.go b/space.go index 78ce9ff..f8d22ca 100644 --- a/space.go +++ b/space.go @@ -15,6 +15,7 @@ type spaces struct { type space struct { Id_ string `yaml:"id"` + UUID_ string `yaml:"uuid"` Name_ string `yaml:"name"` Public_ bool `yaml:"public"` ProviderID_ string `yaml:"provider-id,omitempty"` @@ -24,6 +25,7 @@ type space struct { // type that supports the Space interface. type SpaceArgs struct { Id string + UUID string Name string Public bool ProviderID string @@ -32,6 +34,7 @@ type SpaceArgs struct { func newSpace(args SpaceArgs) *space { return &space{ Id_: args.Id, + UUID_: args.UUID, Name_: args.Name, Public_: args.Public, ProviderID_: args.ProviderID, @@ -43,6 +46,11 @@ func (s *space) Id() string { return s.Id_ } +// UUID implements Space. +func (s *space) UUID() string { + return s.UUID_ +} + // Name implements Space. func (s *space) Name() string { return s.Name_ @@ -67,22 +75,28 @@ func importSpaces(source map[string]interface{}) ([]*space, error) { valid := coerced.(map[string]interface{}) version := int(valid["version"].(int64)) - importFunc, ok := spaceDeserializationFuncs[version] + getFields, ok := spaceFieldsFuncs[version] if !ok { return nil, errors.NotValidf("version %d", version) } sourceList := valid["spaces"].([]interface{}) - return importSpaceList(sourceList, importFunc) + return importSpaceList(sourceList, schema.FieldMap(getFields()), version) } -func importSpaceList(sourceList []interface{}, importFunc spaceDeserializationFunc) ([]*space, error) { +func importSpaceList(sourceList []interface{}, checker schema.Checker, version int) ([]*space, error) { result := make([]*space, 0, len(sourceList)) for i, value := range sourceList { source, ok := value.(map[string]interface{}) if !ok { return nil, errors.Errorf("unexpected value for space %d, %T", i, value) } - space, err := importFunc(source) + coerced, err := checker.Coerce(source, nil) + + if err != nil { + return nil, errors.Annotatef(err, "space %d v%d schema check failed", i, version) + } + valid := coerced.(map[string]interface{}) + space, err := newSpaceFromValid(valid, version) if err != nil { return nil, errors.Annotatef(err, "space %d", i) } @@ -91,51 +105,26 @@ func importSpaceList(sourceList []interface{}, importFunc spaceDeserializationFu return result, nil } -type spaceDeserializationFunc func(map[string]interface{}) (*space, error) - -var spaceDeserializationFuncs = map[int]spaceDeserializationFunc{ - 1: importSpaceV1, - 2: importSpaceV2, -} - -func importSpaceV1(source map[string]interface{}) (*space, error) { - fields, defaults := spaceV1Fields() - checker := schema.FieldMap(fields, defaults) - - coerced, err := checker.Coerce(source, nil) - if err != nil { - return nil, errors.Annotatef(err, "space v1 schema check failed") - } - valid := coerced.(map[string]interface{}) - // From here we know that the map returned from the schema coercion - // contains fields of the right type. - - return &space{ +func newSpaceFromValid(valid map[string]interface{}, version int) (*space, error) { + result := space{ Name_: valid["name"].(string), Public_: valid["public"].(bool), ProviderID_: valid["provider-id"].(string), - }, nil -} - -func importSpaceV2(source map[string]interface{}) (*space, error) { - fields, defaults := spaceV1Fields() - fields["id"] = schema.String() - checker := schema.FieldMap(fields, defaults) - - coerced, err := checker.Coerce(source, nil) - if err != nil { - return nil, errors.Annotatef(err, "space v2 schema check failed") } - valid := coerced.(map[string]interface{}) - // From here we know that the map returned from the schema coercion - // contains fields of the right type. + // id was added in V2 and removed in V3. + if version == 2 { + result.Id_ = valid["id"].(string) + } + if version >= 3 { + result.UUID_ = valid["uuid"].(string) + } + return &result, nil +} - return &space{ - Id_: valid["id"].(string), - Name_: valid["name"].(string), - Public_: valid["public"].(bool), - ProviderID_: valid["provider-id"].(string), - }, nil +var spaceFieldsFuncs = map[int]fieldsFunc{ + 1: spaceV1Fields, + 2: spaceV2Fields, + 3: spaceV3Fields, } func spaceV1Fields() (schema.Fields, schema.Defaults) { @@ -150,3 +139,18 @@ func spaceV1Fields() (schema.Fields, schema.Defaults) { } return fields, defaults } + +func spaceV2Fields() (schema.Fields, schema.Defaults) { + fields, defaults := spaceV1Fields() + fields["id"] = schema.String() + + return fields, defaults +} + +func spaceV3Fields() (schema.Fields, schema.Defaults) { + fields, defaults := spaceV2Fields() + fields["uuid"] = schema.String() + delete(fields, "id") + + return fields, defaults +} diff --git a/space_test.go b/space_test.go index c607ab7..8c0f523 100644 --- a/space_test.go +++ b/space_test.go @@ -35,6 +35,7 @@ func (s *SpaceSerializationSuite) TestNewSpace(c *gc.C) { } space := newSpace(args) c.Assert(space.Id(), gc.Equals, "") + c.Assert(space.UUID(), gc.Equals, "") c.Assert(space.Name(), gc.Equals, args.Name) c.Assert(space.Public(), gc.Equals, args.Public) c.Assert(space.ProviderID(), gc.Equals, args.ProviderID) @@ -92,3 +93,30 @@ func (s *SpaceSerializationSuite) TestParsingSerializedDataV2(c *gc.C) { c.Assert(spaces, jc.DeepEquals, initial.Spaces_) } + +func (s *SpaceSerializationSuite) TestParsingSerializedDataV3(c *gc.C) { + initial := spaces{ + Version: 3, + Spaces_: []*space{ + newSpace(SpaceArgs{ + UUID: "018ea48e-c6a6-7d51-ae76-9bfee4a6b6dd", + Name: "special", + Public: true, + ProviderID: "magic", + }), + newSpace(SpaceArgs{Name: "foo"}), + }, + } + + 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) + + spaces, err := importSpaces(source) + c.Assert(err, jc.ErrorIsNil) + + c.Assert(spaces, jc.DeepEquals, initial.Spaces_) +} diff --git a/subnet.go b/subnet.go index df89e89..61b655c 100644 --- a/subnet.go +++ b/subnet.go @@ -15,6 +15,7 @@ type subnets struct { type subnet struct { ID_ string `yaml:"subnet-id"` + UUID_ string `yaml:"uuid"` ProviderId_ string `yaml:"provider-id,omitempty"` ProviderNetworkId_ string `yaml:"provider-network-id,omitempty"` ProviderSpaceId_ string `yaml:"provider-space-id,omitempty"` @@ -24,6 +25,7 @@ type subnet struct { AvailabilityZones_ []string `yaml:"availability-zones"` IsPublic_ bool `yaml:"is-public"` SpaceID_ string `yaml:"space-id"` + SpaceUUID_ string `yaml:"space-uuid"` // SpaceName is now deprecated and not used past version 4. SpaceName_ string `yaml:"space-name"` @@ -36,6 +38,7 @@ type subnet struct { // new internal subnet type that supports the Subnet interface. type SubnetArgs struct { ID string + UUID string ProviderId string ProviderNetworkId string ProviderSpaceId string @@ -48,6 +51,7 @@ type SubnetArgs struct { SpaceName string SpaceID string + SpaceUUID string FanLocalUnderlay string FanOverlay string } @@ -55,11 +59,13 @@ type SubnetArgs struct { func newSubnet(args SubnetArgs) *subnet { return &subnet{ ID_: args.ID, + UUID_: args.UUID, ProviderId_: args.ProviderId, ProviderNetworkId_: args.ProviderNetworkId, ProviderSpaceId_: args.ProviderSpaceId, SpaceName_: args.SpaceName, SpaceID_: args.SpaceID, + SpaceUUID_: args.SpaceUUID, CIDR_: args.CIDR, VLANTag_: args.VLANTag, AvailabilityZones_: args.AvailabilityZones, @@ -74,6 +80,11 @@ func (s *subnet) ID() string { return s.ID_ } +// UUID implements Subnet. +func (s *subnet) UUID() string { + return s.UUID_ +} + // ProviderId implements Subnet. func (s *subnet) ProviderId() string { return s.ProviderId_ @@ -99,6 +110,11 @@ func (s *subnet) SpaceID() string { return s.SpaceID_ } +// SpaceUUID implements Subnet. +func (s *subnet) SpaceUUID() string { + return s.SpaceUUID_ +} + // CIDR implements Subnet. func (s *subnet) CIDR() string { return s.CIDR_ @@ -174,6 +190,7 @@ var subnetFieldsFuncs = map[int]fieldsFunc{ 4: subnetV4Fields, 5: subnetV5Fields, 6: subnetV6Fields, + 7: subnetV7Fields, } func newSubnetFromValid(valid map[string]interface{}, version int) (*subnet, error) { @@ -206,6 +223,10 @@ func newSubnetFromValid(valid map[string]interface{}, version int) (*subnet, err if version >= 6 { result.ID_ = valid["subnet-id"].(string) } + if version >= 7 { + result.UUID_ = valid["uuid"].(string) + result.SpaceUUID_ = valid["space-uuid"].(string) + } return &result, nil } @@ -265,3 +286,10 @@ func subnetV6Fields() (schema.Fields, schema.Defaults) { fields["subnet-id"] = schema.String() return fields, defaults } + +func subnetV7Fields() (schema.Fields, schema.Defaults) { + fields, defaults := subnetV6Fields() + fields["uuid"] = schema.String() + fields["space-uuid"] = schema.String() + return fields, defaults +} diff --git a/subnet_test.go b/subnet_test.go index 770a40b..69c079c 100644 --- a/subnet_test.go +++ b/subnet_test.go @@ -46,6 +46,8 @@ func testSubnet(version int) *subnet { args.IsPublic = false case 5: args.SpaceName = "" + case 6: + args.ID = "" } return newSubnet(args) } @@ -293,3 +295,16 @@ func (s *SubnetSerializationSuite) TestParsingV6Minimal(c *gc.C) { subnet := s.exportImport(c, original, 6) c.Assert(subnet, jc.DeepEquals, original) } + +func (s *SubnetSerializationSuite) TestParsingV7Full(c *gc.C) { + original := testSubnet(5) + original.ID_ = "42" + subnet := s.exportImport(c, original, 6) + c.Assert(subnet, jc.DeepEquals, original) +} + +func (s *SubnetSerializationSuite) TestParsingV7Minimal(c *gc.C) { + original := newSubnet(SubnetArgs{CIDR: "10.0.1.0/24"}) + subnet := s.exportImport(c, original, 6) + c.Assert(subnet, jc.DeepEquals, original) +}