github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/apiserver/facades/client/application/application_unit_test.go (about) 1 // Copyright 2016 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package application_test 5 6 import ( 7 "strings" 8 9 "github.com/juju/errors" 10 "github.com/juju/testing" 11 jc "github.com/juju/testing/checkers" 12 "github.com/juju/utils" 13 gc "gopkg.in/check.v1" 14 "gopkg.in/juju/charm.v6" 15 csparams "gopkg.in/juju/charmrepo.v3/csclient/params" 16 "gopkg.in/juju/names.v2" 17 18 apitesting "github.com/juju/juju/api/testing" 19 "github.com/juju/juju/apiserver/common" 20 "github.com/juju/juju/apiserver/facades/client/application" 21 "github.com/juju/juju/apiserver/params" 22 apiservertesting "github.com/juju/juju/apiserver/testing" 23 "github.com/juju/juju/caas" 24 k8s "github.com/juju/juju/caas/kubernetes/provider" 25 coreapplication "github.com/juju/juju/core/application" 26 "github.com/juju/juju/core/constraints" 27 "github.com/juju/juju/core/crossmodel" 28 "github.com/juju/juju/core/instance" 29 "github.com/juju/juju/core/status" 30 "github.com/juju/juju/environs" 31 "github.com/juju/juju/network" 32 "github.com/juju/juju/state" 33 "github.com/juju/juju/storage" 34 "github.com/juju/juju/storage/provider" 35 coretesting "github.com/juju/juju/testing" 36 ) 37 38 type ApplicationSuite struct { 39 testing.IsolationSuite 40 coretesting.JujuOSEnvSuite 41 backend mockBackend 42 endpoints []state.Endpoint 43 relation mockRelation 44 application mockApplication 45 storagePoolManager *mockStoragePoolManager 46 47 caasBroker *mockCaasBroker 48 env environs.Environ 49 blockChecker mockBlockChecker 50 authorizer apiservertesting.FakeAuthorizer 51 api *application.APIv9 52 deployParams map[string]application.DeployApplicationParams 53 } 54 55 var _ = gc.Suite(&ApplicationSuite{}) 56 57 func (s *ApplicationSuite) setAPIUser(c *gc.C, user names.UserTag) { 58 s.authorizer.Tag = user 59 s.storagePoolManager = &mockStoragePoolManager{storageType: k8s.K8s_ProviderType} 60 s.caasBroker = &mockCaasBroker{storageClassName: "storage"} 61 api, err := application.NewAPIBase( 62 &s.backend, 63 &s.backend, 64 s.authorizer, 65 &s.blockChecker, 66 names.NewModelTag(utils.MustNewUUID().String()), 67 state.ModelTypeIAAS, 68 "caasmodel", 69 func(application.Charm) *state.Charm { 70 return &state.Charm{} 71 }, 72 func(_ application.ApplicationDeployer, p application.DeployApplicationParams) (application.Application, error) { 73 s.deployParams[p.ApplicationName] = p 74 return nil, nil 75 }, 76 s.storagePoolManager, 77 common.NewResources(), 78 s.caasBroker, 79 ) 80 c.Assert(err, jc.ErrorIsNil) 81 s.api = &application.APIv9{api} 82 } 83 84 func (s *ApplicationSuite) SetUpTest(c *gc.C) { 85 s.IsolationSuite.SetUpTest(c) 86 s.JujuOSEnvSuite.SetUpTest(c) 87 s.authorizer = apiservertesting.FakeAuthorizer{ 88 Tag: names.NewUserTag("admin"), 89 } 90 s.deployParams = make(map[string]application.DeployApplicationParams) 91 s.env = &mockEnviron{} 92 s.endpoints = []state.Endpoint{ 93 {ApplicationName: "postgresql"}, 94 {ApplicationName: "bar"}, 95 } 96 s.relation = mockRelation{tag: names.NewRelationTag("wordpress:db mysql:db")} 97 s.backend = mockBackend{ 98 controllers: make(map[string]crossmodel.ControllerInfo), 99 applications: map[string]*mockApplication{ 100 "postgresql": { 101 name: "postgresql", 102 series: "quantal", 103 subordinate: false, 104 charm: &mockCharm{ 105 config: &charm.Config{ 106 Options: map[string]charm.Option{ 107 "stringOption": {Type: "string"}, 108 "intOption": {Type: "int", Default: int(123)}, 109 }, 110 }, 111 meta: &charm.Meta{Name: "charm-postgresql"}, 112 }, 113 units: []*mockUnit{ 114 { 115 name: "postgresql/0", 116 tag: names.NewUnitTag("postgresql/0"), 117 machineId: "machine-0", 118 }, 119 { 120 name: "postgresql/1", 121 tag: names.NewUnitTag("postgresql/1"), 122 machineId: "machine-1", 123 }, 124 }, 125 addedUnit: mockUnit{ 126 tag: names.NewUnitTag("postgresql/99"), 127 }, 128 lxdProfileUpgradeChanges: make(chan struct{}), 129 constraints: constraints.MustParse("arch=amd64 mem=4G cores=1 root-disk=8G"), 130 channel: csparams.DevelopmentChannel, 131 bindings: map[string]string{ 132 "juju-info": "myspace", 133 }, 134 }, 135 "postgresql-subordinate": { 136 name: "postgresql-subordinate", 137 series: "quantal", 138 subordinate: true, 139 charm: &mockCharm{ 140 config: &charm.Config{ 141 Options: map[string]charm.Option{ 142 "stringOption": {Type: "string"}, 143 "intOption": {Type: "int", Default: int(123)}, 144 }, 145 }, 146 meta: &charm.Meta{Name: "charm-postgresql-subordinate"}, 147 }, 148 units: []*mockUnit{ 149 {tag: names.NewUnitTag("postgresql-subordinate/0")}, 150 {tag: names.NewUnitTag("postgresql-subordinate/1")}, 151 }, 152 addedUnit: mockUnit{ 153 tag: names.NewUnitTag("postgresql-subordinate/99"), 154 }, 155 lxdProfileUpgradeChanges: make(chan struct{}), 156 constraints: constraints.MustParse("arch=amd64 mem=4G cores=1 root-disk=8G"), 157 channel: csparams.DevelopmentChannel, 158 }, 159 }, 160 remoteApplications: map[string]application.RemoteApplication{ 161 "hosted-db2": &mockRemoteApplication{}, 162 }, 163 charm: &mockCharm{ 164 meta: &charm.Meta{}, config: &charm.Config{ 165 Options: map[string]charm.Option{ 166 "stringOption": {Type: "string"}, 167 "intOption": {Type: "int", Default: int(123)}}, 168 }, 169 }, 170 endpoints: &s.endpoints, 171 relations: map[int]*mockRelation{ 172 123: &s.relation, 173 }, 174 offerConnections: make(map[string]application.OfferConnection), 175 unitStorageAttachments: map[string][]state.StorageAttachment{ 176 "postgresql/0": { 177 &mockStorageAttachment{ 178 unit: names.NewUnitTag("postgresql/0"), 179 storage: names.NewStorageTag("pgdata/0"), 180 }, 181 &mockStorageAttachment{ 182 unit: names.NewUnitTag("foo/0"), 183 storage: names.NewStorageTag("pgdata/1"), 184 }, 185 }, 186 }, 187 storageInstances: map[string]*mockStorage{ 188 "pgdata/0": { 189 tag: names.NewStorageTag("pgdata/0"), 190 owner: names.NewUnitTag("postgresql/0"), 191 }, 192 "pgdata/1": { 193 tag: names.NewStorageTag("pgdata/1"), 194 owner: names.NewUnitTag("foo/0"), 195 }, 196 }, 197 storageInstanceFilesystems: map[string]*mockFilesystem{ 198 "pgdata/0": {detachable: true}, 199 "pgdata/1": {detachable: false}, 200 }, 201 machines: map[string]*mockMachine{ 202 "machine-0": {id: "0", upgradeCharmProfileComplete: ""}, 203 "machine-1": {id: "1", upgradeCharmProfileComplete: "not required"}, 204 }, 205 } 206 s.blockChecker = mockBlockChecker{} 207 s.setAPIUser(c, names.NewUserTag("admin")) 208 } 209 210 func (s *ApplicationSuite) TearDownTest(c *gc.C) { 211 s.JujuOSEnvSuite.TearDownTest(c) 212 s.IsolationSuite.TearDownTest(c) 213 } 214 215 func (s *ApplicationSuite) TestSetCharmStorageConstraints(c *gc.C) { 216 toUint64Ptr := func(v uint64) *uint64 { 217 return &v 218 } 219 err := s.api.SetCharm(params.ApplicationSetCharm{ 220 ApplicationName: "postgresql", 221 CharmURL: "cs:postgresql", 222 StorageConstraints: map[string]params.StorageConstraints{ 223 "a": {}, 224 "b": {Pool: "radiant"}, 225 "c": {Size: toUint64Ptr(123)}, 226 "d": {Count: toUint64Ptr(456)}, 227 }, 228 }) 229 c.Assert(err, jc.ErrorIsNil) 230 s.backend.CheckCallNames(c, "Application", "Charm") 231 app := s.backend.applications["postgresql"] 232 app.CheckCallNames(c, "SetCharmProfile", "SetCharm") 233 app.CheckCall(c, 0, "SetCharmProfile", "cs:postgresql") 234 app.CheckCall(c, 1, "SetCharm", state.SetCharmConfig{ 235 Charm: &state.Charm{}, 236 StorageConstraints: map[string]state.StorageConstraints{ 237 "a": {}, 238 "b": {Pool: "radiant"}, 239 "c": {Size: 123}, 240 "d": {Count: 456}, 241 }, 242 }) 243 } 244 245 func (s *ApplicationSuite) TestSetCharmConfigSettings(c *gc.C) { 246 err := s.api.SetCharm(params.ApplicationSetCharm{ 247 ApplicationName: "postgresql", 248 CharmURL: "cs:postgresql", 249 ConfigSettings: map[string]string{"stringOption": "value"}, 250 }) 251 c.Assert(err, jc.ErrorIsNil) 252 s.backend.CheckCallNames(c, "Application", "Charm") 253 s.backend.charm.CheckCallNames(c, "Config") 254 app := s.backend.applications["postgresql"] 255 app.CheckCallNames(c, "SetCharmProfile", "SetCharm") 256 app.CheckCall(c, 0, "SetCharmProfile", "cs:postgresql") 257 app.CheckCall(c, 1, "SetCharm", state.SetCharmConfig{ 258 Charm: &state.Charm{}, 259 ConfigSettings: charm.Settings{"stringOption": "value"}, 260 }) 261 } 262 263 func (s *ApplicationSuite) TestSetCharmConfigSettingsYAML(c *gc.C) { 264 err := s.api.SetCharm(params.ApplicationSetCharm{ 265 ApplicationName: "postgresql", 266 CharmURL: "cs:postgresql", 267 ConfigSettingsYAML: ` 268 postgresql: 269 stringOption: value 270 `, 271 }) 272 c.Assert(err, jc.ErrorIsNil) 273 s.backend.CheckCallNames(c, "Application", "Charm") 274 s.backend.charm.CheckCallNames(c, "Config") 275 app := s.backend.applications["postgresql"] 276 app.CheckCallNames(c, "SetCharmProfile", "SetCharm") 277 app.CheckCall(c, 0, "SetCharmProfile", "cs:postgresql") 278 app.CheckCall(c, 1, "SetCharm", state.SetCharmConfig{ 279 Charm: &state.Charm{}, 280 ConfigSettings: charm.Settings{"stringOption": "value"}, 281 }) 282 } 283 284 func (s *ApplicationSuite) TestDestroyRelation(c *gc.C) { 285 err := s.api.DestroyRelation(params.DestroyRelation{Endpoints: []string{"a", "b"}}) 286 c.Assert(err, jc.ErrorIsNil) 287 s.blockChecker.CheckCallNames(c, "RemoveAllowed") 288 s.backend.CheckCallNames(c, "InferEndpoints", "EndpointsRelation") 289 s.backend.CheckCall(c, 0, "InferEndpoints", []string{"a", "b"}) 290 s.relation.CheckCallNames(c, "Destroy") 291 } 292 293 func (s *ApplicationSuite) TestDestroyRelationNoRelationsFound(c *gc.C) { 294 s.backend.SetErrors(nil, errors.New("no relations found")) 295 err := s.api.DestroyRelation(params.DestroyRelation{Endpoints: []string{"a", "b"}}) 296 c.Assert(err, gc.ErrorMatches, "no relations found") 297 } 298 299 func (s *ApplicationSuite) TestDestroyRelationRelationNotFound(c *gc.C) { 300 s.backend.SetErrors(nil, errors.NotFoundf(`relation "a:b c:d"`)) 301 err := s.api.DestroyRelation(params.DestroyRelation{Endpoints: []string{"a:b", "c:d"}}) 302 c.Assert(err, gc.ErrorMatches, `relation "a:b c:d" not found`) 303 } 304 305 func (s *ApplicationSuite) TestBlockRemoveDestroyRelation(c *gc.C) { 306 s.blockChecker.SetErrors(errors.New("postgresql")) 307 err := s.api.DestroyRelation(params.DestroyRelation{Endpoints: []string{"a", "b"}}) 308 c.Assert(err, gc.ErrorMatches, "postgresql") 309 s.blockChecker.CheckCallNames(c, "RemoveAllowed") 310 s.backend.CheckNoCalls(c) 311 s.relation.CheckNoCalls(c) 312 } 313 314 func (s *ApplicationSuite) TestDestroyRelationId(c *gc.C) { 315 err := s.api.DestroyRelation(params.DestroyRelation{RelationId: 123}) 316 c.Assert(err, jc.ErrorIsNil) 317 s.blockChecker.CheckCallNames(c, "RemoveAllowed") 318 s.backend.CheckCallNames(c, "Relation") 319 s.backend.CheckCall(c, 0, "Relation", 123) 320 s.relation.CheckCallNames(c, "Destroy") 321 } 322 323 func (s *ApplicationSuite) TestDestroyRelationIdRelationNotFound(c *gc.C) { 324 s.backend.SetErrors(errors.NotFoundf(`relation "123"`)) 325 err := s.api.DestroyRelation(params.DestroyRelation{RelationId: 123}) 326 c.Assert(err, gc.ErrorMatches, `relation "123" not found`) 327 } 328 329 func (s *ApplicationSuite) TestDestroyApplication(c *gc.C) { 330 results, err := s.api.DestroyApplication(params.DestroyApplicationsParams{ 331 Applications: []params.DestroyApplicationParams{{ 332 ApplicationTag: "application-postgresql", 333 }}, 334 }) 335 c.Assert(err, jc.ErrorIsNil) 336 c.Assert(results.Results, gc.HasLen, 1) 337 c.Assert(results.Results[0], jc.DeepEquals, params.DestroyApplicationResult{ 338 Info: ¶ms.DestroyApplicationInfo{ 339 DestroyedUnits: []params.Entity{ 340 {Tag: "unit-postgresql-0"}, 341 {Tag: "unit-postgresql-1"}, 342 }, 343 DetachedStorage: []params.Entity{ 344 {Tag: "storage-pgdata-0"}, 345 }, 346 DestroyedStorage: []params.Entity{ 347 {Tag: "storage-pgdata-1"}, 348 }, 349 }, 350 }) 351 352 s.backend.CheckCallNames(c, 353 "Application", 354 "UnitStorageAttachments", 355 "StorageInstance", 356 "StorageInstance", 357 "StorageInstanceFilesystem", 358 "StorageInstanceFilesystem", 359 "UnitStorageAttachments", 360 "ApplyOperation", 361 ) 362 s.backend.CheckCall(c, 7, "ApplyOperation", &state.DestroyApplicationOperation{}) 363 } 364 365 func (s *ApplicationSuite) TestDestroyApplicationDestroyStorage(c *gc.C) { 366 results, err := s.api.DestroyApplication(params.DestroyApplicationsParams{ 367 Applications: []params.DestroyApplicationParams{{ 368 ApplicationTag: "application-postgresql", 369 DestroyStorage: true, 370 }}, 371 }) 372 c.Assert(err, jc.ErrorIsNil) 373 c.Assert(results.Results, gc.HasLen, 1) 374 c.Assert(results.Results[0], jc.DeepEquals, params.DestroyApplicationResult{ 375 Info: ¶ms.DestroyApplicationInfo{ 376 DestroyedUnits: []params.Entity{ 377 {Tag: "unit-postgresql-0"}, 378 {Tag: "unit-postgresql-1"}, 379 }, 380 DestroyedStorage: []params.Entity{ 381 {Tag: "storage-pgdata-0"}, 382 {Tag: "storage-pgdata-1"}, 383 }, 384 }, 385 }) 386 387 s.backend.CheckCallNames(c, 388 "Application", 389 "UnitStorageAttachments", 390 "StorageInstance", 391 "StorageInstance", 392 "UnitStorageAttachments", 393 "ApplyOperation", 394 ) 395 s.backend.CheckCall(c, 5, "ApplyOperation", &state.DestroyApplicationOperation{ 396 DestroyStorage: true, 397 }) 398 } 399 400 func (s *ApplicationSuite) TestDestroyApplicationNotFound(c *gc.C) { 401 delete(s.backend.applications, "postgresql") 402 results, err := s.api.DestroyApplication(params.DestroyApplicationsParams{ 403 Applications: []params.DestroyApplicationParams{ 404 {ApplicationTag: "application-postgresql"}, 405 }, 406 }) 407 c.Assert(err, jc.ErrorIsNil) 408 c.Assert(results.Results, gc.HasLen, 1) 409 c.Assert(results.Results[0], jc.DeepEquals, params.DestroyApplicationResult{ 410 Error: ¶ms.Error{ 411 Code: params.CodeNotFound, 412 Message: `application "postgresql" not found`, 413 }, 414 }) 415 } 416 417 func (s *ApplicationSuite) TestDestroyConsumedApplication(c *gc.C) { 418 results, err := s.api.DestroyConsumedApplications(params.DestroyConsumedApplicationsParams{ 419 Applications: []params.DestroyConsumedApplicationParams{{ApplicationTag: "application-hosted-db2"}}, 420 }) 421 c.Assert(err, jc.ErrorIsNil) 422 c.Assert(results.Results, gc.HasLen, 1) 423 c.Assert(results.Results[0], jc.DeepEquals, params.ErrorResult{}) 424 425 s.backend.CheckCallNames(c, "RemoteApplication") 426 app := s.backend.remoteApplications["hosted-db2"] 427 app.(*mockRemoteApplication).CheckCallNames(c, "Destroy") 428 } 429 430 func (s *ApplicationSuite) TestDestroyConsumedApplicationNotFound(c *gc.C) { 431 delete(s.backend.remoteApplications, "hosted-db2") 432 results, err := s.api.DestroyConsumedApplications(params.DestroyConsumedApplicationsParams{ 433 Applications: []params.DestroyConsumedApplicationParams{{ApplicationTag: "application-hosted-db2"}}, 434 }) 435 c.Assert(err, jc.ErrorIsNil) 436 c.Assert(results.Results, gc.HasLen, 1) 437 c.Assert(results.Results[0], jc.DeepEquals, params.ErrorResult{ 438 Error: ¶ms.Error{ 439 Code: params.CodeNotFound, 440 Message: `remote application "hosted-db2" not found`, 441 }, 442 }) 443 } 444 445 func (s *ApplicationSuite) TestDestroyUnit(c *gc.C) { 446 results, err := s.api.DestroyUnit(params.DestroyUnitsParams{ 447 Units: []params.DestroyUnitParams{ 448 {UnitTag: "unit-postgresql-0"}, 449 { 450 UnitTag: "unit-postgresql-1", 451 DestroyStorage: true, 452 }, 453 }, 454 }) 455 c.Assert(err, jc.ErrorIsNil) 456 c.Assert(results.Results, gc.HasLen, 2) 457 c.Assert(results.Results, jc.DeepEquals, []params.DestroyUnitResult{{ 458 Info: ¶ms.DestroyUnitInfo{ 459 DetachedStorage: []params.Entity{ 460 {Tag: "storage-pgdata-0"}, 461 }, 462 DestroyedStorage: []params.Entity{ 463 {Tag: "storage-pgdata-1"}, 464 }, 465 }, 466 }, { 467 Info: ¶ms.DestroyUnitInfo{}, 468 }}) 469 470 s.backend.CheckCallNames(c, 471 "Unit", 472 "UnitStorageAttachments", 473 "StorageInstance", 474 "StorageInstance", 475 "StorageInstanceFilesystem", 476 "StorageInstanceFilesystem", 477 "ApplyOperation", 478 479 "Unit", 480 "UnitStorageAttachments", 481 "ApplyOperation", 482 ) 483 s.backend.CheckCall(c, 6, "ApplyOperation", &state.DestroyUnitOperation{}) 484 s.backend.CheckCall(c, 9, "ApplyOperation", &state.DestroyUnitOperation{ 485 DestroyStorage: true, 486 }) 487 } 488 489 func (s *ApplicationSuite) TestDeployAttachStorage(c *gc.C) { 490 args := params.ApplicationsDeploy{ 491 Applications: []params.ApplicationDeploy{{ 492 ApplicationName: "foo", 493 CharmURL: "local:foo-0", 494 NumUnits: 1, 495 AttachStorage: []string{"storage-foo-0"}, 496 }, { 497 ApplicationName: "bar", 498 CharmURL: "local:bar-1", 499 NumUnits: 2, 500 AttachStorage: []string{"storage-bar-0"}, 501 }, { 502 ApplicationName: "baz", 503 CharmURL: "local:baz-2", 504 NumUnits: 1, 505 AttachStorage: []string{"volume-baz-0"}, 506 }}, 507 } 508 results, err := s.api.Deploy(args) 509 c.Assert(err, jc.ErrorIsNil) 510 c.Assert(results.Results, gc.HasLen, 3) 511 c.Assert(results.Results[0].Error, gc.IsNil) 512 c.Assert(results.Results[1].Error, gc.ErrorMatches, "AttachStorage is non-empty, but NumUnits is 2") 513 c.Assert(results.Results[2].Error, gc.ErrorMatches, `"volume-baz-0" is not a valid volume tag`) 514 } 515 516 func (s *ApplicationSuite) TestDeployCAASModel(c *gc.C) { 517 application.SetModelType(s.api, state.ModelTypeCAAS) 518 s.backend.charm = &mockCharm{ 519 meta: &charm.Meta{}, 520 config: &charm.Config{ 521 Options: map[string]charm.Option{ 522 "stringOption": {Type: "string"}, 523 "intOption": {Type: "int", Default: int(123)}, 524 }, 525 }, 526 } 527 args := params.ApplicationsDeploy{ 528 Applications: []params.ApplicationDeploy{{ 529 ApplicationName: "foo", 530 CharmURL: "local:foo-0", 531 NumUnits: 1, 532 Config: map[string]string{"kubernetes-service-annotations": "a=b c="}, 533 ConfigYAML: "foo:\n stringOption: fred\n kubernetes-service-type: NodeIP", 534 }, { 535 ApplicationName: "foobar", 536 CharmURL: "local:foobar-0", 537 NumUnits: 1, 538 Config: map[string]string{"kubernetes-service-type": "ClusterIP", "intOption": "2"}, 539 ConfigYAML: "foobar:\n intOption: 1\n kubernetes-service-type: NodeIP\n kubernetes-ingress-ssl-redirect: true", 540 }, { 541 ApplicationName: "bar", 542 CharmURL: "local:bar-0", 543 NumUnits: 1, 544 AttachStorage: []string{"storage-bar-0"}, 545 }, { 546 ApplicationName: "baz", 547 CharmURL: "local:baz-0", 548 NumUnits: 1, 549 Placement: []*instance.Placement{{}, {}}, 550 }}, 551 } 552 results, err := s.api.Deploy(args) 553 c.Assert(err, jc.ErrorIsNil) 554 c.Assert(results.Results, gc.HasLen, 4) 555 c.Assert(results.Results[0].Error, gc.IsNil) 556 c.Assert(results.Results[1].Error, gc.IsNil) 557 c.Assert(results.Results[2].Error, gc.ErrorMatches, "AttachStorage may not be specified for caas models") 558 c.Assert(results.Results[3].Error, gc.ErrorMatches, "only 1 placement directive is supported for caas models, got 2") 559 560 c.Assert(s.deployParams["foo"].ApplicationConfig.Attributes()["kubernetes-service-type"], gc.Equals, "NodeIP") 561 // Check parsing of k8s service annotations. 562 c.Assert(s.deployParams["foo"].ApplicationConfig.Attributes()["kubernetes-service-annotations"], jc.DeepEquals, map[string]string{"a": "b", "c": ""}) 563 c.Assert(s.deployParams["foobar"].ApplicationConfig.Attributes()["kubernetes-service-type"], gc.Equals, "ClusterIP") 564 c.Assert(s.deployParams["foobar"].ApplicationConfig.Attributes()["kubernetes-ingress-ssl-redirect"], gc.Equals, true) 565 c.Assert(s.deployParams["foobar"].CharmConfig, jc.DeepEquals, charm.Settings{"intOption": int64(2)}) 566 } 567 568 func (s *ApplicationSuite) TestDeployCAASModelNoOperatorStorage(c *gc.C) { 569 application.SetModelType(s.api, state.ModelTypeCAAS) 570 s.storagePoolManager.SetErrors(errors.NotFoundf("pool")) 571 s.caasBroker.SetErrors(errors.NotFoundf("storage class")) 572 args := params.ApplicationsDeploy{ 573 Applications: []params.ApplicationDeploy{{ 574 ApplicationName: "foo", 575 CharmURL: "local:foo-0", 576 NumUnits: 1, 577 }}, 578 } 579 result, err := s.api.Deploy(args) 580 c.Assert(err, jc.ErrorIsNil) 581 c.Assert(result.Results, gc.HasLen, 1) 582 msg := result.OneError().Error() 583 c.Assert(strings.Replace(msg, "\n", "", -1), gc.Matches, `deploying a Kubernetes application requires a suitable storage class.*`) 584 } 585 586 func (s *ApplicationSuite) TestDeployCAASModelDefaultOperatorStorageClass(c *gc.C) { 587 application.SetModelType(s.api, state.ModelTypeCAAS) 588 s.storagePoolManager.SetErrors(errors.NotFoundf("pool")) 589 args := params.ApplicationsDeploy{ 590 Applications: []params.ApplicationDeploy{{ 591 ApplicationName: "foo", 592 CharmURL: "local:foo-0", 593 NumUnits: 1, 594 }}, 595 } 596 result, err := s.api.Deploy(args) 597 c.Assert(err, jc.ErrorIsNil) 598 c.Assert(result.Results, gc.HasLen, 1) 599 c.Assert(result.Results[0].Error, gc.IsNil) 600 } 601 602 func (s *ApplicationSuite) TestDeployCAASModelWrongOperatorStorageType(c *gc.C) { 603 application.SetModelType(s.api, state.ModelTypeCAAS) 604 s.storagePoolManager.storageType = provider.RootfsProviderType 605 args := params.ApplicationsDeploy{ 606 Applications: []params.ApplicationDeploy{{ 607 ApplicationName: "foo", 608 CharmURL: "local:foo-0", 609 NumUnits: 1, 610 }}, 611 } 612 result, err := s.api.Deploy(args) 613 c.Assert(err, jc.ErrorIsNil) 614 c.Assert(result.Results, gc.HasLen, 1) 615 msg := result.OneError().Error() 616 c.Assert(strings.Replace(msg, "\n", "", -1), gc.Matches, `the "operator-storage" storage pool requires a provider type of "kubernetes", not "rootfs"`) 617 } 618 619 func (s *ApplicationSuite) TestDeployCAASModelNoStoragePool(c *gc.C) { 620 s.caasBroker.SetErrors(errors.NotFoundf("storage class")) 621 application.SetModelType(s.api, state.ModelTypeCAAS) 622 args := params.ApplicationsDeploy{ 623 Applications: []params.ApplicationDeploy{{ 624 ApplicationName: "foo", 625 CharmURL: "local:foo-0", 626 NumUnits: 1, 627 Storage: map[string]storage.Constraints{ 628 "database": {}, 629 }, 630 }}, 631 } 632 result, err := s.api.Deploy(args) 633 c.Assert(err, jc.ErrorIsNil) 634 msg := result.OneError().Error() 635 c.Assert(strings.Replace(msg, "\n", "", -1), gc.Matches, `storage pool for "database" must be specified since there's no cluster default storage class`) 636 } 637 638 func (s *ApplicationSuite) TestDeployCAASModelDefaultStorageClass(c *gc.C) { 639 application.SetModelType(s.api, state.ModelTypeCAAS) 640 args := params.ApplicationsDeploy{ 641 Applications: []params.ApplicationDeploy{{ 642 ApplicationName: "foo", 643 CharmURL: "local:foo-0", 644 NumUnits: 1, 645 Storage: map[string]storage.Constraints{ 646 "database": {}, 647 }, 648 }}, 649 } 650 result, err := s.api.Deploy(args) 651 c.Assert(err, jc.ErrorIsNil) 652 c.Assert(result.Results[0].Error, gc.IsNil) 653 } 654 655 func (s *ApplicationSuite) TestDeployCAASModelWrongStorageType(c *gc.C) { 656 application.SetModelType(s.api, state.ModelTypeCAAS) 657 args := params.ApplicationsDeploy{ 658 Applications: []params.ApplicationDeploy{{ 659 ApplicationName: "foo", 660 CharmURL: "local:foo-0", 661 NumUnits: 1, 662 Storage: map[string]storage.Constraints{ 663 "database": {Pool: "db"}, 664 }, 665 }}, 666 } 667 result, err := s.api.Deploy(args) 668 c.Assert(err, jc.ErrorIsNil) 669 c.Assert(result.OneError(), gc.ErrorMatches, `invalid storage provider type "rootfs" for "database"`) 670 } 671 672 func (s *ApplicationSuite) TestAddUnits(c *gc.C) { 673 results, err := s.api.AddUnits(params.AddApplicationUnits{ 674 ApplicationName: "postgresql", 675 NumUnits: 1, 676 }) 677 c.Assert(err, jc.ErrorIsNil) 678 679 c.Assert(results, jc.DeepEquals, params.AddApplicationUnitsResults{ 680 Units: []string{"postgresql/99"}, 681 }) 682 app := s.backend.applications["postgresql"] 683 app.CheckCall(c, 0, "AddUnit", state.AddUnitParams{}) 684 app.addedUnit.CheckCall(c, 0, "AssignWithPolicy", state.AssignCleanEmpty) 685 } 686 687 func (s *ApplicationSuite) TestAddUnitsCAASModel(c *gc.C) { 688 application.SetModelType(s.api, state.ModelTypeCAAS) 689 _, err := s.api.AddUnits(params.AddApplicationUnits{ 690 ApplicationName: "postgresql", 691 NumUnits: 1, 692 }) 693 c.Assert(err, gc.ErrorMatches, "adding units on a non-container model not supported") 694 app := s.backend.applications["postgresql"] 695 app.CheckNoCalls(c) 696 } 697 698 func (s *ApplicationSuite) TestDestroyUnitsCAASModel(c *gc.C) { 699 application.SetModelType(s.api, state.ModelTypeCAAS) 700 _, err := s.api.DestroyUnit(params.DestroyUnitsParams{ 701 Units: []params.DestroyUnitParams{ 702 {UnitTag: "unit-postgresql-0"}, 703 { 704 UnitTag: "unit-postgresql-1", 705 DestroyStorage: true, 706 }, 707 }, 708 }) 709 c.Assert(err, gc.ErrorMatches, "removing units on a non-container model not supported") 710 app := s.backend.applications["postgresql"] 711 app.CheckNoCalls(c) 712 } 713 714 func (s *ApplicationSuite) TestScaleApplicationsCAASModel(c *gc.C) { 715 application.SetModelType(s.api, state.ModelTypeCAAS) 716 results, err := s.api.ScaleApplications(params.ScaleApplicationsParams{ 717 Applications: []params.ScaleApplicationParams{{ 718 ApplicationTag: "application-postgresql", 719 Scale: 5, 720 }}}) 721 c.Assert(err, jc.ErrorIsNil) 722 723 c.Assert(results, jc.DeepEquals, params.ScaleApplicationResults{ 724 Results: []params.ScaleApplicationResult{{ 725 Info: ¶ms.ScaleApplicationInfo{Scale: 5}, 726 }}, 727 }) 728 app := s.backend.applications["postgresql"] 729 app.CheckCall(c, 0, "Scale", 5) 730 } 731 732 func (s *ApplicationSuite) TestScaleApplicationsCAASModelScaleChange(c *gc.C) { 733 application.SetModelType(s.api, state.ModelTypeCAAS) 734 s.backend.applications["postgresql"].scale = 2 735 results, err := s.api.ScaleApplications(params.ScaleApplicationsParams{ 736 Applications: []params.ScaleApplicationParams{{ 737 ApplicationTag: "application-postgresql", 738 ScaleChange: 5, 739 }}}) 740 c.Assert(err, jc.ErrorIsNil) 741 742 c.Assert(results, jc.DeepEquals, params.ScaleApplicationResults{ 743 Results: []params.ScaleApplicationResult{{ 744 Info: ¶ms.ScaleApplicationInfo{Scale: 7}, 745 }}, 746 }) 747 app := s.backend.applications["postgresql"] 748 app.CheckCall(c, 0, "ChangeScale", 5) 749 } 750 751 func (s *ApplicationSuite) TestScaleApplicationsCAASModelScaleArgCheck(c *gc.C) { 752 application.SetModelType(s.api, state.ModelTypeCAAS) 753 s.backend.applications["postgresql"].scale = 2 754 755 for i, test := range []struct { 756 scale int 757 scaleChange int 758 errorStr string 759 }{{ 760 scale: 5, 761 scaleChange: 5, 762 errorStr: "requesting both scale and scale-change not valid", 763 }, { 764 scale: 0, 765 scaleChange: 0, 766 errorStr: "scale of 0 not valid", 767 }, { 768 scale: -1, 769 scaleChange: 0, 770 errorStr: "scale < 0 not valid", 771 }} { 772 c.Logf("test #%d", i) 773 results, err := s.api.ScaleApplications(params.ScaleApplicationsParams{ 774 Applications: []params.ScaleApplicationParams{{ 775 ApplicationTag: "application-postgresql", 776 Scale: test.scale, 777 ScaleChange: test.scaleChange, 778 }}}) 779 c.Assert(err, jc.ErrorIsNil) 780 c.Assert(results.Results, gc.HasLen, 1) 781 c.Assert(results.Results[0].Error, gc.ErrorMatches, test.errorStr) 782 } 783 } 784 785 func (s *ApplicationSuite) TestScaleApplicationsIAASModel(c *gc.C) { 786 _, err := s.api.ScaleApplications(params.ScaleApplicationsParams{ 787 Applications: []params.ScaleApplicationParams{{ 788 ApplicationTag: "application-postgresql", 789 Scale: 5, 790 }}}) 791 c.Assert(err, gc.ErrorMatches, "scaling applications on a non-container model not supported") 792 app := s.backend.applications["postgresql"] 793 app.CheckNoCalls(c) 794 } 795 796 func (s *ApplicationSuite) TestAddUnitsAttachStorage(c *gc.C) { 797 _, err := s.api.AddUnits(params.AddApplicationUnits{ 798 ApplicationName: "postgresql", 799 NumUnits: 1, 800 AttachStorage: []string{"storage-pgdata-0"}, 801 }) 802 c.Assert(err, jc.ErrorIsNil) 803 804 app := s.backend.applications["postgresql"] 805 app.CheckCall(c, 0, "AddUnit", state.AddUnitParams{ 806 AttachStorage: []names.StorageTag{names.NewStorageTag("pgdata/0")}, 807 }) 808 } 809 810 func (s *ApplicationSuite) TestAddUnitsAttachStorageMultipleUnits(c *gc.C) { 811 _, err := s.api.AddUnits(params.AddApplicationUnits{ 812 ApplicationName: "foo", 813 NumUnits: 2, 814 AttachStorage: []string{"storage-foo-0"}, 815 }) 816 c.Assert(err, gc.ErrorMatches, "AttachStorage is non-empty, but NumUnits is 2") 817 } 818 819 func (s *ApplicationSuite) TestAddUnitsAttachStorageInvalidStorageTag(c *gc.C) { 820 _, err := s.api.AddUnits(params.AddApplicationUnits{ 821 ApplicationName: "foo", 822 NumUnits: 1, 823 AttachStorage: []string{"volume-0"}, 824 }) 825 c.Assert(err, gc.ErrorMatches, `"volume-0" is not a valid storage tag`) 826 } 827 828 func (s *ApplicationSuite) TestSetRelationSuspended(c *gc.C) { 829 s.backend.offerConnections["wordpress:db mysql:db"] = &mockOfferConnection{} 830 results, err := s.api.SetRelationsSuspended(params.RelationSuspendedArgs{ 831 Args: []params.RelationSuspendedArg{{ 832 RelationId: 123, 833 Suspended: true, 834 Message: "message", 835 }}, 836 }) 837 c.Assert(err, jc.ErrorIsNil) 838 c.Assert(results.OneError(), gc.IsNil) 839 c.Assert(s.relation.suspended, jc.IsTrue) 840 c.Assert(s.relation.suspendedReason, gc.Equals, "message") 841 c.Assert(s.relation.status, gc.Equals, status.Suspending) 842 c.Assert(s.relation.message, gc.Equals, "message") 843 } 844 845 func (s *ApplicationSuite) TestSetRelationSuspendedNoOp(c *gc.C) { 846 s.backend.offerConnections["wordpress:db mysql:db"] = &mockOfferConnection{} 847 s.relation.suspended = true 848 s.relation.status = status.Error 849 results, err := s.api.SetRelationsSuspended(params.RelationSuspendedArgs{ 850 Args: []params.RelationSuspendedArg{{ 851 RelationId: 123, 852 Suspended: true, 853 }}, 854 }) 855 c.Assert(err, jc.ErrorIsNil) 856 c.Assert(results.OneError(), gc.IsNil) 857 c.Assert(s.relation.suspended, jc.IsTrue) 858 c.Assert(s.relation.status, gc.Equals, status.Error) 859 } 860 861 func (s *ApplicationSuite) TestSetRelationSuspendedFalse(c *gc.C) { 862 s.backend.offerConnections["wordpress:db mysql:db"] = &mockOfferConnection{} 863 s.relation.suspended = true 864 s.relation.suspendedReason = "reason" 865 s.relation.status = status.Error 866 results, err := s.api.SetRelationsSuspended(params.RelationSuspendedArgs{ 867 Args: []params.RelationSuspendedArg{{ 868 RelationId: 123, 869 Suspended: false, 870 }}, 871 }) 872 c.Assert(err, jc.ErrorIsNil) 873 c.Assert(results.OneError(), gc.IsNil) 874 c.Assert(s.relation.suspended, jc.IsFalse) 875 c.Assert(s.relation.suspendedReason, gc.Equals, "") 876 c.Assert(s.relation.status, gc.Equals, status.Joining) 877 } 878 879 func (s *ApplicationSuite) TestSetNonOfferRelationStatus(c *gc.C) { 880 s.backend.relations[123].tag = names.NewRelationTag("mediawiki:db mysql:db") 881 results, err := s.api.SetRelationsSuspended(params.RelationSuspendedArgs{ 882 Args: []params.RelationSuspendedArg{{ 883 RelationId: 123, 884 Suspended: true, 885 }}, 886 }) 887 c.Assert(err, jc.ErrorIsNil) 888 c.Assert(results.OneError(), gc.ErrorMatches, `cannot set suspend status for "mediawiki:db mysql:db" which is not associated with an offer`) 889 } 890 891 func (s *ApplicationSuite) TestBlockSetRelationSuspended(c *gc.C) { 892 s.blockChecker.SetErrors(errors.New("blocked")) 893 _, err := s.api.SetRelationsSuspended(params.RelationSuspendedArgs{ 894 Args: []params.RelationSuspendedArg{{ 895 RelationId: 123, 896 Suspended: true, 897 }}, 898 }) 899 c.Assert(err, gc.ErrorMatches, "blocked") 900 s.blockChecker.CheckCallNames(c, "ChangeAllowed") 901 s.relation.CheckNoCalls(c) 902 } 903 904 func (s *ApplicationSuite) TestSetRelationSuspendedPermissionDenied(c *gc.C) { 905 s.setAPIUser(c, names.NewUserTag("fred")) 906 _, err := s.api.SetRelationsSuspended(params.RelationSuspendedArgs{ 907 Args: []params.RelationSuspendedArg{{ 908 RelationId: 123, 909 Suspended: true, 910 }}, 911 }) 912 c.Assert(err, gc.ErrorMatches, "permission denied") 913 s.relation.CheckNoCalls(c) 914 } 915 916 func (s *ApplicationSuite) TestConsumeIdempotent(c *gc.C) { 917 for i := 0; i < 2; i++ { 918 results, err := s.api.Consume(params.ConsumeApplicationArgs{ 919 Args: []params.ConsumeApplicationArg{{ 920 ApplicationOfferDetails: params.ApplicationOfferDetails{ 921 SourceModelTag: coretesting.ModelTag.String(), 922 OfferName: "hosted-mysql", 923 OfferUUID: "hosted-mysql-uuid", 924 ApplicationDescription: "a database", 925 Endpoints: []params.RemoteEndpoint{{Name: "database", Interface: "mysql", Role: "provider"}}, 926 OfferURL: "othermodel.hosted-mysql", 927 }, 928 }}, 929 }) 930 c.Assert(err, jc.ErrorIsNil) 931 c.Assert(results.OneError(), gc.IsNil) 932 } 933 obtained, ok := s.backend.remoteApplications["hosted-mysql"] 934 c.Assert(ok, jc.IsTrue) 935 c.Assert(obtained, jc.DeepEquals, &mockRemoteApplication{ 936 name: "hosted-mysql", 937 sourceModelTag: coretesting.ModelTag, 938 offerUUID: "hosted-mysql-uuid", 939 offerURL: "othermodel.hosted-mysql", 940 endpoints: []state.Endpoint{ 941 {ApplicationName: "hosted-mysql", Relation: charm.Relation{Name: "database", Interface: "mysql", Role: "provider"}}}, 942 }) 943 } 944 945 func (s *ApplicationSuite) TestConsumeFromExternalController(c *gc.C) { 946 mac, err := apitesting.NewMacaroon("test") 947 c.Assert(err, jc.ErrorIsNil) 948 controllerUUID := utils.MustNewUUID().String() 949 results, err := s.api.Consume(params.ConsumeApplicationArgs{ 950 Args: []params.ConsumeApplicationArg{{ 951 ApplicationOfferDetails: params.ApplicationOfferDetails{ 952 SourceModelTag: coretesting.ModelTag.String(), 953 OfferName: "hosted-mysql", 954 OfferUUID: "hosted-mysql-uuid", 955 ApplicationDescription: "a database", 956 Endpoints: []params.RemoteEndpoint{{Name: "database", Interface: "mysql", Role: "provider"}}, 957 OfferURL: "othermodel.hosted-mysql", 958 }, 959 Macaroon: mac, 960 ControllerInfo: ¶ms.ExternalControllerInfo{ 961 ControllerTag: names.NewControllerTag(controllerUUID).String(), 962 Alias: "controller-alias", 963 CACert: coretesting.CACert, 964 Addrs: []string{"192.168.1.1:1234"}, 965 }, 966 }}, 967 }) 968 c.Assert(err, jc.ErrorIsNil) 969 c.Assert(results.OneError(), gc.IsNil) 970 obtained, ok := s.backend.remoteApplications["hosted-mysql"] 971 c.Assert(ok, jc.IsTrue) 972 c.Assert(obtained, jc.DeepEquals, &mockRemoteApplication{ 973 name: "hosted-mysql", 974 sourceModelTag: coretesting.ModelTag, 975 offerUUID: "hosted-mysql-uuid", 976 offerURL: "othermodel.hosted-mysql", 977 endpoints: []state.Endpoint{ 978 {ApplicationName: "hosted-mysql", Relation: charm.Relation{Name: "database", Interface: "mysql", Role: "provider"}}}, 979 mac: mac, 980 }) 981 c.Assert(s.backend.controllers[coretesting.ModelTag.Id()], jc.DeepEquals, crossmodel.ControllerInfo{ 982 ControllerTag: names.NewControllerTag(controllerUUID), 983 Alias: "controller-alias", 984 CACert: coretesting.CACert, 985 Addrs: []string{"192.168.1.1:1234"}, 986 }) 987 } 988 989 func (s *ApplicationSuite) TestConsumeFromSameController(c *gc.C) { 990 mac, err := apitesting.NewMacaroon("test") 991 c.Assert(err, jc.ErrorIsNil) 992 results, err := s.api.Consume(params.ConsumeApplicationArgs{ 993 Args: []params.ConsumeApplicationArg{{ 994 ApplicationOfferDetails: params.ApplicationOfferDetails{ 995 SourceModelTag: coretesting.ModelTag.String(), 996 OfferName: "hosted-mysql", 997 OfferUUID: "hosted-mysql-uuid", 998 ApplicationDescription: "a database", 999 Endpoints: []params.RemoteEndpoint{{Name: "database", Interface: "mysql", Role: "provider"}}, 1000 OfferURL: "othermodel.hosted-mysql", 1001 }, 1002 Macaroon: mac, 1003 ControllerInfo: ¶ms.ExternalControllerInfo{ 1004 ControllerTag: coretesting.ControllerTag.String(), 1005 Alias: "controller-alias", 1006 CACert: coretesting.CACert, 1007 Addrs: []string{"192.168.1.1:1234"}, 1008 }, 1009 }}, 1010 }) 1011 c.Assert(err, jc.ErrorIsNil) 1012 c.Assert(results.OneError(), gc.IsNil) 1013 _, ok := s.backend.remoteApplications["hosted-mysql"] 1014 c.Assert(ok, jc.IsTrue) 1015 c.Assert(s.backend.controllers, gc.HasLen, 0) 1016 } 1017 1018 func (s *ApplicationSuite) TestConsumeIncludesSpaceInfo(c *gc.C) { 1019 s.env.(*mockEnviron).spaceInfo = &environs.ProviderSpaceInfo{ 1020 CloudType: "grandaddy", 1021 ProviderAttributes: map[string]interface{}{ 1022 "thunderjaws": 1, 1023 }, 1024 SpaceInfo: network.SpaceInfo{ 1025 Name: "yourspace", 1026 ProviderId: "juju-space-myspace", 1027 Subnets: []network.SubnetInfo{{ 1028 CIDR: "5.6.7.0/24", 1029 ProviderId: "juju-subnet-1", 1030 AvailabilityZones: []string{"az1"}, 1031 }}, 1032 }, 1033 } 1034 1035 results, err := s.api.Consume(params.ConsumeApplicationArgs{ 1036 Args: []params.ConsumeApplicationArg{{ 1037 ApplicationAlias: "beirut", 1038 ApplicationOfferDetails: params.ApplicationOfferDetails{ 1039 SourceModelTag: coretesting.ModelTag.String(), 1040 OfferName: "hosted-mysql", 1041 OfferUUID: "hosted-mysql-uuid", 1042 ApplicationDescription: "a database", 1043 Endpoints: []params.RemoteEndpoint{{Name: "server", Interface: "mysql", Role: "provider"}}, 1044 OfferURL: "othermodel.hosted-mysql", 1045 Bindings: map[string]string{"server": "myspace"}, 1046 Spaces: []params.RemoteSpace{ 1047 { 1048 CloudType: "grandaddy", 1049 Name: "myspace", 1050 ProviderId: "juju-space-myspace", 1051 ProviderAttributes: map[string]interface{}{ 1052 "thunderjaws": 1, 1053 }, 1054 Subnets: []params.Subnet{{ 1055 CIDR: "5.6.7.0/24", 1056 ProviderId: "juju-subnet-1", 1057 Zones: []string{"az1"}, 1058 }}, 1059 }, 1060 }, 1061 }, 1062 }}, 1063 }) 1064 c.Assert(err, jc.ErrorIsNil) 1065 c.Assert(results.OneError(), gc.IsNil) 1066 1067 obtained, ok := s.backend.remoteApplications["beirut"] 1068 c.Assert(ok, jc.IsTrue) 1069 endpoints, err := obtained.Endpoints() 1070 c.Assert(err, jc.ErrorIsNil) 1071 epNames := make([]string, len(endpoints)) 1072 for i, ep := range endpoints { 1073 epNames[i] = ep.Name 1074 } 1075 c.Assert(epNames, jc.SameContents, []string{"server"}) 1076 c.Assert(obtained.Bindings(), jc.DeepEquals, map[string]string{"server": "myspace"}) 1077 c.Assert(obtained.Spaces(), jc.DeepEquals, []state.RemoteSpace{{ 1078 CloudType: "grandaddy", 1079 Name: "myspace", 1080 ProviderId: "juju-space-myspace", 1081 ProviderAttributes: map[string]interface{}{ 1082 "thunderjaws": 1, 1083 }, 1084 Subnets: []state.RemoteSubnet{{ 1085 CIDR: "5.6.7.0/24", 1086 ProviderId: "juju-subnet-1", 1087 AvailabilityZones: []string{"az1"}, 1088 }}, 1089 }}) 1090 } 1091 1092 func (s *ApplicationSuite) TestConsumeRemoteAppExistsDifferentSourceModel(c *gc.C) { 1093 arg := params.ConsumeApplicationArg{ 1094 ApplicationOfferDetails: params.ApplicationOfferDetails{ 1095 SourceModelTag: coretesting.ModelTag.String(), 1096 OfferName: "hosted-mysql", 1097 OfferUUID: "hosted-mysql-uuid", 1098 ApplicationDescription: "a database", 1099 Endpoints: []params.RemoteEndpoint{{Name: "database", Interface: "mysql", Role: "provider"}}, 1100 OfferURL: "othermodel.hosted-mysql", 1101 }, 1102 } 1103 results, err := s.api.Consume(params.ConsumeApplicationArgs{ 1104 Args: []params.ConsumeApplicationArg{arg}, 1105 }) 1106 c.Assert(err, jc.ErrorIsNil) 1107 c.Assert(results.Results, gc.HasLen, 1) 1108 c.Assert(results.Results[0].Error, gc.IsNil) 1109 1110 arg.SourceModelTag = names.NewModelTag(utils.MustNewUUID().String()).String() 1111 results, err = s.api.Consume(params.ConsumeApplicationArgs{ 1112 Args: []params.ConsumeApplicationArg{arg}, 1113 }) 1114 c.Assert(err, jc.ErrorIsNil) 1115 c.Assert(results.OneError(), gc.ErrorMatches, `remote application called "hosted-mysql" from a different model already exists`) 1116 } 1117 1118 func (s *ApplicationSuite) assertConsumeWithNoSpacesInfoAvailable(c *gc.C) { 1119 results, err := s.api.Consume(params.ConsumeApplicationArgs{ 1120 Args: []params.ConsumeApplicationArg{{ 1121 ApplicationOfferDetails: params.ApplicationOfferDetails{ 1122 SourceModelTag: coretesting.ModelTag.String(), 1123 OfferName: "hosted-mysql", 1124 OfferUUID: "hosted-mysql-uuid", 1125 ApplicationDescription: "a database", 1126 Endpoints: []params.RemoteEndpoint{{Name: "database", Interface: "mysql", Role: "provider"}}, 1127 OfferURL: "othermodel.hosted-mysql", 1128 }, 1129 }}, 1130 }) 1131 c.Assert(err, jc.ErrorIsNil) 1132 c.Assert(results.OneError(), gc.IsNil) 1133 1134 // Successfully added, but with no bindings or spaces since the 1135 // environ doesn't support networking. 1136 obtained, ok := s.backend.remoteApplications["hosted-mysql"] 1137 c.Assert(ok, jc.IsTrue) 1138 c.Assert(err, jc.ErrorIsNil) 1139 c.Assert(obtained.Bindings(), gc.IsNil) 1140 c.Assert(obtained.Spaces(), gc.IsNil) 1141 } 1142 1143 func (s *ApplicationSuite) TestConsumeWithNonNetworkingEnviron(c *gc.C) { 1144 s.env = &mockNoNetworkEnviron{} 1145 s.assertConsumeWithNoSpacesInfoAvailable(c) 1146 } 1147 1148 func (s *ApplicationSuite) TestConsumeProviderSpaceInfoNotSupported(c *gc.C) { 1149 s.env.(*mockEnviron).stub.SetErrors(errors.NotSupportedf("provider space info")) 1150 s.assertConsumeWithNoSpacesInfoAvailable(c) 1151 } 1152 1153 func (s *ApplicationSuite) TestApplicationUpdateSeries(c *gc.C) { 1154 args := params.UpdateSeriesArgs{ 1155 Args: []params.UpdateSeriesArg{{ 1156 Entity: params.Entity{Tag: names.NewApplicationTag("postgresql").String()}, 1157 Series: "trusty", 1158 }, { 1159 Entity: params.Entity{Tag: names.NewApplicationTag("postgresql").String()}, 1160 Series: "quantal", 1161 }, { 1162 Entity: params.Entity{Tag: names.NewApplicationTag("name").String()}, 1163 Series: "trusty", 1164 }, { 1165 Entity: params.Entity{Tag: names.NewUnitTag("mysql/0").String()}, 1166 Series: "trusty", 1167 }}, 1168 } 1169 results, err := s.api.UpdateApplicationSeries(args) 1170 c.Assert(err, jc.ErrorIsNil) 1171 c.Assert(results, jc.DeepEquals, params.ErrorResults{ 1172 Results: []params.ErrorResult{ 1173 {}, {}, 1174 {Error: ¶ms.Error{Message: "application \"name\" not found", Code: "not found"}}, 1175 {Error: ¶ms.Error{Message: "\"unit-mysql-0\" is not a valid application tag", Code: ""}}, 1176 }}) 1177 s.backend.CheckCall(c, 0, "Application", "postgresql") 1178 s.backend.CheckCall(c, 1, "Application", "postgresql") 1179 1180 app := s.backend.applications["postgresql"] 1181 app.CheckCall(c, 0, "IsPrincipal") 1182 app.CheckCall(c, 1, "Series") 1183 app.CheckCall(c, 2, "UpdateApplicationSeries", "trusty", false) 1184 app.CheckCall(c, 3, "IsPrincipal") 1185 app.CheckCall(c, 4, "Series") 1186 // ensure that app.UpdateApplicationSeries wasn't called a 2nd time. 1187 c.Assert(len(app.Calls()), gc.Equals, 5) 1188 } 1189 1190 func (s *ApplicationSuite) TestApplicationUpdateSeriesNoParams(c *gc.C) { 1191 results, err := s.api.UpdateApplicationSeries( 1192 params.UpdateSeriesArgs{ 1193 Args: []params.UpdateSeriesArg{}, 1194 }, 1195 ) 1196 c.Assert(err, jc.ErrorIsNil) 1197 c.Assert(results, jc.DeepEquals, params.ErrorResults{Results: []params.ErrorResult{}}) 1198 1199 s.backend.CheckNoCalls(c) 1200 } 1201 1202 func (s *ApplicationSuite) TestApplicationUpdateSeriesNoSeries(c *gc.C) { 1203 results, err := s.api.UpdateApplicationSeries( 1204 params.UpdateSeriesArgs{ 1205 Args: []params.UpdateSeriesArg{{Entity: params.Entity{Tag: names.NewApplicationTag("postgresql").String()}}}, 1206 }, 1207 ) 1208 c.Assert(err, jc.ErrorIsNil) 1209 c.Assert(len(results.Results), gc.Equals, 1) 1210 c.Assert(results.Results[0], jc.DeepEquals, params.ErrorResult{ 1211 Error: ¶ms.Error{ 1212 Code: params.CodeBadRequest, 1213 Message: `series missing from args`, 1214 }, 1215 }) 1216 1217 s.backend.CheckNoCalls(c) 1218 } 1219 1220 func (s *ApplicationSuite) TestApplicationUpdateSeriesOfSubordinate(c *gc.C) { 1221 args := params.UpdateSeriesArgs{ 1222 Args: []params.UpdateSeriesArg{{ 1223 Entity: params.Entity{Tag: names.NewApplicationTag("postgresql-subordinate").String()}, 1224 Series: "xenial", 1225 }}, 1226 } 1227 results, err := s.api.UpdateApplicationSeries(args) 1228 c.Assert(err, jc.ErrorIsNil) 1229 c.Assert(len(results.Results), gc.Equals, 1) 1230 c.Assert(results.Results[0], jc.DeepEquals, params.ErrorResult{ 1231 Error: ¶ms.Error{ 1232 Code: params.CodeNotSupported, 1233 Message: `"postgresql-subordinate" is a subordinate application, update-series not supported`, 1234 }, 1235 }) 1236 1237 s.backend.CheckCall(c, 0, "Application", "postgresql-subordinate") 1238 1239 app := s.backend.applications["postgresql-subordinate"] 1240 app.CheckCall(c, 0, "IsPrincipal") 1241 } 1242 1243 func (s *ApplicationSuite) TestApplicationUpdateSeriesIncompatibleSeries(c *gc.C) { 1244 app := s.backend.applications["postgresql"] 1245 app.SetErrors(nil, nil, &state.ErrIncompatibleSeries{[]string{"yakkety", "zesty"}, "xenial", "testCharm"}) 1246 results, err := s.api.UpdateApplicationSeries( 1247 params.UpdateSeriesArgs{ 1248 Args: []params.UpdateSeriesArg{{ 1249 Entity: params.Entity{Tag: names.NewApplicationTag("postgresql").String()}, 1250 Series: "xenial", 1251 }}, 1252 }) 1253 c.Assert(err, jc.ErrorIsNil) 1254 c.Assert(len(results.Results), gc.Equals, 1) 1255 c.Assert(results.Results[0], jc.DeepEquals, params.ErrorResult{ 1256 Error: ¶ms.Error{ 1257 Code: params.CodeIncompatibleSeries, 1258 Message: "series \"xenial\" not supported by charm \"testCharm\", supported series are: yakkety, zesty", 1259 }, 1260 }) 1261 } 1262 1263 func (s *ApplicationSuite) TestApplicationUpdateSeriesPermissionDenied(c *gc.C) { 1264 user := names.NewUserTag("fred") 1265 s.setAPIUser(c, user) 1266 _, err := s.api.UpdateApplicationSeries( 1267 params.UpdateSeriesArgs{ 1268 Args: []params.UpdateSeriesArg{{ 1269 Entity: params.Entity{Tag: names.NewApplicationTag("postgresql").String()}, 1270 Series: "trusty", 1271 }}, 1272 }, 1273 ) 1274 c.Assert(err, gc.ErrorMatches, "permission denied") 1275 } 1276 1277 func (s *ApplicationSuite) TestRemoteRelationBadCIDR(c *gc.C) { 1278 endpoints := []string{"wordpress", "hosted-mysql:nope"} 1279 _, err := s.api.AddRelation(params.AddRelation{Endpoints: endpoints, ViaCIDRs: []string{"bad.cidr"}}) 1280 c.Assert(err, gc.ErrorMatches, `invalid CIDR address: bad.cidr`) 1281 } 1282 1283 func (s *ApplicationSuite) TestRemoteRelationDisAllowedCIDR(c *gc.C) { 1284 endpoints := []string{"wordpress", "hosted-mysql:nope"} 1285 _, err := s.api.AddRelation(params.AddRelation{Endpoints: endpoints, ViaCIDRs: []string{"0.0.0.0/0"}}) 1286 c.Assert(err, gc.ErrorMatches, `CIDR "0.0.0.0/0" not allowed`) 1287 } 1288 1289 func (s *ApplicationSuite) TestSetApplicationConfig(c *gc.C) { 1290 application.SetModelType(s.api, state.ModelTypeCAAS) 1291 result, err := s.api.SetApplicationsConfig(params.ApplicationConfigSetArgs{ 1292 Args: []params.ApplicationConfigSet{{ 1293 ApplicationName: "postgresql", 1294 Config: map[string]string{ 1295 "juju-external-hostname": "value", 1296 "stringOption": "stringVal"}, 1297 }}}) 1298 c.Assert(err, jc.ErrorIsNil) 1299 c.Assert(result.OneError(), jc.ErrorIsNil) 1300 s.backend.CheckCallNames(c, "Application") 1301 app := s.backend.applications["postgresql"] 1302 app.CheckCallNames(c, "UpdateApplicationConfig", "Charm", "UpdateCharmConfig") 1303 1304 schema, err := caas.ConfigSchema(k8s.ConfigSchema()) 1305 c.Assert(err, jc.ErrorIsNil) 1306 defaults := caas.ConfigDefaults(k8s.ConfigDefaults()) 1307 schema, defaults, err = application.AddTrustSchemaAndDefaults(schema, defaults) 1308 c.Assert(err, jc.ErrorIsNil) 1309 1310 app.CheckCall(c, 0, "UpdateApplicationConfig", coreapplication.ConfigAttributes{ 1311 "juju-external-hostname": "value", 1312 }, []string(nil), schema, defaults) 1313 app.CheckCall(c, 2, "UpdateCharmConfig", charm.Settings{"stringOption": "stringVal"}) 1314 } 1315 1316 func (s *ApplicationSuite) TestBlockSetApplicationConfig(c *gc.C) { 1317 s.blockChecker.SetErrors(errors.New("blocked")) 1318 _, err := s.api.SetApplicationsConfig(params.ApplicationConfigSetArgs{}) 1319 c.Assert(err, gc.ErrorMatches, "blocked") 1320 s.blockChecker.CheckCallNames(c, "ChangeAllowed") 1321 s.relation.CheckNoCalls(c) 1322 } 1323 1324 func (s *ApplicationSuite) TestSetApplicationConfigPermissionDenied(c *gc.C) { 1325 s.setAPIUser(c, names.NewUserTag("fred")) 1326 _, err := s.api.SetApplicationsConfig(params.ApplicationConfigSetArgs{ 1327 Args: []params.ApplicationConfigSet{{ 1328 ApplicationName: "postgresql", 1329 }}}) 1330 c.Assert(err, gc.ErrorMatches, "permission denied") 1331 s.application.CheckNoCalls(c) 1332 } 1333 1334 func (s *ApplicationSuite) TestUnsetApplicationConfig(c *gc.C) { 1335 application.SetModelType(s.api, state.ModelTypeCAAS) 1336 result, err := s.api.UnsetApplicationsConfig(params.ApplicationConfigUnsetArgs{ 1337 Args: []params.ApplicationUnset{{ 1338 ApplicationName: "postgresql", 1339 Options: []string{"juju-external-hostname", "stringVal"}, 1340 }}}) 1341 c.Assert(err, jc.ErrorIsNil) 1342 c.Assert(result.OneError(), jc.ErrorIsNil) 1343 c.Assert(err, jc.ErrorIsNil) 1344 s.backend.CheckCallNames(c, "Application") 1345 app := s.backend.applications["postgresql"] 1346 app.CheckCallNames(c, "UpdateApplicationConfig", "UpdateCharmConfig") 1347 1348 schema, err := caas.ConfigSchema(k8s.ConfigSchema()) 1349 c.Assert(err, jc.ErrorIsNil) 1350 defaults := caas.ConfigDefaults(k8s.ConfigDefaults()) 1351 schema, defaults, err = application.AddTrustSchemaAndDefaults(schema, defaults) 1352 c.Assert(err, jc.ErrorIsNil) 1353 1354 app.CheckCall(c, 0, "UpdateApplicationConfig", coreapplication.ConfigAttributes(nil), 1355 []string{"juju-external-hostname"}, schema, defaults) 1356 app.CheckCall(c, 1, "UpdateCharmConfig", charm.Settings{"stringVal": nil}) 1357 } 1358 1359 func (s *ApplicationSuite) TestBlockUnsetApplicationConfig(c *gc.C) { 1360 s.blockChecker.SetErrors(errors.New("blocked")) 1361 _, err := s.api.UnsetApplicationsConfig(params.ApplicationConfigUnsetArgs{}) 1362 c.Assert(err, gc.ErrorMatches, "blocked") 1363 s.blockChecker.CheckCallNames(c, "ChangeAllowed") 1364 s.relation.CheckNoCalls(c) 1365 } 1366 1367 func (s *ApplicationSuite) TestUnsetApplicationConfigPermissionDenied(c *gc.C) { 1368 s.setAPIUser(c, names.NewUserTag("fred")) 1369 _, err := s.api.UnsetApplicationsConfig(params.ApplicationConfigUnsetArgs{ 1370 Args: []params.ApplicationUnset{{ 1371 ApplicationName: "postgresql", 1372 Options: []string{"option"}, 1373 }}}) 1374 c.Assert(err, gc.ErrorMatches, "permission denied") 1375 s.application.CheckNoCalls(c) 1376 } 1377 1378 func (s *ApplicationSuite) TestResolveUnitErrors(c *gc.C) { 1379 entities := []params.Entity{{Tag: "unit-postgresql-0"}, {Tag: "unit-postgresql-1"}} 1380 p := params.UnitsResolved{ 1381 Retry: true, 1382 Tags: params.Entities{ 1383 Entities: entities, 1384 }, 1385 } 1386 result, err := s.api.ResolveUnitErrors(p) 1387 c.Assert(err, jc.ErrorIsNil) 1388 c.Assert(result, gc.DeepEquals, params.ErrorResults{Results: []params.ErrorResult{{}, {}}}) 1389 1390 for i := 0; i < 2; i++ { 1391 unit := s.backend.applications["postgresql"].units[i] 1392 unit.CheckCallNames(c, "Resolve") 1393 unit.CheckCall(c, 0, "Resolve", true) 1394 } 1395 } 1396 1397 func (s *ApplicationSuite) TestResolveUnitErrorsAll(c *gc.C) { 1398 p := params.UnitsResolved{ 1399 All: true, 1400 Retry: true, 1401 } 1402 _, err := s.api.ResolveUnitErrors(p) 1403 c.Assert(err, jc.ErrorIsNil) 1404 1405 unit := s.backend.applications["postgresql"].units[0] 1406 unit.CheckCallNames(c, "Resolve") 1407 unit.CheckCall(c, 0, "Resolve", true) 1408 } 1409 1410 func (s *ApplicationSuite) TestBlockResolveUnitErrors(c *gc.C) { 1411 s.blockChecker.SetErrors(errors.New("blocked")) 1412 _, err := s.api.ResolveUnitErrors(params.UnitsResolved{}) 1413 c.Assert(err, gc.ErrorMatches, "blocked") 1414 s.blockChecker.CheckCallNames(c, "ChangeAllowed") 1415 s.relation.CheckNoCalls(c) 1416 } 1417 1418 func (s *ApplicationSuite) TestResolveUnitErrorsPermissionDenied(c *gc.C) { 1419 s.setAPIUser(c, names.NewUserTag("fred")) 1420 1421 entities := []params.Entity{{Tag: "unit-postgresql-0"}} 1422 p := params.UnitsResolved{ 1423 Retry: true, 1424 Tags: params.Entities{ 1425 Entities: entities, 1426 }, 1427 } 1428 _, err := s.api.ResolveUnitErrors(p) 1429 c.Assert(err, gc.ErrorMatches, "permission denied") 1430 s.application.CheckNoCalls(c) 1431 } 1432 1433 func (s *ApplicationSuite) TestCAASExposeWithoutHostname(c *gc.C) { 1434 application.SetModelType(s.api, state.ModelTypeCAAS) 1435 err := s.api.Expose(params.ApplicationExpose{ 1436 ApplicationName: "postgresql", 1437 }) 1438 c.Assert(err, gc.ErrorMatches, 1439 `cannot expose a CAAS application without a "juju-external-hostname" value set, run\n`+ 1440 `juju config postgresql juju-external-hostname=<value>`) 1441 } 1442 1443 func (s *ApplicationSuite) TestCAASExposeWithHostname(c *gc.C) { 1444 application.SetModelType(s.api, state.ModelTypeCAAS) 1445 app := s.backend.applications["postgresql"] 1446 app.config = coreapplication.ConfigAttributes{"juju-external-hostname": "exthost"} 1447 err := s.api.Expose(params.ApplicationExpose{ 1448 ApplicationName: "postgresql", 1449 }) 1450 c.Assert(err, jc.ErrorIsNil) 1451 app.CheckCallNames(c, "ApplicationConfig", "SetExposed") 1452 } 1453 1454 func (s *ApplicationSuite) TestApplicationsInfoOne(c *gc.C) { 1455 entities := []params.Entity{{Tag: "application-postgresql"}} 1456 result, err := s.api.ApplicationsInfo(params.Entities{entities}) 1457 c.Assert(err, jc.ErrorIsNil) 1458 c.Assert(result.Results, gc.HasLen, len(entities)) 1459 c.Assert(*result.Results[0].Result, gc.DeepEquals, params.ApplicationInfo{ 1460 Tag: "application-postgresql", 1461 Charm: "charm-postgresql", 1462 Series: "quantal", 1463 Channel: "development", 1464 Constraints: constraints.MustParse("arch=amd64 mem=4G cores=1 root-disk=8G"), 1465 Principal: true, 1466 EndpointBindings: map[string]string{ 1467 "juju-info": "myspace", 1468 }, 1469 }) 1470 app := s.backend.applications["postgresql"] 1471 app.CheckCallNames(c, "CharmConfig", "Charm", "ApplicationConfig", "IsPrincipal", "Constraints", "Series", "Channel", "EndpointBindings", "IsPrincipal", "IsExposed", "IsRemote") 1472 } 1473 1474 func (s *ApplicationSuite) TestApplicationsInfoDetailsErr(c *gc.C) { 1475 entities := []params.Entity{{Tag: "application-postgresql"}} 1476 app := s.backend.applications["postgresql"] 1477 app.SetErrors( 1478 errors.Errorf("boom"), // a.CharmConfig() call 1479 ) 1480 1481 result, err := s.api.ApplicationsInfo(params.Entities{entities}) 1482 c.Assert(err, jc.ErrorIsNil) 1483 c.Assert(result.Results, gc.HasLen, len(entities)) 1484 app.CheckCallNames(c, "CharmConfig") 1485 c.Assert(*result.Results[0].Error, gc.ErrorMatches, "boom") 1486 } 1487 1488 func (s *ApplicationSuite) TestApplicationsInfoBindingsErr(c *gc.C) { 1489 entities := []params.Entity{{Tag: "application-postgresql"}} 1490 app := s.backend.applications["postgresql"] 1491 app.SetErrors( 1492 nil, // a.CharmConfig() call 1493 errors.Errorf("boom"), // a.EndpointBindings() call 1494 ) 1495 1496 result, err := s.api.ApplicationsInfo(params.Entities{entities}) 1497 c.Assert(err, jc.ErrorIsNil) 1498 c.Assert(result.Results, gc.HasLen, len(entities)) 1499 app.CheckCallNames(c, "CharmConfig", "Charm", "ApplicationConfig") 1500 c.Assert(*result.Results[0].Error, gc.ErrorMatches, "boom") 1501 } 1502 1503 func (s *ApplicationSuite) TestApplicationsInfoMany(c *gc.C) { 1504 entities := []params.Entity{{Tag: "application-postgresql"}, {Tag: "application-wordpress"}, {Tag: "unit-postgresql-0"}} 1505 result, err := s.api.ApplicationsInfo(params.Entities{entities}) 1506 c.Assert(err, jc.ErrorIsNil) 1507 c.Assert(result.Results, gc.HasLen, len(entities)) 1508 c.Assert(*result.Results[0].Result, gc.DeepEquals, params.ApplicationInfo{ 1509 Tag: "application-postgresql", 1510 Charm: "charm-postgresql", 1511 Series: "quantal", 1512 Channel: "development", 1513 Constraints: constraints.MustParse("arch=amd64 mem=4G cores=1 root-disk=8G"), 1514 Principal: true, 1515 EndpointBindings: map[string]string{ 1516 "juju-info": "myspace", 1517 }, 1518 }) 1519 c.Assert(result.Results[1].Error, gc.ErrorMatches, `application "wordpress" not found`) 1520 c.Assert(result.Results[2].Error, gc.ErrorMatches, `"unit-postgresql-0" is not a valid application tag`) 1521 app := s.backend.applications["postgresql"] 1522 app.CheckCallNames(c, "CharmConfig", "Charm", "ApplicationConfig", "IsPrincipal", "Constraints", "Series", "Channel", "EndpointBindings", "IsPrincipal", "IsExposed", "IsRemote") 1523 }