github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/apiserver/facades/controller/migrationmaster/facade_test.go (about) 1 // Copyright 2016 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package migrationmaster_test 5 6 import ( 7 "fmt" 8 "time" 9 10 "github.com/juju/description/v5" 11 "github.com/juju/errors" 12 "github.com/juju/names/v5" 13 jc "github.com/juju/testing/checkers" 14 "github.com/juju/utils/v3" 15 "github.com/juju/version/v2" 16 "go.uber.org/mock/gomock" 17 gc "gopkg.in/check.v1" 18 "gopkg.in/macaroon.v2" 19 20 "github.com/juju/juju/apiserver/common" 21 apiservererrors "github.com/juju/juju/apiserver/errors" 22 "github.com/juju/juju/apiserver/facade" 23 "github.com/juju/juju/apiserver/facades/controller/migrationmaster" 24 "github.com/juju/juju/apiserver/facades/controller/migrationmaster/mocks" 25 apiservertesting "github.com/juju/juju/apiserver/testing" 26 "github.com/juju/juju/controller" 27 coremigration "github.com/juju/juju/core/migration" 28 "github.com/juju/juju/core/network" 29 "github.com/juju/juju/core/presence" 30 environscloudspec "github.com/juju/juju/environs/cloudspec" 31 "github.com/juju/juju/rpc/params" 32 "github.com/juju/juju/state" 33 coretesting "github.com/juju/juju/testing" 34 jujuversion "github.com/juju/juju/version" 35 ) 36 37 type Suite struct { 38 coretesting.BaseSuite 39 40 controllerBackend *mocks.MockControllerState 41 backend *mocks.MockBackend 42 precheckBackend *mocks.MockPrecheckBackend 43 44 controllerUUID string 45 modelUUID string 46 model description.Model 47 resources *common.Resources 48 authorizer apiservertesting.FakeAuthorizer 49 cloudSpec environscloudspec.CloudSpec 50 } 51 52 var _ = gc.Suite(&Suite{}) 53 54 func (s *Suite) SetUpTest(c *gc.C) { 55 s.BaseSuite.SetUpTest(c) 56 57 s.controllerUUID = utils.MustNewUUID().String() 58 s.modelUUID = utils.MustNewUUID().String() 59 60 s.model = description.NewModel(description.ModelArgs{ 61 Type: "iaas", 62 Config: map[string]interface{}{"uuid": s.modelUUID}, 63 Owner: names.NewUserTag("admin"), 64 LatestToolsVersion: jujuversion.Current, 65 }) 66 67 s.resources = common.NewResources() 68 s.AddCleanup(func(*gc.C) { s.resources.StopAll() }) 69 70 s.authorizer = apiservertesting.FakeAuthorizer{Controller: true} 71 s.cloudSpec = environscloudspec.CloudSpec{Type: "lxd"} 72 } 73 74 func (s *Suite) TestNotController(c *gc.C) { 75 s.authorizer.Controller = false 76 77 api, err := s.makeAPI() 78 c.Assert(api, gc.IsNil) 79 c.Assert(err, gc.Equals, apiservererrors.ErrPerm) 80 } 81 82 func (s *Suite) TestWatch(c *gc.C) { 83 ctrl := s.setupMocks(c) 84 defer ctrl.Finish() 85 86 // Watcher with an initial event in the pipe. 87 w := mocks.NewMockNotifyWatcher(ctrl) 88 w.EXPECT().Stop().Return(nil).AnyTimes() 89 90 ch := make(chan struct{}, 1) 91 ch <- struct{}{} 92 w.EXPECT().Changes().Return(ch).Times(2) 93 94 s.backend.EXPECT().WatchForMigration().Return(w) 95 96 result := s.mustMakeAPI(c).Watch() 97 c.Assert(result.Error, gc.IsNil) 98 99 resource := s.resources.Get(result.NotifyWatcherId) 100 watcher, _ := resource.(state.NotifyWatcher) 101 c.Assert(watcher, gc.NotNil) 102 103 select { 104 case <-watcher.Changes(): 105 c.Fatalf("initial event not consumed") 106 case <-time.After(coretesting.ShortWait): 107 } 108 } 109 110 func (s *Suite) TestMigrationStatus(c *gc.C) { 111 ctrl := s.setupMocks(c) 112 defer ctrl.Finish() 113 114 password := "secret" 115 116 mig := mocks.NewMockModelMigration(ctrl) 117 118 mac, err := macaroon.New([]byte(password), []byte("id"), "location", macaroon.LatestVersion) 119 c.Assert(err, jc.ErrorIsNil) 120 121 targetInfo := coremigration.TargetInfo{ 122 ControllerTag: names.NewControllerTag(s.controllerUUID), 123 Addrs: []string{"1.1.1.1:1", "2.2.2.2:2"}, 124 CACert: "trust me", 125 AuthTag: names.NewUserTag("admin"), 126 Password: password, 127 Macaroons: []macaroon.Slice{{mac}}, 128 } 129 130 exp := mig.EXPECT() 131 exp.TargetInfo().Return(&targetInfo, nil) 132 exp.Phase().Return(coremigration.IMPORT, nil) 133 exp.ModelUUID().Return(s.modelUUID) 134 exp.Id().Return("ID") 135 now := time.Now() 136 exp.PhaseChangedTime().Return(now) 137 138 s.backend.EXPECT().LatestMigration().Return(mig, nil) 139 140 api := s.mustMakeAPI(c) 141 status, err := api.MigrationStatus() 142 c.Assert(err, jc.ErrorIsNil) 143 144 c.Check(status, gc.DeepEquals, params.MasterMigrationStatus{ 145 Spec: params.MigrationSpec{ 146 ModelTag: names.NewModelTag(s.modelUUID).String(), 147 TargetInfo: params.MigrationTargetInfo{ 148 ControllerTag: names.NewControllerTag(s.controllerUUID).String(), 149 Addrs: []string{"1.1.1.1:1", "2.2.2.2:2"}, 150 CACert: "trust me", 151 AuthTag: names.NewUserTag("admin").String(), 152 Password: password, 153 Macaroons: `[[{"l":"location","i":"id","s64":"qYAr8nQmJzPWKDppxigFtWaNv0dbzX7cJaligz98LLo"}]]`, 154 }, 155 }, 156 MigrationId: "ID", 157 Phase: "IMPORT", 158 PhaseChangedTime: now, 159 }) 160 } 161 162 func (s *Suite) TestModelInfo(c *gc.C) { 163 defer s.setupMocks(c).Finish() 164 165 exp := s.backend.EXPECT() 166 exp.ModelUUID().Return("model-uuid") 167 exp.ModelName().Return("model-name", nil) 168 exp.ModelOwner().Return(names.NewUserTag("owner"), nil) 169 exp.AgentVersion().Return(version.MustParse("1.2.3"), nil) 170 171 mod, err := s.mustMakeAPI(c).ModelInfo() 172 c.Assert(err, jc.ErrorIsNil) 173 174 c.Assert(mod.UUID, gc.Equals, "model-uuid") 175 c.Assert(mod.Name, gc.Equals, "model-name") 176 c.Assert(mod.OwnerTag, gc.Equals, names.NewUserTag("owner").String()) 177 c.Assert(mod.AgentVersion, gc.Equals, version.MustParse("1.2.3")) 178 } 179 180 func (s *Suite) TestSourceControllerInfo(c *gc.C) { 181 defer s.setupMocks(c).Finish() 182 183 exp := s.backend.EXPECT() 184 exp.AllLocalRelatedModels().Return([]string{"related-model-uuid"}, nil) 185 s.backend.EXPECT().ControllerConfig().Return(controller.Config{ 186 controller.ControllerUUIDKey: coretesting.ControllerTag.Id(), 187 controller.ControllerName: "mycontroller", 188 controller.CACertKey: "cacert", 189 }, nil) 190 apiAddr := []network.SpaceHostPorts{{{ 191 SpaceAddress: network.SpaceAddress{ 192 MachineAddress: network.MachineAddress{Value: "10.0.0.1"}, 193 }, 194 NetPort: 666, 195 }}} 196 s.controllerBackend.EXPECT().APIHostPortsForClients().Return(apiAddr, nil) 197 198 info, err := s.mustMakeAPI(c).SourceControllerInfo() 199 c.Assert(err, jc.ErrorIsNil) 200 201 c.Assert(info, jc.DeepEquals, params.MigrationSourceInfo{ 202 LocalRelatedModels: []string{"related-model-uuid"}, 203 ControllerTag: coretesting.ControllerTag.String(), 204 ControllerAlias: "mycontroller", 205 Addrs: []string{"10.0.0.1:666"}, 206 CACert: "cacert", 207 }) 208 } 209 210 func (s *Suite) TestSetPhase(c *gc.C) { 211 ctrl := s.setupMocks(c) 212 defer ctrl.Finish() 213 214 mig := mocks.NewMockModelMigration(ctrl) 215 mig.EXPECT().SetPhase(coremigration.ABORT).Return(nil) 216 217 s.backend.EXPECT().LatestMigration().Return(mig, nil) 218 219 err := s.mustMakeAPI(c).SetPhase(params.SetMigrationPhaseArgs{Phase: "ABORT"}) 220 c.Assert(err, jc.ErrorIsNil) 221 222 } 223 224 func (s *Suite) TestSetPhaseNoMigration(c *gc.C) { 225 defer s.setupMocks(c).Finish() 226 227 s.backend.EXPECT().LatestMigration().Return(nil, errors.New("boom")) 228 229 err := s.mustMakeAPI(c).SetPhase(params.SetMigrationPhaseArgs{Phase: "ABORT"}) 230 c.Assert(err, gc.ErrorMatches, "could not get migration: boom") 231 } 232 233 func (s *Suite) TestSetPhaseBadPhase(c *gc.C) { 234 err := s.mustMakeAPI(c).SetPhase(params.SetMigrationPhaseArgs{Phase: "wat"}) 235 c.Assert(err, gc.ErrorMatches, `invalid phase: "wat"`) 236 } 237 238 func (s *Suite) TestSetPhaseError(c *gc.C) { 239 ctrl := s.setupMocks(c) 240 defer ctrl.Finish() 241 242 mig := mocks.NewMockModelMigration(ctrl) 243 mig.EXPECT().SetPhase(coremigration.ABORT).Return(errors.New("blam")) 244 245 s.backend.EXPECT().LatestMigration().Return(mig, nil) 246 247 err := s.mustMakeAPI(c).SetPhase(params.SetMigrationPhaseArgs{Phase: "ABORT"}) 248 c.Assert(err, gc.ErrorMatches, "failed to set phase: blam") 249 } 250 251 func (s *Suite) TestSetStatusMessage(c *gc.C) { 252 ctrl := s.setupMocks(c) 253 defer ctrl.Finish() 254 255 mig := mocks.NewMockModelMigration(ctrl) 256 mig.EXPECT().SetStatusMessage("foo").Return(nil) 257 258 s.backend.EXPECT().LatestMigration().Return(mig, nil) 259 260 err := s.mustMakeAPI(c).SetStatusMessage(params.SetMigrationStatusMessageArgs{Message: "foo"}) 261 c.Assert(err, jc.ErrorIsNil) 262 } 263 264 func (s *Suite) TestSetStatusMessageNoMigration(c *gc.C) { 265 defer s.setupMocks(c).Finish() 266 267 s.backend.EXPECT().LatestMigration().Return(nil, errors.New("boom")) 268 269 err := s.mustMakeAPI(c).SetStatusMessage(params.SetMigrationStatusMessageArgs{Message: "foo"}) 270 c.Assert(err, gc.ErrorMatches, "could not get migration: boom") 271 } 272 273 func (s *Suite) TestSetStatusMessageError(c *gc.C) { 274 ctrl := s.setupMocks(c) 275 defer ctrl.Finish() 276 277 mig := mocks.NewMockModelMigration(ctrl) 278 mig.EXPECT().SetStatusMessage("foo").Return(errors.New("blam")) 279 280 s.backend.EXPECT().LatestMigration().Return(mig, nil) 281 282 err := s.mustMakeAPI(c).SetStatusMessage(params.SetMigrationStatusMessageArgs{Message: "foo"}) 283 c.Assert(err, gc.ErrorMatches, "failed to set status message: blam") 284 } 285 286 func (s *Suite) TestPrechecksModelError(c *gc.C) { 287 defer s.setupMocks(c).Finish() 288 289 s.precheckBackend.EXPECT().Model().Return(nil, errors.New("boom")) 290 291 err := s.mustMakeAPI(c).Prechecks(params.PrechecksArgs{TargetControllerVersion: version.MustParse("2.9.32")}) 292 c.Assert(err, gc.ErrorMatches, "retrieving model: boom") 293 } 294 295 func (s *Suite) TestProcessRelations(c *gc.C) { 296 api := s.mustMakeAPI(c) 297 err := api.ProcessRelations(params.ProcessRelations{ControllerAlias: "foo"}) 298 c.Assert(err, jc.ErrorIsNil) 299 } 300 301 func (s *Suite) TestExportIAAS(c *gc.C) { 302 s.assertExport(c, "iaas") 303 } 304 305 func (s *Suite) TestExportCAAS(c *gc.C) { 306 s.model = description.NewModel(description.ModelArgs{ 307 Type: "caas", 308 Config: map[string]interface{}{"uuid": s.modelUUID}, 309 Owner: names.NewUserTag("admin"), 310 LatestToolsVersion: jujuversion.Current, 311 }) 312 s.assertExport(c, "caas") 313 } 314 315 func (s *Suite) assertExport(c *gc.C, modelType string) { 316 defer s.setupMocks(c).Finish() 317 318 app := s.model.AddApplication(description.ApplicationArgs{ 319 Tag: names.NewApplicationTag("foo"), 320 CharmURL: "ch:foo-0", 321 }) 322 323 const tools0 = "2.0.0-ubuntu-amd64" 324 const tools1 = "2.0.1-ubuntu-amd64" 325 m := s.model.AddMachine(description.MachineArgs{Id: names.NewMachineTag("9")}) 326 m.SetTools(description.AgentToolsArgs{ 327 Version: version.MustParseBinary(tools1), 328 }) 329 330 res := app.AddResource(description.ResourceArgs{Name: "bin"}) 331 appRev := res.SetApplicationRevision(description.ResourceRevisionArgs{ 332 Revision: 2, 333 Type: "file", 334 Path: "bin.tar.gz", 335 Description: "who knows", 336 Origin: "upload", 337 FingerprintHex: "abcd", 338 Size: 123, 339 Timestamp: time.Now(), 340 Username: "bob", 341 }) 342 csRev := res.SetCharmStoreRevision(description.ResourceRevisionArgs{ 343 Revision: 3, 344 Type: "file", 345 Path: "fink.tar.gz", 346 Description: "knows who", 347 Origin: "store", 348 FingerprintHex: "deaf", 349 Size: 321, 350 Timestamp: time.Now(), 351 Username: "xena", 352 }) 353 354 unit := app.AddUnit(description.UnitArgs{ 355 Tag: names.NewUnitTag("foo/0"), 356 }) 357 unit.SetTools(description.AgentToolsArgs{ 358 Version: version.MustParseBinary(tools0), 359 }) 360 unitRes := unit.AddResource(description.UnitResourceArgs{ 361 Name: "bin", 362 RevisionArgs: description.ResourceRevisionArgs{ 363 Revision: 1, 364 Type: "file", 365 Path: "bin.tar.gz", 366 Description: "nose knows", 367 Origin: "upload", 368 FingerprintHex: "beef", 369 Size: 222, 370 Timestamp: time.Now(), 371 Username: "bambam", 372 }, 373 }) 374 unitRev := unitRes.Revision() 375 376 s.backend.EXPECT().Export(map[string]string{}).Return(s.model, nil) 377 378 serialized, err := s.mustMakeAPI(c).Export() 379 c.Assert(err, jc.ErrorIsNil) 380 381 // We don't want to tie this test the serialisation output (that's 382 // tested elsewhere). Just check that at least one thing we expect 383 // is in the serialised output. 384 c.Check(string(serialized.Bytes), jc.Contains, jujuversion.Current.String()) 385 386 c.Check(serialized.Charms, gc.DeepEquals, []string{"ch:foo-0"}) 387 if modelType == "caas" { 388 c.Check(serialized.Tools, gc.HasLen, 0) 389 } else { 390 c.Check(serialized.Tools, jc.SameContents, []params.SerializedModelTools{ 391 {tools0, "/tools/" + tools0}, 392 {tools1, "/tools/" + tools1}, 393 }) 394 } 395 c.Check(serialized.Resources, gc.DeepEquals, []params.SerializedModelResource{{ 396 Application: "foo", 397 Name: "bin", 398 ApplicationRevision: params.SerializedModelResourceRevision{ 399 Revision: appRev.Revision(), 400 Type: appRev.Type(), 401 Path: appRev.Path(), 402 Description: appRev.Description(), 403 Origin: appRev.Origin(), 404 FingerprintHex: appRev.FingerprintHex(), 405 Size: appRev.Size(), 406 Timestamp: appRev.Timestamp(), 407 Username: appRev.Username(), 408 }, 409 CharmStoreRevision: params.SerializedModelResourceRevision{ 410 Revision: csRev.Revision(), 411 Type: csRev.Type(), 412 Path: csRev.Path(), 413 Description: csRev.Description(), 414 Origin: csRev.Origin(), 415 FingerprintHex: csRev.FingerprintHex(), 416 Size: csRev.Size(), 417 Timestamp: csRev.Timestamp(), 418 Username: csRev.Username(), 419 }, 420 UnitRevisions: map[string]params.SerializedModelResourceRevision{ 421 "foo/0": { 422 Revision: unitRev.Revision(), 423 Type: unitRev.Type(), 424 Path: unitRev.Path(), 425 Description: unitRev.Description(), 426 Origin: unitRev.Origin(), 427 FingerprintHex: unitRev.FingerprintHex(), 428 Size: unitRev.Size(), 429 Timestamp: unitRev.Timestamp(), 430 Username: unitRev.Username(), 431 }, 432 }, 433 }}) 434 } 435 436 func (s *Suite) TestReap(c *gc.C) { 437 ctrl := s.setupMocks(c) 438 defer ctrl.Finish() 439 440 mig := mocks.NewMockModelMigration(ctrl) 441 442 exp := s.backend.EXPECT() 443 exp.LatestMigration().Return(mig, nil) 444 445 // Reaping should set the migration phase to DONE - otherwise 446 // there's a race between the migrationmaster worker updating the 447 // phase and being stopped because the model's gone. This leaves 448 // the migration as active in the source controller, which will 449 // prevent the model from being migrated back. 450 exp.RemoveExportingModelDocs().Return(nil) 451 mig.EXPECT().SetPhase(coremigration.DONE).Return(nil) 452 453 err := s.mustMakeAPI(c).Reap() 454 c.Check(err, jc.ErrorIsNil) 455 456 } 457 458 func (s *Suite) TestReapError(c *gc.C) { 459 ctrl := s.setupMocks(c) 460 defer ctrl.Finish() 461 462 mig := mocks.NewMockModelMigration(ctrl) 463 464 s.backend.EXPECT().LatestMigration().Return(mig, nil) 465 s.backend.EXPECT().RemoveExportingModelDocs().Return(errors.New("boom")) 466 467 err := s.mustMakeAPI(c).Reap() 468 c.Check(err, gc.ErrorMatches, "boom") 469 } 470 471 func (s *Suite) TestWatchMinionReports(c *gc.C) { 472 ctrl := s.setupMocks(c) 473 defer ctrl.Finish() 474 475 // Watcher with an initial event in the pipe. 476 w := mocks.NewMockNotifyWatcher(ctrl) 477 w.EXPECT().Stop().Return(nil).AnyTimes() 478 479 ch := make(chan struct{}, 1) 480 ch <- struct{}{} 481 w.EXPECT().Changes().Return(ch).Times(2) 482 483 mig := mocks.NewMockModelMigration(ctrl) 484 mig.EXPECT().WatchMinionReports().Return(w, nil) 485 486 s.backend.EXPECT().LatestMigration().Return(mig, nil) 487 488 result := s.mustMakeAPI(c).WatchMinionReports() 489 c.Assert(result.Error, gc.IsNil) 490 491 resource := s.resources.Get(result.NotifyWatcherId) 492 watcher, _ := resource.(state.NotifyWatcher) 493 c.Assert(watcher, gc.NotNil) 494 495 select { 496 case <-watcher.Changes(): 497 c.Fatalf("initial event not consumed") 498 case <-time.After(coretesting.ShortWait): 499 } 500 } 501 502 func (s *Suite) TestMinionReports(c *gc.C) { 503 ctrl := s.setupMocks(c) 504 defer ctrl.Finish() 505 506 // Report 16 unknowns. 507 // These are in reverse order in order to test sorting. 508 unknown := make([]names.Tag, 0, 16) 509 for i := cap(unknown) - 1; i >= 0; i-- { 510 unknown = append(unknown, names.NewMachineTag(fmt.Sprintf("%d", i))) 511 } 512 m50c0 := names.NewMachineTag("50/lxd/0") 513 m50c1 := names.NewMachineTag("50/lxd/1") 514 m50 := names.NewMachineTag("50") 515 m51 := names.NewMachineTag("51") 516 m52 := names.NewMachineTag("52") 517 u0 := names.NewUnitTag("foo/0") 518 u1 := names.NewUnitTag("foo/1") 519 520 mig := mocks.NewMockModelMigration(ctrl) 521 522 exp := mig.EXPECT() 523 exp.Id().Return("ID") 524 exp.Phase().Return(coremigration.IMPORT, nil) 525 exp.MinionReports().Return(&state.MinionReports{ 526 Succeeded: []names.Tag{m50, m51, u0}, 527 Failed: []names.Tag{u1, m52, m50c1, m50c0}, 528 Unknown: unknown, 529 }, nil) 530 531 s.backend.EXPECT().LatestMigration().Return(mig, nil) 532 533 reports, err := s.mustMakeAPI(c).MinionReports() 534 c.Assert(err, jc.ErrorIsNil) 535 536 // Expect the sample of unknowns to be in order and be limited to 537 // the first 10. 538 expectedSample := make([]string, 0, 10) 539 for i := 0; i < cap(expectedSample); i++ { 540 expectedSample = append(expectedSample, names.NewMachineTag(fmt.Sprintf("%d", i)).String()) 541 } 542 c.Assert(reports, gc.DeepEquals, params.MinionReports{ 543 MigrationId: "ID", 544 Phase: "IMPORT", 545 SuccessCount: 3, 546 UnknownCount: len(unknown), 547 UnknownSample: expectedSample, 548 Failed: []string{ 549 // Note sorting. 550 m50c0.String(), 551 m50c1.String(), 552 m52.String(), 553 u1.String(), 554 }, 555 }) 556 } 557 558 func (s *Suite) TestMinionReportTimeout(c *gc.C) { 559 ctrl := s.setupMocks(c) 560 defer ctrl.Finish() 561 562 timeout := "30s" 563 564 s.backend.EXPECT().ControllerConfig().Return(controller.Config{ 565 controller.MigrationMinionWaitMax: timeout, 566 }, nil) 567 568 res, err := s.mustMakeAPI(c).MinionReportTimeout() 569 c.Assert(err, jc.ErrorIsNil) 570 c.Assert(res.Error, gc.IsNil) 571 c.Check(res.Result, gc.Equals, timeout) 572 } 573 574 func (s *Suite) setupMocks(c *gc.C) *gomock.Controller { 575 ctrl := gomock.NewController(c) 576 577 s.controllerBackend = mocks.NewMockControllerState(ctrl) 578 s.backend = mocks.NewMockBackend(ctrl) 579 s.precheckBackend = mocks.NewMockPrecheckBackend(ctrl) 580 return ctrl 581 } 582 583 func (s *Suite) mustMakeAPI(c *gc.C) *migrationmaster.API { 584 api, err := s.makeAPI() 585 c.Assert(err, jc.ErrorIsNil) 586 return api 587 } 588 589 func (s *Suite) makeAPI() (*migrationmaster.API, error) { 590 return migrationmaster.NewAPI( 591 s.controllerBackend, 592 s.backend, 593 s.precheckBackend, 594 nil, // pool 595 s.resources, 596 s.authorizer, 597 &stubPresence{}, 598 func(names.ModelTag) (environscloudspec.CloudSpec, error) { return s.cloudSpec, nil }, 599 stubLeadership{}, 600 ) 601 } 602 603 type stubPresence struct{} 604 605 func (f *stubPresence) ModelPresence(modelUUID string) facade.ModelPresence { 606 return f 607 } 608 609 func (f *stubPresence) AgentStatus(agent string) (presence.Status, error) { 610 return presence.Alive, nil 611 } 612 613 type stubLeadership struct{} 614 615 func (stubLeadership) Leaders() (map[string]string, error) { 616 return map[string]string{}, nil 617 }