github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/migration/migration_test.go (about) 1 // Copyright 2016 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package migration_test 5 6 import ( 7 "bytes" 8 "fmt" 9 "io" 10 "net/url" 11 "time" 12 13 "github.com/juju/charm/v12" 14 "github.com/juju/description/v5" 15 "github.com/juju/errors" 16 "github.com/juju/testing" 17 jc "github.com/juju/testing/checkers" 18 "github.com/juju/utils/v3" 19 "github.com/juju/version/v2" 20 gc "gopkg.in/check.v1" 21 22 "github.com/juju/juju/core/leadership" 23 coremigration "github.com/juju/juju/core/migration" 24 "github.com/juju/juju/core/resources" 25 resourcetesting "github.com/juju/juju/core/resources/testing" 26 "github.com/juju/juju/migration" 27 "github.com/juju/juju/provider/dummy" 28 "github.com/juju/juju/state" 29 statetesting "github.com/juju/juju/state/testing" 30 coretesting "github.com/juju/juju/testing" 31 "github.com/juju/juju/testing/factory" 32 "github.com/juju/juju/tools" 33 ) 34 35 type ImportSuite struct { 36 statetesting.StateSuite 37 } 38 39 var _ = gc.Suite(&ImportSuite{}) 40 41 func (s *ImportSuite) SetUpTest(c *gc.C) { 42 // Specify the config to use for the controller model before 43 // calling SetUpTest of the StateSuite, otherwise we get 44 // coretesting.ModelConfig(c). The default provider type 45 // specified in the coretesting.ModelConfig function is one that 46 // isn't registered as a valid provider. For our tests here we 47 // need a real registered provider, so we use the dummy provider. 48 // NOTE: make a better test provider. 49 s.InitialConfig = coretesting.CustomModelConfig(c, dummy.SampleConfig()) 50 s.StateSuite.SetUpTest(c) 51 } 52 53 func (s *ImportSuite) TestBadBytes(c *gc.C) { 54 bytes := []byte("not a model") 55 controller := state.NewController(s.StatePool) 56 model, st, err := migration.ImportModel(controller, fakeGetClaimer, bytes) 57 c.Check(st, gc.IsNil) 58 c.Check(model, gc.IsNil) 59 c.Assert(err, gc.ErrorMatches, "yaml: unmarshal errors:\n.*") 60 } 61 62 func (s *ImportSuite) exportImport(c *gc.C, leaders map[string]string, getClaimer migration.ClaimerFunc) *state.State { 63 model, err := s.State.Export(leaders) 64 c.Assert(err, jc.ErrorIsNil) 65 66 // Update the config values in the exported model for different values for 67 // "state-port", "api-port", and "ca-cert". Also give the model a new UUID 68 // and name, so we can import it nicely. 69 uuid := utils.MustNewUUID().String() 70 model.UpdateConfig(map[string]interface{}{ 71 "name": "new-model", 72 "uuid": uuid, 73 }) 74 75 bytes, err := description.Serialize(model) 76 c.Check(err, jc.ErrorIsNil) 77 78 controller := state.NewController(s.StatePool) 79 dbModel, dbState, err := migration.ImportModel(controller, getClaimer, bytes) 80 c.Assert(err, jc.ErrorIsNil) 81 s.AddCleanup(func(*gc.C) { dbState.Close() }) 82 83 dbConfig, err := dbModel.Config() 84 c.Assert(err, jc.ErrorIsNil) 85 c.Assert(dbConfig.UUID(), gc.Equals, uuid) 86 c.Assert(dbConfig.Name(), gc.Equals, "new-model") 87 return dbState 88 } 89 90 func (s *ImportSuite) TestImportModel(c *gc.C) { 91 s.exportImport(c, map[string]string{}, fakeGetClaimer) 92 } 93 94 func (s *ImportSuite) TestImportsLeadership(c *gc.C) { 95 s.makeApplicationWithUnits(c, "wordpress", 3) 96 s.makeApplicationWithUnits(c, "mysql", 2) 97 leaders := map[string]string{"wordpress": "wordpress/1"} 98 99 var ( 100 claimer fakeClaimer 101 modelUUID string 102 ) 103 dbState := s.exportImport(c, leaders, func(uuid string) (leadership.Claimer, error) { 104 modelUUID = uuid 105 return &claimer, nil 106 }) 107 c.Assert(modelUUID, gc.Equals, dbState.ModelUUID()) 108 c.Assert(claimer.stub.Calls(), gc.HasLen, 1) 109 claimer.stub.CheckCall(c, 0, "ClaimLeadership", "wordpress", "wordpress/1", time.Minute) 110 } 111 112 func (s *ImportSuite) makeApplicationWithUnits(c *gc.C, applicationname string, count int) { 113 units := make([]*state.Unit, count) 114 application := s.Factory.MakeApplication(c, &factory.ApplicationParams{ 115 Name: applicationname, 116 Charm: s.Factory.MakeCharm(c, &factory.CharmParams{ 117 Name: applicationname, 118 }), 119 }) 120 for i := 0; i < count; i++ { 121 units[i] = s.Factory.MakeUnit(c, &factory.UnitParams{ 122 Application: application, 123 }) 124 } 125 } 126 127 func (s *ImportSuite) TestUploadBinariesConfigValidate(c *gc.C) { 128 type T migration.UploadBinariesConfig // alias for brevity 129 130 check := func(modify func(*T), missing string) { 131 config := T{ 132 CharmDownloader: struct{ migration.CharmDownloader }{}, 133 CharmUploader: struct{ migration.CharmUploader }{}, 134 ToolsDownloader: struct{ migration.ToolsDownloader }{}, 135 ToolsUploader: struct{ migration.ToolsUploader }{}, 136 ResourceDownloader: struct{ migration.ResourceDownloader }{}, 137 ResourceUploader: struct{ migration.ResourceUploader }{}, 138 } 139 modify(&config) 140 realConfig := migration.UploadBinariesConfig(config) 141 c.Check(realConfig.Validate(), gc.ErrorMatches, fmt.Sprintf("missing %s not valid", missing)) 142 } 143 144 check(func(c *T) { c.CharmDownloader = nil }, "CharmDownloader") 145 check(func(c *T) { c.CharmUploader = nil }, "CharmUploader") 146 check(func(c *T) { c.ToolsDownloader = nil }, "ToolsDownloader") 147 check(func(c *T) { c.ToolsUploader = nil }, "ToolsUploader") 148 check(func(c *T) { c.ResourceDownloader = nil }, "ResourceDownloader") 149 check(func(c *T) { c.ResourceUploader = nil }, "ResourceUploader") 150 } 151 152 func (s *ImportSuite) TestBinariesMigration(c *gc.C) { 153 downloader := &fakeDownloader{} 154 uploader := &fakeUploader{ 155 tools: make(map[version.Binary]string), 156 resources: make(map[string]string), 157 } 158 159 toolsMap := map[version.Binary]string{ 160 version.MustParseBinary("2.1.0-ubuntu-amd64"): "/tools/0", 161 version.MustParseBinary("2.0.0-ubuntu-amd64"): "/tools/1", 162 } 163 164 app0Res := resourcetesting.NewResource(c, nil, "blob0", "app0", "blob0").Resource 165 app1Res := resourcetesting.NewResource(c, nil, "blob1", "app1", "blob1").Resource 166 app1UnitRes := app1Res 167 app1UnitRes.Revision = 1 168 app2Res := resourcetesting.NewPlaceholderResource(c, "blob2", "app2") 169 resources := []coremigration.SerializedModelResource{ 170 {ApplicationRevision: app0Res}, 171 { 172 ApplicationRevision: app1Res, 173 UnitRevisions: map[string]resources.Resource{"app1/99": app1UnitRes}, 174 }, 175 {ApplicationRevision: app2Res}, 176 } 177 178 config := migration.UploadBinariesConfig{ 179 Charms: []string{ 180 // These 2 are out of order. Rev 2 must be uploaded first. 181 "local:trusty/magic-10", 182 "local:trusty/magic-2", 183 "ch:trusty/postgresql-42", 184 }, 185 CharmDownloader: downloader, 186 CharmUploader: uploader, 187 Tools: toolsMap, 188 ToolsDownloader: downloader, 189 ToolsUploader: uploader, 190 Resources: resources, 191 ResourceDownloader: downloader, 192 ResourceUploader: uploader, 193 } 194 err := migration.UploadBinaries(config) 195 c.Assert(err, jc.ErrorIsNil) 196 197 expectedCurls := []string{ 198 // Note ordering. 199 "ch:trusty/postgresql-42", 200 "local:trusty/magic-2", 201 "local:trusty/magic-10", 202 } 203 c.Assert(downloader.curls, jc.DeepEquals, expectedCurls) 204 c.Assert(uploader.curls, jc.DeepEquals, expectedCurls) 205 206 expectedRefs := []string{ 207 "postgresql-a77196f", 208 "magic-d348864", 209 "magic-5f44d22", 210 } 211 c.Assert(uploader.charmRefs, jc.DeepEquals, expectedRefs) 212 213 c.Assert(downloader.uris, jc.SameContents, []string{ 214 "/tools/0", 215 "/tools/1", 216 }) 217 c.Assert(uploader.tools, jc.DeepEquals, toolsMap) 218 219 c.Assert(downloader.resources, jc.SameContents, []string{ 220 "app0/blob0", 221 "app1/blob1", 222 }) 223 c.Assert(uploader.resources, jc.DeepEquals, map[string]string{ 224 "app0/blob0": "blob0", 225 "app1/blob1": "blob1", 226 }) 227 c.Assert(uploader.unitResources, jc.SameContents, []string{"app1/99-blob1"}) 228 } 229 230 func (s *ImportSuite) TestWrongCharmURLAssigned(c *gc.C) { 231 downloader := &fakeDownloader{} 232 uploader := &fakeUploader{ 233 reassignCharmURL: true, 234 } 235 236 config := migration.UploadBinariesConfig{ 237 Charms: []string{"local:foo/bar-2"}, 238 CharmDownloader: downloader, 239 CharmUploader: uploader, 240 ToolsDownloader: downloader, 241 ToolsUploader: uploader, 242 ResourceDownloader: downloader, 243 ResourceUploader: uploader, 244 } 245 err := migration.UploadBinaries(config) 246 c.Assert(err, gc.ErrorMatches, 247 "cannot upload charms: charm local:foo/bar-2 unexpectedly assigned local:foo/bar-100") 248 } 249 250 type fakeDownloader struct { 251 curls []string 252 uris []string 253 resources []string 254 } 255 256 func (d *fakeDownloader) OpenCharm(curl string) (io.ReadCloser, error) { 257 d.curls = append(d.curls, curl) 258 // Return the charm URL string as the fake charm content 259 return io.NopCloser(bytes.NewReader([]byte(curl + " content"))), nil 260 } 261 262 func (d *fakeDownloader) OpenURI(uri string, query url.Values) (io.ReadCloser, error) { 263 if query != nil { 264 panic("query should be empty") 265 } 266 d.uris = append(d.uris, uri) 267 // Return the URI string as fake content 268 return io.NopCloser(bytes.NewReader([]byte(uri))), nil 269 } 270 271 func (d *fakeDownloader) OpenResource(app, name string) (io.ReadCloser, error) { 272 d.resources = append(d.resources, app+"/"+name) 273 // Use the resource name as the content. 274 return io.NopCloser(bytes.NewReader([]byte(name))), nil 275 } 276 277 type fakeUploader struct { 278 tools map[version.Binary]string 279 curls []string 280 charmRefs []string 281 resources map[string]string 282 unitResources []string 283 reassignCharmURL bool 284 } 285 286 func (f *fakeUploader) UploadTools(r io.ReadSeeker, v version.Binary) (tools.List, error) { 287 data, err := io.ReadAll(r) 288 if err != nil { 289 return nil, errors.Trace(err) 290 } 291 f.tools[v] = string(data) 292 return tools.List{&tools.Tools{Version: v}}, nil 293 } 294 295 func (f *fakeUploader) UploadCharm(curl string, charmRef string, r io.ReadSeeker) (string, error) { 296 data, err := io.ReadAll(r) 297 if err != nil { 298 return "", errors.Trace(err) 299 } 300 if string(data) != curl+" content" { 301 panic(fmt.Sprintf("unexpected charm body for %s: %s", curl, data)) 302 } 303 f.curls = append(f.curls, curl) 304 f.charmRefs = append(f.charmRefs, charmRef) 305 306 outU := curl 307 if f.reassignCharmURL { 308 outU = charm.MustParseURL(outU).WithRevision(100).String() 309 } 310 return outU, nil 311 } 312 313 func (f *fakeUploader) UploadResource(res resources.Resource, r io.ReadSeeker) error { 314 body, err := io.ReadAll(r) 315 if err != nil { 316 return errors.Trace(err) 317 } 318 f.resources[res.ApplicationID+"/"+res.Name] = string(body) 319 return nil 320 } 321 322 func (f *fakeUploader) SetPlaceholderResource(res resources.Resource) error { 323 f.resources[res.ApplicationID+"/"+res.Name] = "<placeholder>" 324 return nil 325 } 326 327 func (f *fakeUploader) SetUnitResource(unit string, res resources.Resource) error { 328 f.unitResources = append(f.unitResources, unit+"-"+res.Name) 329 return nil 330 } 331 332 func fakeGetClaimer(string) (leadership.Claimer, error) { 333 return &fakeClaimer{}, nil 334 } 335 336 type fakeClaimer struct { 337 leadership.Claimer 338 stub testing.Stub 339 } 340 341 func (c *fakeClaimer) ClaimLeadership(application, unit string, duration time.Duration) error { 342 c.stub.AddCall("ClaimLeadership", application, unit, duration) 343 return c.stub.NextErr() 344 }