github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/apiserver/facades/client/application/deploy_test.go (about)

     1  // Copyright 2012, 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package application_test
     5  
     6  import (
     7  	"fmt"
     8  
     9  	"github.com/juju/charm/v12"
    10  	"github.com/juju/collections/set"
    11  	"github.com/juju/errors"
    12  	jc "github.com/juju/testing/checkers"
    13  	gc "gopkg.in/check.v1"
    14  	"gopkg.in/juju/environschema.v1"
    15  
    16  	"github.com/juju/juju/apiserver/facades/client/application"
    17  	"github.com/juju/juju/controller"
    18  	corecharm "github.com/juju/juju/core/charm"
    19  	coreconfig "github.com/juju/juju/core/config"
    20  	"github.com/juju/juju/core/constraints"
    21  	"github.com/juju/juju/core/instance"
    22  	"github.com/juju/juju/core/model"
    23  	"github.com/juju/juju/core/network"
    24  	"github.com/juju/juju/juju/testing"
    25  	"github.com/juju/juju/state"
    26  	"github.com/juju/juju/testcharms"
    27  	coretesting "github.com/juju/juju/testing"
    28  )
    29  
    30  // DeployLocalSuite uses a fresh copy of the same local dummy charm for each
    31  // test, because DeployApplication demands that a charm already exists in state,
    32  // and that is the simplest way to get one in there.
    33  type DeployLocalSuite struct {
    34  	testing.JujuConnSuite
    35  	charm *state.Charm
    36  }
    37  
    38  var _ = gc.Suite(&DeployLocalSuite{})
    39  
    40  func (s *DeployLocalSuite) SetUpSuite(c *gc.C) {
    41  	s.JujuConnSuite.SetUpSuite(c)
    42  }
    43  
    44  func (s *DeployLocalSuite) SetUpTest(c *gc.C) {
    45  	s.JujuConnSuite.SetUpTest(c)
    46  	curl := charm.MustParseURL("local:quantal/dummy")
    47  	ch := testcharms.RepoForSeries("quantal").CharmDir("dummy")
    48  	charm, err := testing.PutCharm(s.State, curl, ch)
    49  	c.Assert(err, jc.ErrorIsNil)
    50  	s.charm = charm
    51  }
    52  
    53  func (s *DeployLocalSuite) TestDeployControllerNotAllowed(c *gc.C) {
    54  	ch := s.AddTestingCharm(c, "juju-controller")
    55  	model, err := s.State.Model()
    56  	c.Assert(err, jc.ErrorIsNil)
    57  	_, err = application.DeployApplication(stateDeployer{s.State},
    58  		model,
    59  		application.DeployApplicationParams{
    60  			ApplicationName: "my-controller",
    61  			Charm:           ch,
    62  			CharmOrigin:     corecharm.Origin{Platform: corecharm.Platform{OS: "ubuntu", Channel: "22.04"}},
    63  		})
    64  	c.Assert(err, gc.ErrorMatches, "manual deploy of the controller charm not supported")
    65  }
    66  
    67  func (s *DeployLocalSuite) TestDeployMinimal(c *gc.C) {
    68  	model, err := s.State.Model()
    69  	c.Assert(err, jc.ErrorIsNil)
    70  	app, err := application.DeployApplication(stateDeployer{s.State},
    71  		model,
    72  		application.DeployApplicationParams{
    73  			ApplicationName: "bob",
    74  			Charm:           s.charm,
    75  			CharmOrigin:     corecharm.Origin{Platform: corecharm.Platform{OS: "ubuntu", Channel: "22.04"}},
    76  		})
    77  	c.Assert(err, jc.ErrorIsNil)
    78  	s.assertCharm(c, app, s.charm.URL())
    79  	s.assertSettings(c, app, charm.Settings{})
    80  	s.assertApplicationConfig(c, app, coreconfig.ConfigAttributes{})
    81  	s.assertConstraints(c, app, constraints.MustParse("arch=amd64"))
    82  	s.assertMachines(c, app, constraints.Value{})
    83  }
    84  
    85  func (s *DeployLocalSuite) TestDeployChannel(c *gc.C) {
    86  	var f fakeDeployer
    87  
    88  	model, err := s.State.Model()
    89  	c.Assert(err, jc.ErrorIsNil)
    90  
    91  	_, err = application.DeployApplication(&f,
    92  		model,
    93  		application.DeployApplicationParams{
    94  			ApplicationName: "bob",
    95  			Charm:           s.charm,
    96  			CharmOrigin:     corecharm.Origin{Platform: corecharm.Platform{OS: "ubuntu", Channel: "22.04"}},
    97  		})
    98  	c.Assert(err, jc.ErrorIsNil)
    99  
   100  	c.Assert(f.args.Name, gc.Equals, "bob")
   101  	c.Assert(f.args.Charm, gc.DeepEquals, s.charm)
   102  	c.Assert(f.args.CharmOrigin, jc.DeepEquals, &state.CharmOrigin{
   103  		Platform: &state.Platform{OS: "ubuntu", Channel: "22.04"}})
   104  }
   105  
   106  func (s *DeployLocalSuite) TestDeployWithImplicitBindings(c *gc.C) {
   107  	wordpressCharm := s.addWordpressCharmWithExtraBindings(c)
   108  
   109  	model, err := s.State.Model()
   110  	c.Assert(err, jc.ErrorIsNil)
   111  
   112  	app, err := application.DeployApplication(stateDeployer{s.State},
   113  		model,
   114  		application.DeployApplicationParams{
   115  			ApplicationName:  "bob",
   116  			Charm:            wordpressCharm,
   117  			EndpointBindings: nil,
   118  			CharmOrigin:      corecharm.Origin{Platform: corecharm.Platform{OS: "ubuntu", Channel: "22.04"}},
   119  		})
   120  	c.Assert(err, jc.ErrorIsNil)
   121  
   122  	s.assertBindings(c, app, map[string]string{
   123  		"": network.AlphaSpaceId,
   124  		// relation names
   125  		"url":             network.AlphaSpaceId,
   126  		"logging-dir":     network.AlphaSpaceId,
   127  		"monitoring-port": network.AlphaSpaceId,
   128  		"db":              network.AlphaSpaceId,
   129  		"cache":           network.AlphaSpaceId,
   130  		"cluster":         network.AlphaSpaceId,
   131  		// extra-bindings names
   132  		"db-client": network.AlphaSpaceId,
   133  		"admin-api": network.AlphaSpaceId,
   134  		"foo-bar":   network.AlphaSpaceId,
   135  	})
   136  }
   137  
   138  func (s *DeployLocalSuite) addWordpressCharm(c *gc.C) *state.Charm {
   139  	wordpressCharmURL := charm.MustParseURL("local:quantal/wordpress")
   140  	return s.addWordpressCharmFromURL(c, wordpressCharmURL)
   141  }
   142  
   143  func (s *DeployLocalSuite) addWordpressCharmWithExtraBindings(c *gc.C) *state.Charm {
   144  	wordpressCharmURL := charm.MustParseURL("local:quantal/wordpress-extra-bindings")
   145  	return s.addWordpressCharmFromURL(c, wordpressCharmURL)
   146  }
   147  
   148  func (s *DeployLocalSuite) addWordpressCharmFromURL(c *gc.C, charmURL *charm.URL) *state.Charm {
   149  	ch := testcharms.RepoForSeries("quantal").CharmDir(charmURL.Name)
   150  	wordpressCharm, err := testing.PutCharm(s.State, charmURL, ch)
   151  	c.Assert(err, jc.ErrorIsNil)
   152  	return wordpressCharm
   153  }
   154  
   155  func (s *DeployLocalSuite) assertBindings(c *gc.C, app application.Application, expected map[string]string) {
   156  	type withEndpointBindings interface {
   157  		EndpointBindings() (application.Bindings, error)
   158  	}
   159  	bindings, err := app.(withEndpointBindings).EndpointBindings()
   160  	c.Assert(err, jc.ErrorIsNil)
   161  	c.Assert(bindings.Map(), jc.DeepEquals, expected)
   162  }
   163  
   164  func (s *DeployLocalSuite) TestDeployWithSomeSpecifiedBindings(c *gc.C) {
   165  	wordpressCharm := s.addWordpressCharm(c)
   166  	dbSpace, err := s.State.AddSpace("db", "", nil, false)
   167  	c.Assert(err, jc.ErrorIsNil)
   168  	publicSpace, err := s.State.AddSpace("public", "", nil, false)
   169  	c.Assert(err, jc.ErrorIsNil)
   170  
   171  	model, err := s.State.Model()
   172  	c.Assert(err, jc.ErrorIsNil)
   173  
   174  	app, err := application.DeployApplication(stateDeployer{s.State},
   175  		model,
   176  		application.DeployApplicationParams{
   177  			ApplicationName: "bob",
   178  			Charm:           wordpressCharm,
   179  			EndpointBindings: map[string]string{
   180  				"":   publicSpace.Id(),
   181  				"db": dbSpace.Id(),
   182  			},
   183  			CharmOrigin: corecharm.Origin{Platform: corecharm.Platform{OS: "ubuntu", Channel: "22.04"}},
   184  		})
   185  	c.Assert(err, jc.ErrorIsNil)
   186  
   187  	s.assertBindings(c, app, map[string]string{
   188  		// default binding
   189  		"": publicSpace.Id(),
   190  		// relation names
   191  		"url":             publicSpace.Id(),
   192  		"logging-dir":     publicSpace.Id(),
   193  		"monitoring-port": publicSpace.Id(),
   194  		"db":              dbSpace.Id(),
   195  		"cache":           publicSpace.Id(),
   196  		// extra-bindings names
   197  		"db-client": publicSpace.Id(),
   198  		"admin-api": publicSpace.Id(),
   199  		"foo-bar":   publicSpace.Id(),
   200  	})
   201  }
   202  
   203  func (s *DeployLocalSuite) TestDeployWithBoundRelationNamesAndExtraBindingsNames(c *gc.C) {
   204  	wordpressCharm := s.addWordpressCharmWithExtraBindings(c)
   205  	dbSpace, err := s.State.AddSpace("db", "", nil, false)
   206  	c.Assert(err, jc.ErrorIsNil)
   207  	publicSpace, err := s.State.AddSpace("public", "", nil, false)
   208  	c.Assert(err, jc.ErrorIsNil)
   209  	internalSpace, err := s.State.AddSpace("internal", "", nil, false)
   210  	c.Assert(err, jc.ErrorIsNil)
   211  
   212  	model, err := s.State.Model()
   213  	c.Assert(err, jc.ErrorIsNil)
   214  
   215  	app, err := application.DeployApplication(stateDeployer{s.State},
   216  		model,
   217  		application.DeployApplicationParams{
   218  			ApplicationName: "bob",
   219  			Charm:           wordpressCharm,
   220  			EndpointBindings: map[string]string{
   221  				"":          publicSpace.Id(),
   222  				"db":        dbSpace.Id(),
   223  				"db-client": dbSpace.Id(),
   224  				"admin-api": internalSpace.Id(),
   225  			},
   226  			CharmOrigin: corecharm.Origin{Platform: corecharm.Platform{OS: "ubuntu", Channel: "22.04"}},
   227  		})
   228  	c.Assert(err, jc.ErrorIsNil)
   229  
   230  	s.assertBindings(c, app, map[string]string{
   231  		"":                publicSpace.Id(),
   232  		"url":             publicSpace.Id(),
   233  		"logging-dir":     publicSpace.Id(),
   234  		"monitoring-port": publicSpace.Id(),
   235  		"db":              dbSpace.Id(),
   236  		"cache":           publicSpace.Id(),
   237  		"db-client":       dbSpace.Id(),
   238  		"admin-api":       internalSpace.Id(),
   239  		"cluster":         publicSpace.Id(),
   240  		"foo-bar":         publicSpace.Id(), // like for relations, uses the application-default.
   241  	})
   242  
   243  }
   244  
   245  func (s *DeployLocalSuite) TestDeployWithInvalidSpace(c *gc.C) {
   246  	wordpressCharm := s.addWordpressCharm(c)
   247  	_, err := s.State.AddSpace("db", "", nil, false)
   248  	c.Assert(err, jc.ErrorIsNil)
   249  	publicSpace, err := s.State.AddSpace("public", "", nil, false)
   250  	c.Assert(err, jc.ErrorIsNil)
   251  
   252  	model, err := s.State.Model()
   253  	c.Assert(err, jc.ErrorIsNil)
   254  
   255  	app, err := application.DeployApplication(stateDeployer{s.State},
   256  		model,
   257  		application.DeployApplicationParams{
   258  			ApplicationName: "bob",
   259  			Charm:           wordpressCharm,
   260  			EndpointBindings: map[string]string{
   261  				"":   publicSpace.Id(),
   262  				"db": "42", //unknown space id
   263  			},
   264  			CharmOrigin: corecharm.Origin{Platform: corecharm.Platform{OS: "ubuntu", Channel: "22.04"}},
   265  		})
   266  	c.Assert(err, gc.ErrorMatches, `cannot add application "bob": space not found`)
   267  	c.Check(app, gc.IsNil)
   268  	// The application should not have been added
   269  	_, err = s.State.Application("bob")
   270  	c.Assert(err, jc.Satisfies, errors.IsNotFound)
   271  }
   272  
   273  func (s *DeployLocalSuite) TestDeployResources(c *gc.C) {
   274  	var f fakeDeployer
   275  
   276  	model, err := s.State.Model()
   277  	c.Assert(err, jc.ErrorIsNil)
   278  
   279  	_, err = application.DeployApplication(&f,
   280  		model,
   281  		application.DeployApplicationParams{
   282  			ApplicationName: "bob",
   283  			Charm:           s.charm,
   284  			EndpointBindings: map[string]string{
   285  				"": "public",
   286  			},
   287  			Resources:   map[string]string{"foo": "bar"},
   288  			CharmOrigin: corecharm.Origin{Platform: corecharm.Platform{OS: "ubuntu", Channel: "22.04"}},
   289  		})
   290  	c.Assert(err, jc.ErrorIsNil)
   291  
   292  	c.Assert(f.args.Name, gc.Equals, "bob")
   293  	c.Assert(f.args.Charm, gc.DeepEquals, s.charm)
   294  	c.Assert(f.args.Resources, gc.DeepEquals, map[string]string{"foo": "bar"})
   295  }
   296  
   297  func (s *DeployLocalSuite) TestDeploySettings(c *gc.C) {
   298  	model, err := s.State.Model()
   299  	c.Assert(err, jc.ErrorIsNil)
   300  
   301  	app, err := application.DeployApplication(stateDeployer{s.State},
   302  		model,
   303  		application.DeployApplicationParams{
   304  			ApplicationName: "bob",
   305  			Charm:           s.charm,
   306  			CharmConfig: charm.Settings{
   307  				"title":       "banana cupcakes",
   308  				"skill-level": 9901,
   309  			},
   310  			CharmOrigin: corecharm.Origin{Platform: corecharm.Platform{OS: "ubuntu", Channel: "22.04"}},
   311  		})
   312  	c.Assert(err, jc.ErrorIsNil)
   313  	s.assertSettings(c, app, charm.Settings{
   314  		"title":       "banana cupcakes",
   315  		"skill-level": int64(9901),
   316  	})
   317  }
   318  
   319  func (s *DeployLocalSuite) TestDeploySettingsError(c *gc.C) {
   320  	model, err := s.State.Model()
   321  	c.Assert(err, jc.ErrorIsNil)
   322  
   323  	_, err = application.DeployApplication(stateDeployer{s.State},
   324  		model,
   325  		application.DeployApplicationParams{
   326  			ApplicationName: "bob",
   327  			Charm:           s.charm,
   328  			CharmConfig: charm.Settings{
   329  				"skill-level": 99.01,
   330  			},
   331  			CharmOrigin: corecharm.Origin{Platform: corecharm.Platform{OS: "ubuntu", Channel: "22.04"}},
   332  		})
   333  	c.Assert(err, gc.ErrorMatches, `option "skill-level" expected int, got 99.01`)
   334  	_, err = s.State.Application("bob")
   335  	c.Assert(err, jc.Satisfies, errors.IsNotFound)
   336  }
   337  
   338  func sampleApplicationConfigSchema() environschema.Fields {
   339  	schema := environschema.Fields{
   340  		"title":       environschema.Attr{Type: environschema.Tstring},
   341  		"outlook":     environschema.Attr{Type: environschema.Tstring},
   342  		"username":    environschema.Attr{Type: environschema.Tstring},
   343  		"skill-level": environschema.Attr{Type: environschema.Tint},
   344  	}
   345  	return schema
   346  }
   347  
   348  func (s *DeployLocalSuite) TestDeployWithApplicationConfig(c *gc.C) {
   349  	cfg, err := coreconfig.NewConfig(map[string]interface{}{
   350  		"outlook":     "good",
   351  		"skill-level": 1,
   352  	}, sampleApplicationConfigSchema(), nil)
   353  	c.Assert(err, jc.ErrorIsNil)
   354  
   355  	model, err := s.State.Model()
   356  	c.Assert(err, jc.ErrorIsNil)
   357  
   358  	app, err := application.DeployApplication(stateDeployer{s.State},
   359  		model,
   360  		application.DeployApplicationParams{
   361  			ApplicationName:   "bob",
   362  			Charm:             s.charm,
   363  			ApplicationConfig: cfg,
   364  			CharmOrigin:       corecharm.Origin{Platform: corecharm.Platform{OS: "ubuntu", Channel: "22.04"}},
   365  		})
   366  	c.Assert(err, jc.ErrorIsNil)
   367  	s.assertApplicationConfig(c, app, coreconfig.ConfigAttributes{
   368  		"outlook":     "good",
   369  		"skill-level": 1,
   370  	})
   371  }
   372  
   373  func (s *DeployLocalSuite) TestDeployConstraints(c *gc.C) {
   374  	err := s.State.SetModelConstraints(constraints.MustParse("mem=2G"))
   375  	c.Assert(err, jc.ErrorIsNil)
   376  	applicationCons := constraints.MustParse("cores=2")
   377  
   378  	model, err := s.State.Model()
   379  	c.Assert(err, jc.ErrorIsNil)
   380  
   381  	app, err := application.DeployApplication(stateDeployer{s.State},
   382  		model,
   383  		application.DeployApplicationParams{
   384  			ApplicationName: "bob",
   385  			Charm:           s.charm,
   386  			Constraints:     applicationCons,
   387  			CharmOrigin:     corecharm.Origin{Platform: corecharm.Platform{OS: "ubuntu", Channel: "22.04"}},
   388  		})
   389  	c.Assert(err, jc.ErrorIsNil)
   390  	s.assertConstraints(c, app, constraints.MustParse("cores=2 arch=amd64"))
   391  }
   392  
   393  func (s *DeployLocalSuite) TestDeployNumUnits(c *gc.C) {
   394  	var f fakeDeployer
   395  
   396  	model, err := s.State.Model()
   397  	c.Assert(err, jc.ErrorIsNil)
   398  
   399  	applicationCons := constraints.MustParse("cores=2")
   400  	_, err = application.DeployApplication(&f,
   401  		model,
   402  		application.DeployApplicationParams{
   403  			ApplicationName: "bob",
   404  			Charm:           s.charm,
   405  			Constraints:     applicationCons,
   406  			NumUnits:        2,
   407  			CharmOrigin:     corecharm.Origin{Platform: corecharm.Platform{OS: "ubuntu", Channel: "22.04"}},
   408  		})
   409  	c.Assert(err, jc.ErrorIsNil)
   410  
   411  	c.Assert(f.args.Name, gc.Equals, "bob")
   412  	c.Assert(f.args.Charm, gc.DeepEquals, s.charm)
   413  	c.Assert(f.args.Constraints, gc.DeepEquals, applicationCons)
   414  	c.Assert(f.args.NumUnits, gc.Equals, 2)
   415  }
   416  
   417  func (s *DeployLocalSuite) TestDeployForceMachineId(c *gc.C) {
   418  	var f fakeDeployer
   419  
   420  	model, err := s.State.Model()
   421  	c.Assert(err, jc.ErrorIsNil)
   422  
   423  	applicationCons := constraints.MustParse("cores=2")
   424  	_, err = application.DeployApplication(&f,
   425  		model,
   426  		application.DeployApplicationParams{
   427  			ApplicationName: "bob",
   428  			Charm:           s.charm,
   429  			Constraints:     applicationCons,
   430  			NumUnits:        1,
   431  			Placement:       []*instance.Placement{instance.MustParsePlacement("0")},
   432  			CharmOrigin:     corecharm.Origin{Platform: corecharm.Platform{OS: "ubuntu", Channel: "22.04"}},
   433  		})
   434  	c.Assert(err, jc.ErrorIsNil)
   435  
   436  	c.Assert(f.args.Name, gc.Equals, "bob")
   437  	c.Assert(f.args.Charm, gc.DeepEquals, s.charm)
   438  	c.Assert(f.args.Constraints, gc.DeepEquals, applicationCons)
   439  	c.Assert(f.args.NumUnits, gc.Equals, 1)
   440  	c.Assert(f.args.Placement, gc.HasLen, 1)
   441  	c.Assert(*f.args.Placement[0], gc.Equals, instance.Placement{Scope: instance.MachineScope, Directive: "0"})
   442  }
   443  
   444  func (s *DeployLocalSuite) TestDeployForceMachineIdWithContainer(c *gc.C) {
   445  	var f fakeDeployer
   446  
   447  	model, err := s.State.Model()
   448  	c.Assert(err, jc.ErrorIsNil)
   449  
   450  	applicationCons := constraints.MustParse("cores=2")
   451  	_, err = application.DeployApplication(&f,
   452  		model,
   453  		application.DeployApplicationParams{
   454  			ApplicationName: "bob",
   455  			Charm:           s.charm,
   456  			Constraints:     applicationCons,
   457  			NumUnits:        1,
   458  			Placement:       []*instance.Placement{instance.MustParsePlacement(fmt.Sprintf("%s:0", instance.LXD))},
   459  			CharmOrigin:     corecharm.Origin{Platform: corecharm.Platform{OS: "ubuntu", Channel: "22.04"}},
   460  		})
   461  	c.Assert(err, jc.ErrorIsNil)
   462  	c.Assert(f.args.Name, gc.Equals, "bob")
   463  	c.Assert(f.args.Charm, gc.DeepEquals, s.charm)
   464  	c.Assert(f.args.Constraints, gc.DeepEquals, applicationCons)
   465  	c.Assert(f.args.NumUnits, gc.Equals, 1)
   466  	c.Assert(f.args.Placement, gc.HasLen, 1)
   467  	c.Assert(*f.args.Placement[0], gc.Equals, instance.Placement{Scope: string(instance.LXD), Directive: "0"})
   468  }
   469  
   470  func (s *DeployLocalSuite) TestDeploy(c *gc.C) {
   471  	var f fakeDeployer
   472  
   473  	model, err := s.State.Model()
   474  	c.Assert(err, jc.ErrorIsNil)
   475  
   476  	applicationCons := constraints.MustParse("cores=2")
   477  	placement := []*instance.Placement{
   478  		{Scope: s.State.ModelUUID(), Directive: "valid"},
   479  		{Scope: "#", Directive: "0"},
   480  		{Scope: "lxd", Directive: "1"},
   481  		{Scope: "lxd", Directive: ""},
   482  	}
   483  	_, err = application.DeployApplication(&f,
   484  		model,
   485  		application.DeployApplicationParams{
   486  			ApplicationName: "bob",
   487  			Charm:           s.charm,
   488  			Constraints:     applicationCons,
   489  			NumUnits:        4,
   490  			Placement:       placement,
   491  			CharmOrigin:     corecharm.Origin{Platform: corecharm.Platform{OS: "ubuntu", Channel: "22.04"}},
   492  		})
   493  	c.Assert(err, jc.ErrorIsNil)
   494  
   495  	c.Assert(f.args.Name, gc.Equals, "bob")
   496  	c.Assert(f.args.Charm, gc.DeepEquals, s.charm)
   497  	c.Assert(f.args.Constraints, gc.DeepEquals, applicationCons)
   498  	c.Assert(f.args.NumUnits, gc.Equals, 4)
   499  	c.Assert(f.args.Placement, gc.DeepEquals, placement)
   500  }
   501  
   502  func (s *DeployLocalSuite) TestDeployWithUnmetCharmRequirements(c *gc.C) {
   503  	curl := charm.MustParseURL("local:focal/juju-qa-test-assumes-v2")
   504  	ch := testcharms.Hub.CharmDir("juju-qa-test-assumes-v2")
   505  	charm, err := testing.PutCharm(s.State, curl, ch)
   506  	c.Assert(err, jc.ErrorIsNil)
   507  
   508  	var f = fakeDeployer{}
   509  
   510  	model, err := s.State.Model()
   511  	c.Assert(err, jc.ErrorIsNil)
   512  
   513  	_, err = application.DeployApplication(&f,
   514  		model,
   515  		application.DeployApplicationParams{
   516  			ApplicationName: "assume-metal",
   517  			Charm:           charm,
   518  			NumUnits:        1,
   519  			CharmOrigin:     corecharm.Origin{Platform: corecharm.Platform{OS: "ubuntu", Channel: "22.04"}},
   520  		})
   521  	c.Assert(err, gc.ErrorMatches, "(?m).*Charm feature requirements cannot be met.*")
   522  }
   523  
   524  func (s *DeployLocalSuite) TestDeployWithUnmetCharmRequirementsAndForce(c *gc.C) {
   525  	curl := charm.MustParseURL("local:focal/juju-qa-test-assumes-v2")
   526  	ch := testcharms.Hub.CharmDir("juju-qa-test-assumes-v2")
   527  	charm, err := testing.PutCharm(s.State, curl, ch)
   528  	c.Assert(err, jc.ErrorIsNil)
   529  
   530  	var f = fakeDeployer{}
   531  
   532  	model, err := s.State.Model()
   533  	c.Assert(err, jc.ErrorIsNil)
   534  
   535  	_, err = application.DeployApplication(&f,
   536  		model,
   537  		application.DeployApplicationParams{
   538  			ApplicationName: "assume-metal",
   539  			Charm:           charm,
   540  			NumUnits:        1,
   541  			Force:           true, // bypass assumes checks
   542  			CharmOrigin:     corecharm.Origin{Platform: corecharm.Platform{OS: "ubuntu", Channel: "22.04"}},
   543  		})
   544  	c.Assert(err, jc.ErrorIsNil)
   545  }
   546  
   547  func (s *DeployLocalSuite) TestDeployWithFewerPlacement(c *gc.C) {
   548  	var f fakeDeployer
   549  
   550  	model, err := s.State.Model()
   551  	c.Assert(err, jc.ErrorIsNil)
   552  
   553  	applicationCons := constraints.MustParse("cores=2")
   554  	placement := []*instance.Placement{{Scope: s.State.ModelUUID(), Directive: "valid"}}
   555  	_, err = application.DeployApplication(&f,
   556  		model,
   557  		application.DeployApplicationParams{
   558  			ApplicationName: "bob",
   559  			Charm:           s.charm,
   560  			Constraints:     applicationCons,
   561  			NumUnits:        3,
   562  			Placement:       placement,
   563  			CharmOrigin:     corecharm.Origin{Platform: corecharm.Platform{OS: "ubuntu", Channel: "22.04"}},
   564  		})
   565  	c.Assert(err, jc.ErrorIsNil)
   566  	c.Assert(f.args.Name, gc.Equals, "bob")
   567  	c.Assert(f.args.Charm, gc.DeepEquals, s.charm)
   568  	c.Assert(f.args.Constraints, gc.DeepEquals, applicationCons)
   569  	c.Assert(f.args.NumUnits, gc.Equals, 3)
   570  	c.Assert(f.args.Placement, gc.DeepEquals, placement)
   571  }
   572  
   573  func (s *DeployLocalSuite) assertCharm(c *gc.C, app application.Application, expect string) {
   574  	curl, force := app.CharmURL()
   575  	c.Assert(*curl, gc.Equals, expect)
   576  	c.Assert(force, jc.IsFalse)
   577  }
   578  
   579  func (s *DeployLocalSuite) assertSettings(c *gc.C, app application.Application, _ charm.Settings) {
   580  	settings, err := app.CharmConfig(model.GenerationMaster)
   581  	c.Assert(err, jc.ErrorIsNil)
   582  	expected := s.charm.Config().DefaultSettings()
   583  	for name, value := range settings {
   584  		expected[name] = value
   585  	}
   586  	c.Assert(settings, gc.DeepEquals, expected)
   587  }
   588  
   589  func (s *DeployLocalSuite) assertApplicationConfig(c *gc.C, app application.Application, wantCfg coreconfig.ConfigAttributes) {
   590  	cfg, err := app.ApplicationConfig()
   591  	c.Assert(err, jc.ErrorIsNil)
   592  	c.Assert(cfg, gc.DeepEquals, wantCfg)
   593  }
   594  
   595  func (s *DeployLocalSuite) assertConstraints(c *gc.C, app application.Application, expect constraints.Value) {
   596  	cons, err := app.Constraints()
   597  	c.Assert(err, jc.ErrorIsNil)
   598  	c.Assert(cons, gc.DeepEquals, expect)
   599  }
   600  
   601  func (s *DeployLocalSuite) assertMachines(c *gc.C, app application.Application, expectCons constraints.Value, expectIds ...string) {
   602  	type withAssignedMachineId interface {
   603  		AssignedMachineId() (string, error)
   604  	}
   605  
   606  	units, err := app.AllUnits()
   607  	c.Assert(err, jc.ErrorIsNil)
   608  	c.Assert(units, gc.HasLen, len(expectIds))
   609  	// first manually tell state to assign all the units
   610  	for _, unit := range units {
   611  		id := unit.UnitTag().Id()
   612  		res, err := s.State.AssignStagedUnits([]string{id})
   613  		c.Assert(err, jc.ErrorIsNil)
   614  		c.Assert(res[0].Error, jc.ErrorIsNil)
   615  		c.Assert(res[0].Unit, gc.Equals, id)
   616  	}
   617  
   618  	// refresh the list of units from state
   619  	units, err = app.AllUnits()
   620  	c.Assert(err, jc.ErrorIsNil)
   621  	c.Assert(units, gc.HasLen, len(expectIds))
   622  	unseenIds := set.NewStrings(expectIds...)
   623  	for _, unit := range units {
   624  		id, err := unit.(withAssignedMachineId).AssignedMachineId()
   625  		c.Assert(err, jc.ErrorIsNil)
   626  		unseenIds.Remove(id)
   627  		machine, err := s.State.Machine(id)
   628  		c.Assert(err, jc.ErrorIsNil)
   629  		cons, err := machine.Constraints()
   630  		c.Assert(err, jc.ErrorIsNil)
   631  		c.Assert(cons, gc.DeepEquals, expectCons)
   632  	}
   633  	c.Assert(unseenIds, gc.DeepEquals, set.NewStrings())
   634  }
   635  
   636  type stateDeployer struct {
   637  	*state.State
   638  }
   639  
   640  func (d stateDeployer) AddApplication(args state.AddApplicationArgs) (application.Application, error) {
   641  	app, err := d.State.AddApplication(args)
   642  	if err != nil {
   643  		return nil, err
   644  	}
   645  	return application.NewStateApplication(d.State, app), nil
   646  }
   647  
   648  type fakeDeployer struct {
   649  	args          state.AddApplicationArgs
   650  	controllerCfg *controller.Config
   651  }
   652  
   653  func (f *fakeDeployer) ControllerConfig() (controller.Config, error) {
   654  	if f.controllerCfg != nil {
   655  		return *f.controllerCfg, nil
   656  	}
   657  	return controller.NewConfig(coretesting.ControllerTag.Id(), coretesting.CACert, map[string]interface{}{})
   658  }
   659  
   660  func (f *fakeDeployer) AddApplication(args state.AddApplicationArgs) (application.Application, error) {
   661  	f.args = args
   662  	return nil, nil
   663  }