From 6cbefd7e4d12f03beb2802bf63c4fec846edab68 Mon Sep 17 00:00:00 2001 From: Ales Stimec Date: Mon, 16 Dec 2024 12:44:07 +0100 Subject: [PATCH] refactor(addhostedcloud): refactors AddHostedCloud AddHostedCloud expected the host cloud/region to really be host cloud-type/region, which is not what the terraform provider specifies. This PR changes it so that one can specify cloud-name/region-name as well as cloud-type/region-name when adding a host cloud. --- cmd/jimmctl/cmd/addcloudtocontroller_test.go | 2 +- go.sum | 14 --- internal/db/cloud.go | 31 ++++- internal/db/cloud_test.go | 53 +++++++- internal/jimm/cloud.go | 59 +++++++-- internal/jimm/cloud_test.go | 122 ++++++++++++++++++- 6 files changed, 247 insertions(+), 34 deletions(-) diff --git a/cmd/jimmctl/cmd/addcloudtocontroller_test.go b/cmd/jimmctl/cmd/addcloudtocontroller_test.go index e56b09290..50733bd0e 100644 --- a/cmd/jimmctl/cmd/addcloudtocontroller_test.go +++ b/cmd/jimmctl/cmd/addcloudtocontroller_test.go @@ -44,7 +44,7 @@ func (s *addCloudToControllerSuite) SetUpTest(c *gc.C) { Regions: []dbmodel.CloudRegion{{Name: "default", CloudName: "test-cloud"}}, }) c.Assert(err, gc.IsNil) - region, err := s.JIMM.Database.FindRegion(context.Background(), "kubernetes", "default") + region, err := s.JIMM.Database.FindRegionByCloudType(context.Background(), "kubernetes", "default") c.Assert(err, gc.IsNil) // We grant user bob administrator access to JIMM and the added diff --git a/go.sum b/go.sum index 59119ec70..03618b65d 100644 --- a/go.sum +++ b/go.sum @@ -590,10 +590,6 @@ github.com/juju/clock v1.1.1 h1:NvgHG9DQmOpBevgt6gzkyimdWBooLXDy1cQn89qJzBI= github.com/juju/clock v1.1.1/go.mod h1:HIBvJ8kiV/n7UHwKuCkdYL4l/MDECztHR2sAvWDxxf0= github.com/juju/cmd v0.0.0-20171107070456-e74f39857ca0/go.mod h1:yWJQHl73rdSX4DHVKGqkAip+huBslxRwS8m9CrOLq18= github.com/juju/cmd/v3 v3.0.0-20220202061353-b1cc80b193b0/go.mod h1:EoGJiEG+vbMwO9l+Es0SDTlaQPjH6nLcnnc4NfZB3cY= -github.com/juju/cmd/v3 v3.0.16 h1:P/tG4BPtE+MsmECtnFx+KBC8NFi1ESvHbZ8NzMAll1g= -github.com/juju/cmd/v3 v3.0.16/go.mod h1:lGtDvm2BG+FKnIS8yY/vrhxQNX9imnL6bPIYGSTchuI= -github.com/juju/cmd/v3 v3.1.0 h1:u8aXkeZI/BWSK310oOVMgFq66odDY7QGbL+hU1wp1OA= -github.com/juju/cmd/v3 v3.1.0/go.mod h1:lGtDvm2BG+FKnIS8yY/vrhxQNX9imnL6bPIYGSTchuI= github.com/juju/cmd/v3 v3.1.1 h1:mhXjnm/tj1uYieTd6zc+lAyCrXsETrD+WdsTI6gAM3c= github.com/juju/cmd/v3 v3.1.1/go.mod h1:lGtDvm2BG+FKnIS8yY/vrhxQNX9imnL6bPIYGSTchuI= github.com/juju/collections v0.0.0-20200605021417-0d0ec82b7271/go.mod h1:5XgO71dV1JClcOJE+4dzdn4HrI5LiyKd7PlVG6eZYhY= @@ -1145,8 +1141,6 @@ golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= -golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= -golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1267,8 +1261,6 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= -golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1341,8 +1333,6 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= -golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= @@ -1356,8 +1346,6 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= -golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1375,8 +1363,6 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= -golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/internal/db/cloud.go b/internal/db/cloud.go index 59a03c586..1034cfcee 100644 --- a/internal/db/cloud.go +++ b/internal/db/cloud.go @@ -152,9 +152,9 @@ func (d *Database) AddCloudRegion(ctx context.Context, cr *dbmodel.CloudRegion) return nil } -// FindRegion finds a region with the given name on a cloud with the given -// provider type. -func (d *Database) FindRegion(ctx context.Context, providerType, name string) (_ *dbmodel.CloudRegion, err error) { +// FindRegionByCloudType finds a region with the given name on a cloud with the given +// cloud type. +func (d *Database) FindRegionByCloudType(ctx context.Context, providerType, regionName string) (_ *dbmodel.CloudRegion, err error) { const op = errors.Op("db.FindRegion") if err := d.ready(); err != nil { return nil, errors.E(op, err) @@ -166,7 +166,30 @@ func (d *Database) FindRegion(ctx context.Context, providerType, name string) (_ db := d.DB.WithContext(ctx) db = db.Preload("Cloud").Preload("Controllers").Preload("Controllers.Controller") - db = db.Model(dbmodel.CloudRegion{}).Joins("INNER JOIN clouds ON clouds.name = cloud_regions.cloud_name").Where("clouds.type = ? AND cloud_regions.name = ?", providerType, name) + db = db.Model(dbmodel.CloudRegion{}).Joins("INNER JOIN clouds ON clouds.name = cloud_regions.cloud_name").Where("clouds.type = ? AND cloud_regions.name = ?", providerType, regionName) + + var region dbmodel.CloudRegion + if err := db.First(®ion).Error; err != nil { + return nil, errors.E(op, dbError(err)) + } + return ®ion, nil +} + +// FindRegionByCloudName finds a region with the given name on a cloud with the given +// name. +func (d *Database) FindRegionByCloudName(ctx context.Context, cloudName, regionName string) (_ *dbmodel.CloudRegion, err error) { + const op = errors.Op("db.FindRegion") + if err := d.ready(); err != nil { + return nil, errors.E(op, err) + } + + durationObserver := servermon.DurationObserver(servermon.DBQueryDurationHistogram, string(op)) + defer durationObserver() + defer servermon.ErrorCounter(servermon.DBQueryErrorCount, &err, string(op)) + + db := d.DB.WithContext(ctx) + db = db.Preload("Cloud").Preload("Controllers").Preload("Controllers.Controller") + db = db.Model(dbmodel.CloudRegion{}).Joins("INNER JOIN clouds ON clouds.name = cloud_regions.cloud_name").Where("clouds.name = ? AND cloud_regions.name = ?", cloudName, regionName) var region dbmodel.CloudRegion if err := db.First(®ion).Error; err != nil { diff --git a/internal/db/cloud_test.go b/internal/db/cloud_test.go index 5f935d869..f959bc7f3 100644 --- a/internal/db/cloud_test.go +++ b/internal/db/cloud_test.go @@ -217,13 +217,13 @@ func TestFindRegionUnconfiguredDatabase(t *testing.T) { c := qt.New(t) var d db.Database - cr, err := d.FindRegion(context.Background(), "", "") + cr, err := d.FindRegionByCloudType(context.Background(), "", "") c.Check(err, qt.ErrorMatches, `database not configured`) c.Check(errors.ErrorCode(err), qt.Equals, errors.CodeServerConfiguration) c.Check(cr, qt.IsNil) } -func (s *dbSuite) TestFindRegion(c *qt.C) { +func (s *dbSuite) TestFindRegionByCloudType(c *qt.C) { ctx := context.Background() err := s.Database.Migrate(ctx, false) @@ -246,7 +246,7 @@ controllers: `) env.PopulateDB(c, s.Database) - cr, err := s.Database.FindRegion(ctx, "testp", "test-region") + cr, err := s.Database.FindRegionByCloudType(ctx, "testp", "test-region") c.Assert(err, qt.IsNil) c.Check(cr, jimmtest.DBObjectEquals, &dbmodel.CloudRegion{ Cloud: dbmodel.Cloud{ @@ -265,7 +265,52 @@ controllers: }}, }) - _, err = s.Database.FindRegion(ctx, "no-such", "region") + _, err = s.Database.FindRegionByCloudType(ctx, "no-such", "region") + c.Check(errors.ErrorCode(err), qt.Equals, errors.CodeNotFound) +} +func (s *dbSuite) TestFindRegionByCloudName(c *qt.C) { + ctx := context.Background() + + err := s.Database.Migrate(ctx, false) + c.Assert(err, qt.IsNil) + + env := jimmtest.ParseEnvironment(c, `clouds: +- name: test + type: testp + regions: + - name: test-region +controllers: +- name: test + uuid: 00000001-0000-0000-0000-000000000001 + cloud: test + region: test-region + cloud-regions: + - cloud: test + region: test-region + priority: 1 +`) + env.PopulateDB(c, s.Database) + + cr, err := s.Database.FindRegionByCloudName(ctx, "test", "test-region") + c.Assert(err, qt.IsNil) + c.Check(cr, jimmtest.DBObjectEquals, &dbmodel.CloudRegion{ + Cloud: dbmodel.Cloud{ + Name: "test", + Type: "testp", + }, + Name: "test-region", + Controllers: []dbmodel.CloudRegionControllerPriority{{ + Controller: dbmodel.Controller{ + Name: "test", + UUID: "00000001-0000-0000-0000-000000000001", + CloudName: "test", + CloudRegion: "test-region", + }, + Priority: 1, + }}, + }) + + _, err = s.Database.FindRegionByCloudName(ctx, "no-such", "region") c.Check(errors.ErrorCode(err), qt.Equals, errors.CodeNotFound) } diff --git a/internal/jimm/cloud.go b/internal/jimm/cloud.go index 1cb61e9ac..a6e226db2 100644 --- a/internal/jimm/cloud.go +++ b/internal/jimm/cloud.go @@ -177,6 +177,52 @@ func (j *JIMM) AddCloudToController(ctx context.Context, user *openfga.User, con return nil } +func (j *JIMM) determineHostCloudRegion(ctx context.Context, hostCloudRegion string) (*dbmodel.CloudRegion, error) { + // if the hostCloudRegion does not contain '/', then we consider it to be + // the cloud name + if !strings.Contains(hostCloudRegion, "/") { + cl := dbmodel.Cloud{ + Name: hostCloudRegion, + } + if err := j.Database.GetCloud(ctx, &cl); err != nil { + return nil, errors.E(errors.CodeNotFound, "unable to find host cloud %q", hostCloudRegion) + } + if len(cl.Regions) > 1 { + return nil, errors.E(errors.CodeBadRequest, "unable to determine a unique region for host cloud %q - consider specifying the host cloud region", hostCloudRegion) + } + if len(cl.Regions) == 0 { + return nil, errors.E(errors.CodeBadRequest, "the host cloud %q does not have a valid region", hostCloudRegion) + } + return &cl.Regions[0], nil + } + + parts := strings.Split(hostCloudRegion, "/") + if len(parts) != 2 || parts[0] == "" { + return nil, errors.E(errors.CodeBadRequest, fmt.Sprintf("invalid cloud/region format %q", hostCloudRegion)) + } + + findRegionFunctions := []func(context.Context, string, string) (*dbmodel.CloudRegion, error){ + j.Database.FindRegionByCloudType, + j.Database.FindRegionByCloudName, + } + + var err error + var region *dbmodel.CloudRegion + for _, findRegionFunction := range findRegionFunctions { + region, err = findRegionFunction(ctx, parts[0], parts[1]) + if err == nil { + break + } + } + if err != nil { + if errors.ErrorCode(err) == errors.CodeNotFound { + return nil, errors.E(err, errors.CodeNotFound, fmt.Sprintf("unable to find cloud/region %q", hostCloudRegion)) + } + return nil, err + } + return region, nil +} + // AddHostedCloud adds the cloud defined by the given tag and cloud to the // JAAS system. The cloud will be created on a controller running on the // requested host cloud-region and the cloud created there. If the given @@ -213,14 +259,9 @@ func (j *JIMM) AddHostedCloud(ctx context.Context, user *openfga.User, tag names if cloud.HostCloudRegion == "" { return errors.E(op, errors.CodeCloudRegionRequired, "cloud host region not specified") } - parts := strings.SplitN(cloud.HostCloudRegion, "/", 2) - if len(parts) != 2 || parts[0] == "" { - return errors.E(op, errors.CodeBadRequest, fmt.Sprintf("invalid cloud/region format %q", cloud.HostCloudRegion)) - } - region, err := j.Database.FindRegion(ctx, parts[0], parts[1]) - if errors.ErrorCode(err) == errors.CodeNotFound { - return errors.E(op, err, errors.CodeNotFound, fmt.Sprintf("unable to find cloud/region %q", cloud.HostCloudRegion)) - } else if err != nil { + + region, err := j.determineHostCloudRegion(ctx, cloud.HostCloudRegion) + if err != nil { return errors.E(op, err) } @@ -728,7 +769,7 @@ func validateCloudRegion(ctx context.Context, db *db.Database, user *openfga.Use return errors.E(errors.CodeIncompatibleClouds, fmt.Sprintf("cloud host region %q has invalid cloud/region format", cloud.HostCloudRegion)) } - region, err := db.FindRegion(ctx, parts[0], parts[1]) + region, err := db.FindRegionByCloudType(ctx, parts[0], parts[1]) if err != nil { if errors.ErrorCode(err) == errors.CodeNotFound { return errors.E(errors.CodeIncompatibleClouds, fmt.Sprintf("unable to find cloud/region %q", cloud.HostCloudRegion)) diff --git a/internal/jimm/cloud_test.go b/internal/jimm/cloud_test.go index 8c317a124..716efc9da 100644 --- a/internal/jimm/cloud_test.go +++ b/internal/jimm/cloud_test.go @@ -419,6 +419,124 @@ var addHostedCloudTests = []struct { CACertificates: dbmodel.Strings{"CACERT"}, Config: dbmodel.Map{"A": string("a")}, }, +}, { + name: "Success - with cloud name and region", + addCloud: func(context.Context, names.CloudTag, jujuparams.Cloud, bool) error { + return nil + }, + grantCloudAccess: func(context.Context, names.CloudTag, names.UserTag, string) error { + return nil + }, + cloud_: func(_ context.Context, _ names.CloudTag, cld *jujuparams.Cloud) error { + cld.Type = "kubernetes" + cld.HostCloudRegion = "test-cloud/test-region" + cld.AuthTypes = []string{"empty", "userpass"} + cld.Endpoint = "https://example.com" + cld.IdentityEndpoint = "https://example.com/identity" + cld.StorageEndpoint = "https://example.com/storage" + cld.Regions = []jujuparams.CloudRegion{{ + Name: "default", + }} + cld.CACertificates = []string{"CACERT"} + cld.Config = map[string]interface{}{"A": "a"} + cld.RegionConfig = map[string]map[string]interface{}{ + "default": {"B": 2}, + } + return nil + }, + username: "bob@canonical.com", + cloudName: "new-cloud", + cloud: jujuparams.Cloud{ + Type: "kubernetes", + HostCloudRegion: "test-cloud/test-region", + AuthTypes: []string{"empty", "userpass"}, + Endpoint: "https://example.com", + IdentityEndpoint: "https://example.com/identity", + StorageEndpoint: "https://example.com/storage", + }, + expectCloud: dbmodel.Cloud{ + Name: "new-cloud", + Type: "kubernetes", + HostCloudRegion: "test-cloud/test-region", + AuthTypes: []string{"empty", "userpass"}, + Endpoint: "https://example.com", + IdentityEndpoint: "https://example.com/identity", + StorageEndpoint: "https://example.com/storage", + Regions: []dbmodel.CloudRegion{{ + Name: "default", + Config: dbmodel.Map{"B": float64(2)}, + Controllers: []dbmodel.CloudRegionControllerPriority{{ + Controller: dbmodel.Controller{ + Name: "test-controller", + UUID: "00000001-0000-0000-0000-000000000001", + CloudName: "test-cloud", + CloudRegion: "test-region", + }, + Priority: 1, + }}, + }}, + CACertificates: dbmodel.Strings{"CACERT"}, + Config: dbmodel.Map{"A": string("a")}, + }, +}, { + name: "Success - with cloud name", + addCloud: func(context.Context, names.CloudTag, jujuparams.Cloud, bool) error { + return nil + }, + grantCloudAccess: func(context.Context, names.CloudTag, names.UserTag, string) error { + return nil + }, + cloud_: func(_ context.Context, _ names.CloudTag, cld *jujuparams.Cloud) error { + cld.Type = "kubernetes" + cld.HostCloudRegion = "test-cloud/test-region" + cld.AuthTypes = []string{"empty", "userpass"} + cld.Endpoint = "https://example.com" + cld.IdentityEndpoint = "https://example.com/identity" + cld.StorageEndpoint = "https://example.com/storage" + cld.Regions = []jujuparams.CloudRegion{{ + Name: "default", + }} + cld.CACertificates = []string{"CACERT"} + cld.Config = map[string]interface{}{"A": "a"} + cld.RegionConfig = map[string]map[string]interface{}{ + "default": {"B": 2}, + } + return nil + }, + username: "bob@canonical.com", + cloudName: "new-cloud", + cloud: jujuparams.Cloud{ + Type: "kubernetes", + HostCloudRegion: "test-cloud", + AuthTypes: []string{"empty", "userpass"}, + Endpoint: "https://example.com", + IdentityEndpoint: "https://example.com/identity", + StorageEndpoint: "https://example.com/storage", + }, + expectCloud: dbmodel.Cloud{ + Name: "new-cloud", + Type: "kubernetes", + HostCloudRegion: "test-cloud/test-region", + AuthTypes: []string{"empty", "userpass"}, + Endpoint: "https://example.com", + IdentityEndpoint: "https://example.com/identity", + StorageEndpoint: "https://example.com/storage", + Regions: []dbmodel.CloudRegion{{ + Name: "default", + Config: dbmodel.Map{"B": float64(2)}, + Controllers: []dbmodel.CloudRegionControllerPriority{{ + Controller: dbmodel.Controller{ + Name: "test-controller", + UUID: "00000001-0000-0000-0000-000000000001", + CloudName: "test-cloud", + CloudRegion: "test-region", + }, + Priority: 1, + }}, + }}, + CACertificates: dbmodel.Strings{"CACERT"}, + Config: dbmodel.Map{"A": string("a")}, + }, }, { name: "CloudWithReservedName", username: "bob@canonical.com", @@ -481,13 +599,13 @@ var addHostedCloudTests = []struct { cloudName: "new-cloud", cloud: jujuparams.Cloud{ Type: "kubernetes", - HostCloudRegion: "ec2", + HostCloudRegion: "ec2/eu-central-1/2", AuthTypes: []string{"empty", "userpass"}, Endpoint: "https://example.com", IdentityEndpoint: "https://example.com/identity", StorageEndpoint: "https://example.com/storage", }, - expectError: `invalid cloud/region format "ec2"`, + expectError: `invalid cloud/region format "ec2/eu-central-1/2"`, expectErrorCode: errors.CodeBadRequest, }, { name: "HostCloudIsHosted",