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: &params.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: &params.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: &params.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: &params.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: &params.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: &params.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: &params.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: &params.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: &params.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: &params.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: &params.Error{Message: "application \"name\" not found", Code: "not found"}},
  1175  			{Error: &params.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: &params.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: &params.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: &params.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  }