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