github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cmd/juju/application/deploy.go (about)

     1  // Copyright 2012, 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package application
     5  
     6  import (
     7  	"archive/zip"
     8  	"io/ioutil"
     9  	"os"
    10  	"path/filepath"
    11  	"strconv"
    12  	"strings"
    13  
    14  	"github.com/juju/cmd"
    15  	"github.com/juju/errors"
    16  	"github.com/juju/gnuflag"
    17  	"github.com/juju/romulus"
    18  	"gopkg.in/juju/charm.v6"
    19  	"gopkg.in/juju/charm.v6/resource"
    20  	"gopkg.in/juju/charmrepo.v3"
    21  	"gopkg.in/juju/charmrepo.v3/csclient"
    22  	"gopkg.in/juju/charmrepo.v3/csclient/params"
    23  	"gopkg.in/juju/names.v2"
    24  	"gopkg.in/macaroon-bakery.v2-unstable/httpbakery"
    25  	"gopkg.in/macaroon.v2-unstable"
    26  	"gopkg.in/yaml.v2"
    27  
    28  	"github.com/juju/juju/api"
    29  	"github.com/juju/juju/api/annotations"
    30  	"github.com/juju/juju/api/application"
    31  	apicharms "github.com/juju/juju/api/charms"
    32  	"github.com/juju/juju/api/controller"
    33  	"github.com/juju/juju/api/modelconfig"
    34  	app "github.com/juju/juju/apiserver/facades/client/application"
    35  	apiparams "github.com/juju/juju/apiserver/params"
    36  	"github.com/juju/juju/charmstore"
    37  	jujucmd "github.com/juju/juju/cmd"
    38  	"github.com/juju/juju/cmd/juju/block"
    39  	"github.com/juju/juju/cmd/juju/common"
    40  	"github.com/juju/juju/cmd/modelcmd"
    41  	"github.com/juju/juju/core/constraints"
    42  	"github.com/juju/juju/core/devices"
    43  	"github.com/juju/juju/core/instance"
    44  	"github.com/juju/juju/core/model"
    45  	"github.com/juju/juju/environs/config"
    46  	"github.com/juju/juju/resource/resourceadapters"
    47  	"github.com/juju/juju/storage"
    48  )
    49  
    50  type CharmAdder interface {
    51  	AddLocalCharm(*charm.URL, charm.Charm, bool) (*charm.URL, error)
    52  	AddCharm(*charm.URL, params.Channel, bool) error
    53  	AddCharmWithAuthorization(*charm.URL, params.Channel, *macaroon.Macaroon, bool) error
    54  	AuthorizeCharmstoreEntity(*charm.URL) (*macaroon.Macaroon, error)
    55  }
    56  
    57  type ApplicationAPI interface {
    58  	AddMachines(machineParams []apiparams.AddMachineParams) ([]apiparams.AddMachinesResult, error)
    59  	AddRelation(endpoints, viaCIDRs []string) (*apiparams.AddRelationResults, error)
    60  	AddUnits(application.AddUnitsParams) ([]string, error)
    61  	Expose(application string) error
    62  	GetAnnotations(tags []string) ([]apiparams.AnnotationsGetResult, error)
    63  	GetConfig(generation model.GenerationVersion, appNames ...string) ([]map[string]interface{}, error)
    64  	GetConstraints(appNames ...string) ([]constraints.Value, error)
    65  	SetAnnotation(annotations map[string]map[string]string) ([]apiparams.ErrorResult, error)
    66  	SetCharm(model.GenerationVersion, application.SetCharmConfig) error
    67  	SetConstraints(application string, constraints constraints.Value) error
    68  	Update(apiparams.ApplicationUpdate) error
    69  	ScaleApplication(application.ScaleApplicationParams) (apiparams.ScaleApplicationResult, error)
    70  }
    71  
    72  type ModelAPI interface {
    73  	ModelUUID() (string, bool)
    74  	ModelGet() (map[string]interface{}, error)
    75  	Sequences() (map[string]int, error)
    76  }
    77  
    78  // MeteredDeployAPI represents the methods of the API the deploy
    79  // command needs for metered charms.
    80  type MeteredDeployAPI interface {
    81  	IsMetered(charmURL string) (bool, error)
    82  	SetMetricCredentials(application string, credentials []byte) error
    83  }
    84  
    85  // CharmDeployAPI represents the methods of the API the deploy
    86  // command needs for charms.
    87  type CharmDeployAPI interface {
    88  	CharmInfo(string) (*apicharms.CharmInfo, error)
    89  }
    90  
    91  // DeployAPI represents the methods of the API the deploy
    92  // command needs.
    93  type DeployAPI interface {
    94  	// TODO(katco): Pair DeployAPI down to only the methods required
    95  	// by the deploy command.
    96  	api.Connection
    97  	CharmAdder
    98  	MeteredDeployAPI
    99  	CharmDeployAPI
   100  	ApplicationAPI
   101  	ModelAPI
   102  
   103  	// ApplicationClient
   104  	Deploy(application.DeployArgs) error
   105  	Status(patterns []string) (*apiparams.FullStatus, error)
   106  
   107  	ResolveWithChannel(*charm.URL) (*charm.URL, params.Channel, []string, error)
   108  
   109  	GetBundle(*charm.URL) (charm.Bundle, error)
   110  
   111  	WatchAll() (*api.AllWatcher, error)
   112  
   113  	// PlanURL returns the configured URL prefix for the metering plan API.
   114  	PlanURL() string
   115  }
   116  
   117  // The following structs exist purely because Go cannot create a
   118  // struct with a field named the same as a method name. The DeployAPI
   119  // needs to both embed a *<package>.Client and provide the
   120  // api.Connection Client method.
   121  //
   122  // Once we pair down DeployAPI, this will not longer be a problem.
   123  
   124  type apiClient struct {
   125  	*api.Client
   126  }
   127  
   128  type charmsClient struct {
   129  	*apicharms.Client
   130  }
   131  
   132  type applicationClient struct {
   133  	*application.Client
   134  }
   135  
   136  type modelConfigClient struct {
   137  	*modelconfig.Client
   138  }
   139  
   140  type charmRepoClient struct {
   141  	*charmrepo.CharmStore
   142  }
   143  
   144  type charmstoreClient struct {
   145  	*csclient.Client
   146  }
   147  
   148  type annotationsClient struct {
   149  	*annotations.Client
   150  }
   151  
   152  type plansClient struct {
   153  	planURL string
   154  }
   155  
   156  func (a *charmstoreClient) AuthorizeCharmstoreEntity(url *charm.URL) (*macaroon.Macaroon, error) {
   157  	return authorizeCharmStoreEntity(a.Client, url)
   158  }
   159  
   160  func (c *plansClient) PlanURL() string {
   161  	return c.planURL
   162  }
   163  
   164  type deployAPIAdapter struct {
   165  	api.Connection
   166  	*apiClient
   167  	*charmsClient
   168  	*applicationClient
   169  	*modelConfigClient
   170  	*charmRepoClient
   171  	*charmstoreClient
   172  	*annotationsClient
   173  	*plansClient
   174  }
   175  
   176  func (a *deployAPIAdapter) Client() *api.Client {
   177  	return a.apiClient.Client
   178  }
   179  
   180  func (a *deployAPIAdapter) ModelUUID() (string, bool) {
   181  	return a.apiClient.ModelUUID()
   182  }
   183  
   184  func (a *deployAPIAdapter) Deploy(args application.DeployArgs) error {
   185  	for i, p := range args.Placement {
   186  		if p.Scope == "model-uuid" {
   187  			p.Scope = a.applicationClient.ModelUUID()
   188  		}
   189  		args.Placement[i] = p
   190  	}
   191  
   192  	return errors.Trace(a.applicationClient.Deploy(args))
   193  }
   194  
   195  func (a *deployAPIAdapter) Resolve(cfg *config.Config, url *charm.URL) (
   196  	*charm.URL,
   197  	params.Channel,
   198  	[]string,
   199  	error,
   200  ) {
   201  	return resolveCharm(a.charmRepoClient.ResolveWithChannel, url)
   202  }
   203  
   204  func (a *deployAPIAdapter) Get(url *charm.URL) (charm.Charm, error) {
   205  	return a.charmRepoClient.Get(url)
   206  }
   207  
   208  func (a *deployAPIAdapter) SetAnnotation(annotations map[string]map[string]string) ([]apiparams.ErrorResult, error) {
   209  	return a.annotationsClient.Set(annotations)
   210  }
   211  
   212  func (a *deployAPIAdapter) GetAnnotations(tags []string) ([]apiparams.AnnotationsGetResult, error) {
   213  	return a.annotationsClient.Get(tags)
   214  }
   215  
   216  // NewDeployCommandForTest returns a command to deploy applications intended to be used only in tests.
   217  func NewDeployCommandForTest(newAPIRoot func() (DeployAPI, error), steps []DeployStep) modelcmd.ModelCommand {
   218  	deployCmd := &DeployCommand{
   219  		Steps:      steps,
   220  		NewAPIRoot: newAPIRoot,
   221  	}
   222  	if newAPIRoot == nil {
   223  		deployCmd.NewAPIRoot = func() (DeployAPI, error) {
   224  			apiRoot, err := deployCmd.ModelCommandBase.NewAPIRoot()
   225  			if err != nil {
   226  				return nil, errors.Trace(err)
   227  			}
   228  			bakeryClient, err := deployCmd.BakeryClient()
   229  			if err != nil {
   230  				return nil, errors.Trace(err)
   231  			}
   232  			controllerAPIRoot, err := deployCmd.NewControllerAPIRoot()
   233  			if err != nil {
   234  				return nil, errors.Trace(err)
   235  			}
   236  			csURL, err := getCharmStoreAPIURL(controllerAPIRoot)
   237  			if err != nil {
   238  				return nil, errors.Trace(err)
   239  			}
   240  			mURL, err := deployCmd.getMeteringAPIURL(controllerAPIRoot)
   241  			if err != nil {
   242  				return nil, errors.Trace(err)
   243  			}
   244  			cstoreClient := newCharmStoreClient(bakeryClient, csURL).WithChannel(deployCmd.Channel)
   245  
   246  			return &deployAPIAdapter{
   247  				Connection:        apiRoot,
   248  				apiClient:         &apiClient{Client: apiRoot.Client()},
   249  				charmsClient:      &charmsClient{Client: apicharms.NewClient(apiRoot)},
   250  				applicationClient: &applicationClient{Client: application.NewClient(apiRoot)},
   251  				modelConfigClient: &modelConfigClient{Client: modelconfig.NewClient(apiRoot)},
   252  				charmstoreClient:  &charmstoreClient{Client: cstoreClient},
   253  				annotationsClient: &annotationsClient{Client: annotations.NewClient(apiRoot)},
   254  				charmRepoClient:   &charmRepoClient{CharmStore: charmrepo.NewCharmStoreFromClient(cstoreClient)},
   255  				plansClient:       &plansClient{planURL: mURL},
   256  			}, nil
   257  		}
   258  	}
   259  	return modelcmd.Wrap(deployCmd)
   260  }
   261  
   262  // NewDeployCommand returns a command to deploy applications.
   263  func NewDeployCommand() modelcmd.ModelCommand {
   264  	steps := []DeployStep{
   265  		&RegisterMeteredCharm{
   266  			PlanURL:      romulus.DefaultAPIRoot,
   267  			RegisterPath: "/plan/authorize",
   268  			QueryPath:    "/charm",
   269  		},
   270  		&ValidateLXDProfileCharm{},
   271  	}
   272  	deployCmd := &DeployCommand{
   273  		Steps: steps,
   274  	}
   275  	deployCmd.NewAPIRoot = func() (DeployAPI, error) {
   276  		apiRoot, err := deployCmd.ModelCommandBase.NewAPIRoot()
   277  		if err != nil {
   278  			return nil, errors.Trace(err)
   279  		}
   280  
   281  		controllerAPIRoot, err := deployCmd.NewControllerAPIRoot()
   282  		if err != nil {
   283  			return nil, errors.Trace(err)
   284  		}
   285  		csURL, err := getCharmStoreAPIURL(controllerAPIRoot)
   286  		if err != nil {
   287  			return nil, errors.Trace(err)
   288  		}
   289  		mURL, err := deployCmd.getMeteringAPIURL(controllerAPIRoot)
   290  		if err != nil {
   291  			return nil, errors.Trace(err)
   292  		}
   293  		bakeryClient, err := deployCmd.BakeryClient()
   294  		if err != nil {
   295  			return nil, errors.Trace(err)
   296  		}
   297  		cstoreClient := newCharmStoreClient(bakeryClient, csURL).WithChannel(deployCmd.Channel)
   298  
   299  		return &deployAPIAdapter{
   300  			Connection:        apiRoot,
   301  			apiClient:         &apiClient{Client: apiRoot.Client()},
   302  			charmsClient:      &charmsClient{Client: apicharms.NewClient(apiRoot)},
   303  			applicationClient: &applicationClient{Client: application.NewClient(apiRoot)},
   304  			modelConfigClient: &modelConfigClient{Client: modelconfig.NewClient(apiRoot)},
   305  			charmstoreClient:  &charmstoreClient{Client: cstoreClient},
   306  			annotationsClient: &annotationsClient{Client: annotations.NewClient(apiRoot)},
   307  			charmRepoClient:   &charmRepoClient{CharmStore: charmrepo.NewCharmStoreFromClient(cstoreClient)},
   308  			plansClient:       &plansClient{planURL: mURL},
   309  		}, nil
   310  	}
   311  
   312  	return modelcmd.Wrap(deployCmd)
   313  }
   314  
   315  type DeployCommand struct {
   316  	modelcmd.ModelCommandBase
   317  	UnitCommandBase
   318  
   319  	// CharmOrBundle is either a charm URL, a path where a charm can be found,
   320  	// or a bundle name.
   321  	CharmOrBundle string
   322  
   323  	// BundleOverlay refers to config files that specify additional bundle
   324  	// configuration to be merged with the main bundle.
   325  	BundleOverlayFile []string
   326  
   327  	// Channel holds the charmstore channel to use when obtaining
   328  	// the charm to be deployed.
   329  	Channel params.Channel
   330  
   331  	// Series is the series of the charm to deploy.
   332  	Series string
   333  
   334  	// Force is used to allow a charm to be deployed onto a machine
   335  	// running an unsupported series.
   336  	Force bool
   337  
   338  	// DryRun is used to specify that the bundle shouldn't actually be
   339  	// deployed but just output the changes.
   340  	DryRun bool
   341  
   342  	ApplicationName string
   343  	ConfigOptions   common.ConfigFlag
   344  	ConstraintsStr  string
   345  	Constraints     constraints.Value
   346  	BindToSpaces    string
   347  
   348  	// TODO(axw) move this to UnitCommandBase once we support --storage
   349  	// on add-unit too.
   350  	//
   351  	// Storage is a map of storage constraints, keyed on the storage name
   352  	// defined in charm storage metadata.
   353  	Storage map[string]storage.Constraints
   354  
   355  	// BundleStorage maps application names to maps of storage constraints keyed on
   356  	// the storage name defined in that application's charm storage metadata.
   357  	BundleStorage map[string]map[string]storage.Constraints
   358  
   359  	// Devices is a mapping of device constraints, keyed on the device name
   360  	// defined in charm devices metadata.
   361  	Devices map[string]devices.Constraints
   362  
   363  	// BundleDevices maps application names to maps of device constraints keyed on
   364  	// the device name defined in that application's charm devices metadata.
   365  	BundleDevices map[string]map[string]devices.Constraints
   366  
   367  	// Resources is a map of resource name to filename to be uploaded on deploy.
   368  	Resources map[string]string
   369  
   370  	Bindings map[string]string
   371  	Steps    []DeployStep
   372  
   373  	// UseExisting machines when deploying the bundle.
   374  	UseExisting bool
   375  	// BundleMachines is a mapping for machines in the bundle to machines
   376  	// in the model.
   377  	BundleMachines map[string]string
   378  
   379  	// NewAPIRoot stores a function which returns a new API root.
   380  	NewAPIRoot func() (DeployAPI, error)
   381  
   382  	// Trust signifies that the charm should be deployed with access to
   383  	// trusted credentials. That is, hooks run by the charm can access
   384  	// cloud credentials and other trusted access credentials.
   385  	Trust bool
   386  
   387  	machineMap string
   388  	flagSet    *gnuflag.FlagSet
   389  
   390  	unknownModel bool
   391  }
   392  
   393  const deployDoc = `
   394  A charm can be referred to by its simple name and a series can optionally be
   395  specified:
   396  
   397    juju deploy postgresql
   398    juju deploy xenial/postgresql
   399    juju deploy cs:postgresql
   400    juju deploy cs:xenial/postgresql
   401    juju deploy postgresql --series xenial
   402  
   403  All the above deployments use remote charms found in the Charm Store (denoted
   404  by 'cs') and therefore also make use of "charm URLs".
   405  
   406  A versioned charm URL will be expanded as expected. For example, 'mysql-56'
   407  becomes 'cs:xenial/mysql-56'.
   408  
   409  A local charm may be deployed by giving the path to its directory:
   410  
   411    juju deploy /path/to/charm
   412    juju deploy /path/to/charm --series xenial
   413  
   414  You will need to be explicit if there is an ambiguity between a local and a
   415  remote charm:
   416  
   417    juju deploy ./pig
   418    juju deploy cs:pig
   419  
   420  An error is emitted if the determined series is not supported by the charm. Use
   421  the '--force' option to override this check:
   422  
   423    juju deploy charm --series xenial --force
   424  
   425  A bundle can be expressed similarly to a charm, but not by series:
   426  
   427    juju deploy mediawiki-single
   428    juju deploy bundle/mediawiki-single
   429    juju deploy cs:bundle/mediawiki-single
   430  
   431  A local bundle may be deployed by specifying the path to its YAML file:
   432  
   433    juju deploy /path/to/bundle.yaml
   434  
   435  The final charm/machine series is determined using an order of precedence (most
   436  preferred to least):
   437  
   438   - the '--series' command option
   439   - the series stated in the charm URL
   440   - for a bundle, the series stated in each charm URL (in the bundle file)
   441   - for a bundle, the series given at the top level (in the bundle file)
   442   - the 'default-series' model key
   443   - the top-most series specified in the charm's metadata file
   444     (this sets the charm's 'preferred series' in the Charm Store)
   445  
   446  An 'application name' provides an alternate name for the application. It works
   447  only for charms; it is silently ignored for bundles (although the same can be
   448  done at the bundle file level). Such a name must consist only of lower-case
   449  letters (a-z), numbers (0-9), and single hyphens (-). The name must begin with
   450  a letter and not have a group of all numbers follow a hyphen:
   451  
   452    Valid:   myappname, custom-app, app2-scat-23skidoo
   453    Invalid: myAppName, custom--app, app2-scat-23, areacode-555-info
   454  
   455  Use the '--constraints' option to specify hardware requirements for new machines.
   456  These become the application's default constraints (i.e. they are used if the
   457  application is later scaled out with the ` + "`add-unit`" + ` command). To overcome this
   458  behaviour use the ` + "`set-constraints`" + ` command to change the application's default
   459  constraints or add a machine (` + "`add-machine`" + `) with a certain constraint and then
   460  target that machine with ` + "`add-unit`" + ` by using the '--to' option.
   461  
   462  Use the '--device' option to specify GPU device requirements (with Kubernetes).
   463  The below format is used for this option's value, where the 'label' is named in
   464  the charm metadata file:
   465  
   466    <label>=[<count>,]<device-class>|<vendor/type>[,<attributes>]
   467  
   468  Use the '--config' option to specify application configuration values. This
   469  option accepts either a path to a YAML-formatted file or a key=value pair. A
   470  file should be of this format:
   471  
   472    <charm name>:
   473  	<option name>: <option value>
   474  	...
   475  
   476  For example, to deploy 'mediawiki' with file 'mycfg.yaml' that contains:
   477  
   478    mediawiki:
   479  	name: my media wiki
   480  	admins: me:pwdOne
   481  	debug: true
   482  
   483  use
   484  
   485    juju deploy mediawiki --config mycfg.yaml
   486  
   487  Key=value pairs can also be passed directly in the command. For example, to
   488  declare the 'name' key:
   489  
   490    juju deploy mediawiki --config name='my media wiki'
   491  
   492  To define multiple keys:
   493  
   494    juju deploy mediawiki --config name='my media wiki' --config debug=true
   495  
   496  If a key gets defined multiple times the last value will override any earlier
   497  values. For example,
   498  
   499    juju deploy mediawiki --config name='my media wiki' --config mycfg.yaml
   500  
   501  if mycfg.yaml contains a value for 'name', it will override the earlier 'my
   502  media wiki' value. The same applies to single value options. For example,
   503  
   504    juju deploy mediawiki --config name='a media wiki' --config name='my wiki'
   505  
   506  the value of 'my wiki' will be used.
   507  
   508  Use the '--resource' option to upload resources needed by the charm. This
   509  option may be repeated if multiple resources are needed:
   510  
   511    juju deploy foo --resource bar=/some/file.tgz --resource baz=./docs/cfg.xml
   512  
   513  Where 'bar' and 'baz' are named in the metadata file for charm 'foo'.
   514  
   515  Use the '--to' option to deploy to an existing machine or container by
   516  specifying a "placement directive". The ` + "`status`" + ` command should be used for
   517  guidance on how to refer to machines. A few placement directives are
   518  provider-dependent (e.g.: 'zone').
   519  
   520  In more complex scenarios, "network spaces" are used to partition the cloud
   521  networking layer into sets of subnets. Instances hosting units inside the same
   522  space can communicate with each other without any firewalls. Traffic crossing
   523  space boundaries could be subject to firewall and access restrictions. Using
   524  spaces as deployment targets, rather than their individual subnets, allows Juju
   525  to perform automatic distribution of units across availability zones to support
   526  high availability for applications. Spaces help isolate applications and their
   527  units, both for security purposes and to manage both traffic segregation and
   528  congestion.
   529  
   530  When deploying an application or adding machines, the 'spaces' constraint can
   531  be used to define a comma-delimited list of required and forbidden spaces (the
   532  latter prefixed with '^', similar to the 'tags' constraint).
   533  
   534  When deploying bundles, machines specified in the bundle are added to the model
   535  as new machines. Use the '--map-machines=existing' option to make use of any
   536  existing machines. To map particular existing machines to machines defined in
   537  the bundle, multiple comma separated values of the form 'bundle-id=existing-id'
   538  can be passed. For example, for a bundle that specifies machines 1, 2, and 3;
   539  and a model that has existing machines 1, 2, 3, and 4, the below deployment
   540  would have existing machines 1 and 2 assigned to machines 1 and 2 defined in
   541  the bundle and have existing machine 4 assigned to machine 3 defined in the
   542  bundle.
   543  
   544    juju deploy mybundle --map-machines=existing,3=4
   545  
   546  Only top level machines can be mapped in this way, just as only top level
   547  machines can be defined in the machines section of the bundle.
   548  
   549  When charms that include LXD profiles are deployed the profiles are validated
   550  for security purposes by allowing only certain configurations and devices. Use
   551  the '--force' option to bypass this check. Doing so is not recommended as it
   552  can lead to unexpected behaviour.
   553  
   554  Further reading: https://docs.jujucharms.com/stable/charms-deploying
   555  
   556  Examples:
   557  
   558  Deploy to a new machine:
   559  
   560      juju deploy apache2
   561  
   562  Deploy to machine 23:
   563  
   564      juju deploy mysql --to 23
   565  
   566  Deploy to a new LXD container on a new machine:
   567  
   568      juju deploy mysql --to lxd
   569  
   570  Deploy to a new LXD container on machine 25:
   571  
   572      juju deploy mysql --to lxd:25
   573  
   574  Deploy to LXD container 3 on machine 24:
   575  
   576      juju deploy mysql --to 24/lxd/3
   577  
   578  Deploy 2 units, one on machine 3 and one to a new LXD container on machine 5:
   579  
   580      juju deploy mysql -n 2 --to 3,lxd:5
   581  
   582  Deploy 3 units, one on machine 3 and the remaining two on new machines:
   583  
   584      juju deploy mysql -n 3 --to 3
   585  
   586  Deploy to a machine with at least 8 GiB of memory:
   587  
   588      juju deploy postgresql --constraints mem=8G
   589  
   590  Deploy to a specific availability zone (provider-dependent):
   591  
   592      juju deploy mysql --to zone=us-east-1a
   593  
   594  Deploy to a specific MAAS node:
   595  
   596      juju deploy mysql --to host.maas
   597  
   598  Deploy to a machine that is in the 'dmz' network space but not in either the
   599  'cms' nor the 'database' spaces:
   600  
   601      juju deploy haproxy -n 2 --constraints spaces=dmz,^cms,^database
   602  
   603  Deploy a Kubernetes charm that requires a single Nvidia GPU:
   604  
   605      juju deploy mycharm --device miner=1,nvidia.com/gpu
   606  
   607  Deploy a Kubernetes charm that requires two Nvidia GPUs that have an
   608  attribute of 'gpu=nvidia-tesla-p100':
   609  
   610      juju deploy mycharm --device \
   611         twingpu=2,nvidia.com/gpu,gpu=nvidia-tesla-p100
   612  
   613  See also:
   614      add-relation
   615      add-unit
   616      config
   617      expose
   618      get-constraints
   619      set-constraints
   620      spaces
   621  `
   622  
   623  //go:generate mockgen -package mocks -destination mocks/deploystepapi_mock.go github.com/juju/juju/cmd/juju/application DeployStepAPI
   624  
   625  // DeployStepAPI represents a API required for deploying using the step
   626  // deployment code.
   627  type DeployStepAPI interface {
   628  	MeteredDeployAPI
   629  }
   630  
   631  // DeployStep is an action that needs to be taken during charm deployment.
   632  type DeployStep interface {
   633  	// SetFlags sets flags necessary for the deploy step.
   634  	SetFlags(*gnuflag.FlagSet)
   635  
   636  	// SetPlanURL sets the plan URL prefix.
   637  	SetPlanURL(planURL string)
   638  
   639  	// RunPre runs before the call is made to add the charm to the environment.
   640  	RunPre(DeployStepAPI, *httpbakery.Client, *cmd.Context, DeploymentInfo) error
   641  
   642  	// RunPost runs after the call is made to add the charm to the environment.
   643  	// The error parameter is used to notify the step of a previously occurred error.
   644  	RunPost(DeployStepAPI, *httpbakery.Client, *cmd.Context, DeploymentInfo, error) error
   645  }
   646  
   647  // DeploymentInfo is used to maintain all deployment information for
   648  // deployment steps.
   649  type DeploymentInfo struct {
   650  	CharmID         charmstore.CharmID
   651  	ApplicationName string
   652  	ModelUUID       string
   653  	CharmInfo       *apicharms.CharmInfo
   654  	ApplicationPlan string
   655  	Force           bool
   656  }
   657  
   658  func (c *DeployCommand) Info() *cmd.Info {
   659  	return jujucmd.Info(&cmd.Info{
   660  		Name:    "deploy",
   661  		Args:    "<charm or bundle> [<application name>]",
   662  		Purpose: "Deploys a new application or bundle.",
   663  		Doc:     deployDoc,
   664  	})
   665  }
   666  
   667  var (
   668  	// TODO(thumper): support dry-run for apps as well as bundles.
   669  	bundleOnlyFlags = []string{
   670  		"overlay", "dry-run", "map-machines",
   671  	}
   672  )
   673  
   674  // charmOnlyFlags and bundleOnlyFlags are used to validate flags based on
   675  // whether we are deploying a charm or a bundle.
   676  func charmOnlyFlags() []string {
   677  	charmOnlyFlags := []string{
   678  		"bind", "config", "constraints", "force", "n", "num-units",
   679  		"series", "to", "resource", "attach-storage",
   680  	}
   681  
   682  	charmOnlyFlags = append(charmOnlyFlags, "trust")
   683  
   684  	return charmOnlyFlags
   685  }
   686  
   687  func (c *DeployCommand) SetFlags(f *gnuflag.FlagSet) {
   688  	c.ConfigOptions.SetPreserveStringValue(true)
   689  	// Keep above charmOnlyFlags and bundleOnlyFlags lists updated when adding
   690  	// new flags.
   691  	c.UnitCommandBase.SetFlags(f)
   692  	c.ModelCommandBase.SetFlags(f)
   693  	f.IntVar(&c.NumUnits, "n", 1, "Number of application units to deploy for principal charms")
   694  	f.StringVar((*string)(&c.Channel), "channel", "", "Channel to use when getting the charm or bundle from the charm store")
   695  	f.Var(&c.ConfigOptions, "config", "Either a path to yaml-formatted application config file or a key=value pair ")
   696  
   697  	f.BoolVar(&c.Trust, "trust", false, "Allows charm to run hooks that require access credentials")
   698  
   699  	f.Var(cmd.NewAppendStringsValue(&c.BundleOverlayFile), "overlay", "Bundles to overlay on the primary bundle, applied in order")
   700  	f.StringVar(&c.ConstraintsStr, "constraints", "", "Set application constraints")
   701  	f.StringVar(&c.Series, "series", "", "The series on which to deploy")
   702  	f.BoolVar(&c.DryRun, "dry-run", false, "Just show what the bundle deploy would do")
   703  	f.BoolVar(&c.Force, "force", false, "Allow a charm to be deployed which bypasses checks such as supported series or LXD profile allow list")
   704  	f.Var(storageFlag{&c.Storage, &c.BundleStorage}, "storage", "Charm storage constraints")
   705  	f.Var(devicesFlag{&c.Devices, &c.BundleDevices}, "device", "Charm device constraints")
   706  	f.Var(stringMap{&c.Resources}, "resource", "Resource to be uploaded to the controller")
   707  	f.StringVar(&c.BindToSpaces, "bind", "", "Configure application endpoint bindings to spaces")
   708  	f.StringVar(&c.machineMap, "map-machines", "", "Specify the existing machines to use for bundle deployments")
   709  
   710  	for _, step := range c.Steps {
   711  		step.SetFlags(f)
   712  	}
   713  	c.flagSet = f
   714  }
   715  
   716  func (c *DeployCommand) Init(args []string) error {
   717  	if err := c.validateStorageByModelType(); err != nil {
   718  		if !errors.IsNotFound(err) {
   719  			return errors.Trace(err)
   720  		}
   721  		// It is possible that we will not be able to get model type to validate with.
   722  		// For example, if current client does not know about a model, we
   723  		// would have queried the controller about the model. However,
   724  		// at Init() we do not yet have an API connection.
   725  		// So we do not want to fail here if we encountered NotFoundErr, we want to
   726  		// do a late validation at Run().
   727  		c.unknownModel = true
   728  	}
   729  	switch len(args) {
   730  	case 2:
   731  		if !names.IsValidApplication(args[1]) {
   732  			return errors.Errorf("invalid application name %q", args[1])
   733  		}
   734  		c.ApplicationName = args[1]
   735  		fallthrough
   736  	case 1:
   737  		c.CharmOrBundle = args[0]
   738  	case 0:
   739  		return errors.New("no charm or bundle specified")
   740  	default:
   741  		return cmd.CheckEmpty(args[2:])
   742  	}
   743  
   744  	if err := c.parseBind(); err != nil {
   745  		return err
   746  	}
   747  
   748  	useExisting, mapping, err := parseMachineMap(c.machineMap)
   749  	if err != nil {
   750  		return errors.Annotate(err, "error in --map-machines")
   751  	}
   752  	c.UseExisting = useExisting
   753  	c.BundleMachines = mapping
   754  
   755  	if err := c.UnitCommandBase.Init(args); err != nil {
   756  		return err
   757  	}
   758  	if err := c.validatePlacementByModelType(); err != nil {
   759  		if !errors.IsNotFound(err) {
   760  			return errors.Trace(err)
   761  		}
   762  		// It is possible that we will not be able to get model type to validate with.
   763  		// For example, if current client does not know about a model, we
   764  		// would have queried the controller about the model. However,
   765  		// at Init() we do not yet have an API connection.
   766  		// So we do not want to fail here if we encountered NotFoundErr, we want to
   767  		// do a late validation at Run().
   768  		c.unknownModel = true
   769  	}
   770  	return nil
   771  }
   772  
   773  func (c *DeployCommand) validateStorageByModelType() error {
   774  	modelType, err := c.ModelType()
   775  	if err != nil {
   776  		return err
   777  	}
   778  	if modelType == model.IAAS {
   779  		return nil
   780  	}
   781  	if len(c.AttachStorage) > 0 {
   782  		return errors.New("--attach-storage cannot be used on kubernetes models")
   783  	}
   784  	return nil
   785  }
   786  
   787  func (c *DeployCommand) validatePlacementByModelType() error {
   788  	modelType, err := c.ModelType()
   789  	if err != nil {
   790  		return err
   791  	}
   792  	if modelType == model.IAAS {
   793  		return nil
   794  	}
   795  	if len(c.Placement) > 1 {
   796  		return errors.Errorf("only 1 placement directive is supported, got %d", len(c.Placement))
   797  	}
   798  	if len(c.Placement) == 0 {
   799  		return nil
   800  	}
   801  	if c.Placement[0].Scope == instance.MachineScope || c.Placement[0].Directive == "" {
   802  		return errors.NotSupportedf("placement directive %q", c.PlacementSpec)
   803  	}
   804  	return nil
   805  }
   806  
   807  func parseMachineMap(value string) (bool, map[string]string, error) {
   808  	parts := strings.Split(value, ",")
   809  	useExisting := false
   810  	mapping := make(map[string]string)
   811  	for _, part := range parts {
   812  		part = strings.TrimSpace(part)
   813  		switch part {
   814  		case "":
   815  			// No-op.
   816  		case "existing":
   817  			useExisting = true
   818  		default:
   819  			otherParts := strings.Split(part, "=")
   820  			if len(otherParts) != 2 {
   821  				return false, nil, errors.Errorf("expected \"existing\" or \"<bundle-id>=<machine-id>\", got %q", part)
   822  			}
   823  			bundleID, machineID := strings.TrimSpace(otherParts[0]), strings.TrimSpace(otherParts[1])
   824  
   825  			if i, err := strconv.Atoi(bundleID); err != nil || i < 0 {
   826  				return false, nil, errors.Errorf("bundle-id %q is not a top level machine id", bundleID)
   827  			}
   828  			if i, err := strconv.Atoi(machineID); err != nil || i < 0 {
   829  				return false, nil, errors.Errorf("machine-id %q is not a top level machine id", machineID)
   830  			}
   831  			mapping[bundleID] = machineID
   832  		}
   833  	}
   834  	return useExisting, mapping, nil
   835  }
   836  
   837  type ModelConfigGetter interface {
   838  	ModelGet() (map[string]interface{}, error)
   839  }
   840  
   841  var getModelConfig = func(api ModelConfigGetter) (*config.Config, error) {
   842  	// Separated into a variable for easy overrides
   843  	attrs, err := api.ModelGet()
   844  	if err != nil {
   845  		return nil, errors.Wrap(err, errors.New("cannot fetch model settings"))
   846  	}
   847  
   848  	return config.New(config.NoDefaults, attrs)
   849  }
   850  
   851  func (c *DeployCommand) deployBundle(
   852  	ctx *cmd.Context,
   853  	filePath string,
   854  	data *charm.BundleData,
   855  	bundleURL *charm.URL,
   856  	channel params.Channel,
   857  	apiRoot DeployAPI,
   858  	bundleStorage map[string]map[string]storage.Constraints,
   859  	bundleDevices map[string]map[string]devices.Constraints,
   860  ) (rErr error) {
   861  	bakeryClient, err := c.BakeryClient()
   862  	if err != nil {
   863  		return errors.Trace(err)
   864  	}
   865  	modelUUID, ok := apiRoot.ModelUUID()
   866  	if !ok {
   867  		return errors.New("API connection is controller-only (should never happen)")
   868  	}
   869  
   870  	for application, applicationSpec := range data.Applications {
   871  		if applicationSpec.Plan != "" {
   872  			for _, step := range c.Steps {
   873  				s := step
   874  				charmURL, err := charm.ParseURL(applicationSpec.Charm)
   875  				if err != nil {
   876  					return errors.Trace(err)
   877  				}
   878  
   879  				deployInfo := DeploymentInfo{
   880  					CharmID:         charmstore.CharmID{URL: charmURL},
   881  					ApplicationName: application,
   882  					ApplicationPlan: applicationSpec.Plan,
   883  					ModelUUID:       modelUUID,
   884  					Force:           c.Force,
   885  				}
   886  
   887  				err = s.RunPre(apiRoot, bakeryClient, ctx, deployInfo)
   888  				if err != nil {
   889  					return errors.Trace(err)
   890  				}
   891  
   892  				defer func() {
   893  					err = errors.Trace(s.RunPost(apiRoot, bakeryClient, ctx, deployInfo, rErr))
   894  					if err != nil {
   895  						rErr = err
   896  					}
   897  				}()
   898  			}
   899  		}
   900  	}
   901  
   902  	// TODO(ericsnow) Do something with the CS macaroons that were returned?
   903  	// Deploying bundles does not allow the use force, it's expected that the
   904  	// bundle is correct and therefore the charms are also.
   905  	if _, err := deployBundle(
   906  		filePath,
   907  		data,
   908  		bundleURL,
   909  		c.BundleOverlayFile,
   910  		channel,
   911  		apiRoot,
   912  		ctx,
   913  		bundleStorage,
   914  		bundleDevices,
   915  		c.DryRun,
   916  		c.UseExisting,
   917  		c.BundleMachines,
   918  	); err != nil {
   919  		return errors.Annotate(err, "cannot deploy bundle")
   920  	}
   921  	return nil
   922  }
   923  
   924  func (c *DeployCommand) deployCharm(
   925  	id charmstore.CharmID,
   926  	csMac *macaroon.Macaroon,
   927  	series string,
   928  	ctx *cmd.Context,
   929  	apiRoot DeployAPI,
   930  ) (rErr error) {
   931  	charmInfo, err := apiRoot.CharmInfo(id.URL.String())
   932  	if err != nil {
   933  		return err
   934  	}
   935  
   936  	if len(c.AttachStorage) > 0 && apiRoot.BestFacadeVersion("Application") < 5 {
   937  		// DeployArgs.AttachStorage is only supported from
   938  		// Application API version 5 and onwards.
   939  		return errors.New("this juju controller does not support --attach-storage")
   940  	}
   941  
   942  	// Storage cannot be added to a container.
   943  	if len(c.Storage) > 0 || len(c.AttachStorage) > 0 {
   944  		for _, placement := range c.Placement {
   945  			if t, err := instance.ParseContainerType(placement.Scope); err == nil {
   946  				return errors.NotSupportedf("adding storage to %s container", string(t))
   947  			}
   948  		}
   949  	}
   950  
   951  	numUnits := c.NumUnits
   952  	if charmInfo.Meta.Subordinate {
   953  		if !constraints.IsEmpty(&c.Constraints) {
   954  			return errors.New("cannot use --constraints with subordinate application")
   955  		}
   956  		if numUnits == 1 && c.PlacementSpec == "" {
   957  			numUnits = 0
   958  		} else {
   959  			return errors.New("cannot use --num-units or --to with subordinate application")
   960  		}
   961  	}
   962  	applicationName := c.ApplicationName
   963  	if applicationName == "" {
   964  		applicationName = charmInfo.Meta.Name
   965  	}
   966  
   967  	// Process the --config args.
   968  	// We may have a single file arg specified, in which case
   969  	// it points to a YAML file keyed on the charm name and
   970  	// containing values for any charm settings.
   971  	// We may also have key/value pairs representing
   972  	// charm settings which overrides anything in the YAML file.
   973  	// If more than one file is specified, that is an error.
   974  	var configYAML []byte
   975  	files, err := c.ConfigOptions.AbsoluteFileNames(ctx)
   976  	if err != nil {
   977  		return errors.Trace(err)
   978  	}
   979  	if len(files) > 1 {
   980  		return errors.Errorf("only a single config YAML file can be specified, got %d", len(files))
   981  	}
   982  	if len(files) == 1 {
   983  		configYAML, err = ioutil.ReadFile(files[0])
   984  		if err != nil {
   985  			return errors.Trace(err)
   986  		}
   987  	}
   988  	attr, err := c.ConfigOptions.ReadConfigPairs(ctx)
   989  	if err != nil {
   990  		return errors.Trace(err)
   991  	}
   992  	appConfig := make(map[string]string)
   993  	for k, v := range attr {
   994  		appConfig[k] = v.(string)
   995  	}
   996  
   997  	// Expand the trust flag into the appConfig
   998  	if c.Trust {
   999  		appConfig[app.TrustConfigOptionName] = strconv.FormatBool(c.Trust)
  1000  	}
  1001  
  1002  	// Application facade V5 expects charm config to either all be in YAML
  1003  	// or config map. If config map is specified, that overrides YAML.
  1004  	// So we need to combine the two here to have only one.
  1005  	if apiRoot.BestFacadeVersion("Application") < 6 && len(appConfig) > 0 {
  1006  		var configFromFile map[string]map[string]string
  1007  		err := yaml.Unmarshal(configYAML, &configFromFile)
  1008  		if err != nil {
  1009  			return errors.Annotate(err, "badly formatted YAML config file")
  1010  		}
  1011  		if configFromFile == nil {
  1012  			configFromFile = make(map[string]map[string]string)
  1013  		}
  1014  		charmSettings, ok := configFromFile[applicationName]
  1015  		if !ok {
  1016  			charmSettings = make(map[string]string)
  1017  		}
  1018  		for k, v := range appConfig {
  1019  			charmSettings[k] = v
  1020  		}
  1021  		appConfig = nil
  1022  		configFromFile[applicationName] = charmSettings
  1023  		configYAML, err = yaml.Marshal(configFromFile)
  1024  		if err != nil {
  1025  			return errors.Trace(err)
  1026  		}
  1027  	}
  1028  
  1029  	bakeryClient, err := c.BakeryClient()
  1030  	if err != nil {
  1031  		return errors.Trace(err)
  1032  	}
  1033  
  1034  	uuid, ok := apiRoot.ModelUUID()
  1035  	if !ok {
  1036  		return errors.New("API connection is controller-only (should never happen)")
  1037  	}
  1038  
  1039  	deployInfo := DeploymentInfo{
  1040  		CharmID:         id,
  1041  		ApplicationName: applicationName,
  1042  		ModelUUID:       uuid,
  1043  		CharmInfo:       charmInfo,
  1044  		Force:           c.Force,
  1045  	}
  1046  
  1047  	for _, step := range c.Steps {
  1048  		err = step.RunPre(apiRoot, bakeryClient, ctx, deployInfo)
  1049  		if err != nil {
  1050  			return errors.Trace(err)
  1051  		}
  1052  	}
  1053  
  1054  	defer func() {
  1055  		for _, step := range c.Steps {
  1056  			err = errors.Trace(step.RunPost(apiRoot, bakeryClient, ctx, deployInfo, rErr))
  1057  			if err != nil {
  1058  				rErr = err
  1059  			}
  1060  		}
  1061  	}()
  1062  
  1063  	if id.URL != nil && id.URL.Schema != "local" && len(charmInfo.Meta.Terms) > 0 {
  1064  		ctx.Infof("Deployment under prior agreement to terms: %s",
  1065  			strings.Join(charmInfo.Meta.Terms, " "))
  1066  	}
  1067  
  1068  	ids, err := resourceadapters.DeployResources(
  1069  		applicationName,
  1070  		id,
  1071  		csMac,
  1072  		c.Resources,
  1073  		charmInfo.Meta.Resources,
  1074  		apiRoot,
  1075  	)
  1076  	if err != nil {
  1077  		return errors.Trace(err)
  1078  	}
  1079  
  1080  	if len(appConfig) == 0 {
  1081  		appConfig = nil
  1082  	}
  1083  
  1084  	args := application.DeployArgs{
  1085  		CharmID:          id,
  1086  		Cons:             c.Constraints,
  1087  		ApplicationName:  applicationName,
  1088  		Series:           series,
  1089  		NumUnits:         numUnits,
  1090  		ConfigYAML:       string(configYAML),
  1091  		Config:           appConfig,
  1092  		Placement:        c.Placement,
  1093  		Storage:          c.Storage,
  1094  		Devices:          c.Devices,
  1095  		AttachStorage:    c.AttachStorage,
  1096  		Resources:        ids,
  1097  		EndpointBindings: c.Bindings,
  1098  	}
  1099  	return errors.Trace(apiRoot.Deploy(args))
  1100  }
  1101  
  1102  const parseBindErrorPrefix = "--bind must be in the form '[<default-space>] [<endpoint-name>=<space> ...]'. "
  1103  
  1104  // parseBind parses the --bind option. Valid forms are:
  1105  // * relation-name=space-name
  1106  // * extra-binding-name=space-name
  1107  // * space-name (equivalent to binding all endpoints to the same space, i.e. application-default)
  1108  // * The above in a space separated list to specify multiple bindings,
  1109  //   e.g. "rel1=space1 ext1=space2 space3"
  1110  func (c *DeployCommand) parseBind() error {
  1111  	bindings := make(map[string]string)
  1112  	if c.BindToSpaces == "" {
  1113  		return nil
  1114  	}
  1115  
  1116  	for _, s := range strings.Split(c.BindToSpaces, " ") {
  1117  		s = strings.TrimSpace(s)
  1118  		if s == "" {
  1119  			continue
  1120  		}
  1121  
  1122  		v := strings.Split(s, "=")
  1123  		var endpoint, space string
  1124  		switch len(v) {
  1125  		case 1:
  1126  			endpoint = ""
  1127  			space = v[0]
  1128  		case 2:
  1129  			if v[0] == "" {
  1130  				return errors.New(parseBindErrorPrefix + "Found = without endpoint name. Use a lone space name to set the default.")
  1131  			}
  1132  			endpoint = v[0]
  1133  			space = v[1]
  1134  		default:
  1135  			return errors.New(parseBindErrorPrefix + "Found multiple = in binding. Did you forget to space-separate the binding list?")
  1136  		}
  1137  
  1138  		if !names.IsValidSpace(space) {
  1139  			return errors.New(parseBindErrorPrefix + "Space name invalid.")
  1140  		}
  1141  		bindings[endpoint] = space
  1142  	}
  1143  	c.Bindings = bindings
  1144  	return nil
  1145  }
  1146  
  1147  func (c *DeployCommand) Run(ctx *cmd.Context) error {
  1148  	if c.unknownModel {
  1149  		if err := c.validateStorageByModelType(); err != nil {
  1150  			return errors.Trace(err)
  1151  		}
  1152  		if err := c.validatePlacementByModelType(); err != nil {
  1153  			return errors.Trace(err)
  1154  		}
  1155  	}
  1156  	var err error
  1157  	c.Constraints, err = common.ParseConstraints(ctx, c.ConstraintsStr)
  1158  	if err != nil {
  1159  		return err
  1160  	}
  1161  	apiRoot, err := c.NewAPIRoot()
  1162  	if err != nil {
  1163  		return errors.Trace(err)
  1164  	}
  1165  	defer apiRoot.Close()
  1166  
  1167  	for _, step := range c.Steps {
  1168  		step.SetPlanURL(apiRoot.PlanURL())
  1169  	}
  1170  
  1171  	deploy, err := findDeployerFIFO(
  1172  		func() (deployFn, error) { return c.maybeReadLocalBundle(ctx) },
  1173  		func() (deployFn, error) { return c.maybeReadLocalCharm(apiRoot) },
  1174  		c.maybePredeployedLocalCharm,
  1175  		c.maybeReadCharmstoreBundleFn(apiRoot),
  1176  		c.charmStoreCharm, // This always returns a deployer
  1177  	)
  1178  	if err != nil {
  1179  		return errors.Trace(err)
  1180  	}
  1181  
  1182  	return block.ProcessBlockedError(deploy(ctx, apiRoot), block.BlockChange)
  1183  }
  1184  
  1185  func findDeployerFIFO(maybeDeployers ...func() (deployFn, error)) (deployFn, error) {
  1186  	for _, d := range maybeDeployers {
  1187  		if deploy, err := d(); err != nil {
  1188  			return nil, errors.Trace(err)
  1189  		} else if deploy != nil {
  1190  			return deploy, nil
  1191  		}
  1192  	}
  1193  	return nil, errors.NotFoundf("suitable deployer")
  1194  }
  1195  
  1196  type deployFn func(*cmd.Context, DeployAPI) error
  1197  
  1198  func (c *DeployCommand) validateBundleFlags() error {
  1199  	if flags := getFlags(c.flagSet, charmOnlyFlags()); len(flags) > 0 {
  1200  		return errors.Errorf("options provided but not supported when deploying a bundle: %s", strings.Join(flags, ", "))
  1201  	}
  1202  	return nil
  1203  }
  1204  
  1205  func (c *DeployCommand) validateCharmFlags() error {
  1206  	if flags := getFlags(c.flagSet, bundleOnlyFlags); len(flags) > 0 {
  1207  		return errors.Errorf("options provided but not supported when deploying a charm: %s", strings.Join(flags, ", "))
  1208  	}
  1209  	return nil
  1210  }
  1211  
  1212  func (c *DeployCommand) validateCharmSeries(series string) error {
  1213  	modelType, err := c.ModelType()
  1214  	if err != nil {
  1215  		return errors.Trace(err)
  1216  	}
  1217  	return model.ValidateSeries(modelType, series)
  1218  }
  1219  
  1220  func (c *DeployCommand) validateResourcesNeededForLocalDeploy(charmMeta *charm.Meta) error {
  1221  	modelType, err := c.ModelType()
  1222  	if err != nil {
  1223  		return errors.Trace(err)
  1224  	}
  1225  	if modelType != model.CAAS {
  1226  		return nil
  1227  	}
  1228  	var missingImages []string
  1229  	for resName, resMeta := range charmMeta.Resources {
  1230  		if resMeta.Type == resource.TypeContainerImage {
  1231  			if _, ok := c.Resources[resName]; !ok {
  1232  				missingImages = append(missingImages, resName)
  1233  			}
  1234  		}
  1235  	}
  1236  	if len(missingImages) > 0 {
  1237  		return errors.Errorf("local charm missing OCI images for: %v", strings.Join(missingImages, ", "))
  1238  	}
  1239  	return nil
  1240  }
  1241  
  1242  func (c *DeployCommand) maybePredeployedLocalCharm() (deployFn, error) {
  1243  	// If the charm's schema is local, we should definitively attempt
  1244  	// to deploy a charm that's already deployed in the
  1245  	// environment.
  1246  	userCharmURL, err := charm.ParseURL(c.CharmOrBundle)
  1247  	if err != nil {
  1248  		return nil, errors.Trace(err)
  1249  	} else if userCharmURL.Schema != "local" {
  1250  		logger.Debugf("cannot interpret as a redeployment of a local charm from the controller")
  1251  		return nil, nil
  1252  	}
  1253  
  1254  	// Avoid deploying charm if it's not valid for the model.
  1255  	if err := c.validateCharmSeries(userCharmURL.Series); err != nil {
  1256  		return nil, errors.Trace(err)
  1257  	}
  1258  
  1259  	return func(ctx *cmd.Context, api DeployAPI) error {
  1260  		if err := c.validateCharmFlags(); err != nil {
  1261  			return errors.Trace(err)
  1262  		}
  1263  		charmInfo, err := api.CharmInfo(userCharmURL.String())
  1264  		if err != nil {
  1265  			return err
  1266  		}
  1267  		if err := c.validateResourcesNeededForLocalDeploy(charmInfo.Meta); err != nil {
  1268  			return errors.Trace(err)
  1269  		}
  1270  		formattedCharmURL := userCharmURL.String()
  1271  		ctx.Infof("Located charm %q.", formattedCharmURL)
  1272  		ctx.Infof("Deploying charm %q.", formattedCharmURL)
  1273  		return errors.Trace(c.deployCharm(
  1274  			charmstore.CharmID{URL: userCharmURL},
  1275  			(*macaroon.Macaroon)(nil),
  1276  			userCharmURL.Series,
  1277  			ctx,
  1278  			api,
  1279  		))
  1280  	}, nil
  1281  }
  1282  
  1283  // readLocalBundle returns the bundle data and bundle dir (for
  1284  // resolving includes) for the bundleFile passed in. If the bundle
  1285  // file doesn't exist we return nil.
  1286  func readLocalBundle(ctx *cmd.Context, bundleFile string) (*charm.BundleData, string, error) {
  1287  	bundleData, err := charmrepo.ReadBundleFile(bundleFile)
  1288  	if err == nil {
  1289  		// If the bundle is defined with just a yaml file, the bundle
  1290  		// path is the directory that holds the file.
  1291  		return bundleData, filepath.Dir(ctx.AbsPath(bundleFile)), nil
  1292  	}
  1293  
  1294  	// We may have been given a local bundle archive or exploded directory.
  1295  	bundle, _, pathErr := charmrepo.NewBundleAtPath(bundleFile)
  1296  	if charmrepo.IsInvalidPathError(pathErr) {
  1297  		return nil, "", pathErr
  1298  	}
  1299  	if pathErr != nil {
  1300  		// If the bundle files existed but we couldn't read them,
  1301  		// then return that error rather than trying to interpret
  1302  		// as a charm.
  1303  		if info, statErr := os.Stat(bundleFile); statErr == nil {
  1304  			if info.IsDir() {
  1305  				if _, ok := pathErr.(*charmrepo.NotFoundError); !ok {
  1306  					return nil, "", errors.Trace(pathErr)
  1307  				}
  1308  			}
  1309  		}
  1310  
  1311  		logger.Debugf("cannot interpret as local bundle: %v", err)
  1312  		return nil, "", errors.NotValidf("local bundle %q", bundleFile)
  1313  	}
  1314  	bundleData = bundle.Data()
  1315  
  1316  	// If we get to here bundleFile is a directory, in which case
  1317  	// we should use the absolute path as the bundFilePath, or it is
  1318  	// an archive, in which case we should pass the empty string.
  1319  	var bundleDir string
  1320  	if info, err := os.Stat(bundleFile); err == nil && info.IsDir() {
  1321  		bundleDir = ctx.AbsPath(bundleFile)
  1322  	}
  1323  
  1324  	return bundleData, bundleDir, nil
  1325  }
  1326  
  1327  func (c *DeployCommand) maybeReadLocalBundle(ctx *cmd.Context) (deployFn, error) {
  1328  	bundleFile := c.CharmOrBundle
  1329  	bundleData, bundleDir, err := readLocalBundle(ctx, bundleFile)
  1330  	if charmrepo.IsInvalidPathError(err) {
  1331  		return nil, errors.Errorf(""+
  1332  			"The charm or bundle %q is ambiguous.\n"+
  1333  			"To deploy a local charm or bundle, run `juju deploy ./%[1]s`.\n"+
  1334  			"To deploy a charm or bundle from the store, run `juju deploy cs:%[1]s`.",
  1335  			bundleFile,
  1336  		)
  1337  	}
  1338  	if errors.IsNotValid(err) {
  1339  		// No problem reading it, but it's not a local bundle. Return
  1340  		// nil, nil to indicate the fallback pipeline should try the
  1341  		// next possibility.
  1342  		return nil, nil
  1343  	}
  1344  	if err != nil {
  1345  		return nil, errors.Annotate(err, "cannot deploy bundle")
  1346  	}
  1347  	if err := c.validateBundleFlags(); err != nil {
  1348  		return nil, errors.Trace(err)
  1349  	}
  1350  
  1351  	return func(ctx *cmd.Context, apiRoot DeployAPI) error {
  1352  		return errors.Trace(c.deployBundle(
  1353  			ctx,
  1354  			bundleDir,
  1355  			bundleData,
  1356  			nil,
  1357  			c.Channel,
  1358  			apiRoot,
  1359  			c.BundleStorage,
  1360  			c.BundleDevices,
  1361  		))
  1362  	}, nil
  1363  }
  1364  
  1365  func (c *DeployCommand) maybeReadLocalCharm(apiRoot DeployAPI) (deployFn, error) {
  1366  	// NOTE: Here we select the series using the algorithm defined by
  1367  	// `seriesSelector.CharmSeries`. This serves to override the algorithm found in
  1368  	// `charmrepo.NewCharmAtPath` which is outdated (but must still be
  1369  	// called since the code is coupled with path interpretation logic which
  1370  	// cannot easily be factored out).
  1371  
  1372  	// NOTE: Reading the charm here is only meant to aid in inferring the correct
  1373  	// series, if this fails we fall back to the argument series. If reading
  1374  	// the charm fails here it will also fail below (the charm is read again
  1375  	// below) where it is handled properly. This is just an expedient to get
  1376  	// the correct series. A proper refactoring of the charmrepo package is
  1377  	// needed for a more elegant fix.
  1378  
  1379  	ch, err := charm.ReadCharm(c.CharmOrBundle)
  1380  	series := c.Series
  1381  	if err == nil {
  1382  		modelCfg, err := getModelConfig(apiRoot)
  1383  		if err != nil {
  1384  			return nil, errors.Trace(err)
  1385  		}
  1386  
  1387  		seriesSelector := seriesSelector{
  1388  			seriesFlag:      series,
  1389  			supportedSeries: ch.Meta().Series,
  1390  			force:           c.Force,
  1391  			conf:            modelCfg,
  1392  			fromBundle:      false,
  1393  		}
  1394  
  1395  		series, err = seriesSelector.charmSeries()
  1396  		if err != nil {
  1397  			return nil, errors.Trace(err)
  1398  		}
  1399  	}
  1400  
  1401  	// Charm may have been supplied via a path reference.
  1402  	ch, curl, err := charmrepo.NewCharmAtPathForceSeries(c.CharmOrBundle, series, c.Force)
  1403  	// We check for several types of known error which indicate
  1404  	// that the supplied reference was indeed a path but there was
  1405  	// an issue reading the charm located there.
  1406  	if charm.IsMissingSeriesError(err) {
  1407  		return nil, err
  1408  	} else if charm.IsUnsupportedSeriesError(err) {
  1409  		return nil, errors.Trace(err)
  1410  	} else if errors.Cause(err) == zip.ErrFormat {
  1411  		return nil, errors.Errorf("invalid charm or bundle provided at %q", c.CharmOrBundle)
  1412  	} else if _, ok := err.(*charmrepo.NotFoundError); ok {
  1413  		return nil, errors.Wrap(err, errors.NotFoundf("charm or bundle at %q", c.CharmOrBundle))
  1414  	} else if err != nil && err != os.ErrNotExist {
  1415  		// If we get a "not exists" error then we attempt to interpret
  1416  		// the supplied charm reference as a URL elsewhere, otherwise
  1417  		// we return the error.
  1418  		return nil, errors.Trace(err)
  1419  	} else if err != nil {
  1420  		logger.Debugf("cannot interpret as local charm: %v", err)
  1421  		return nil, nil
  1422  	}
  1423  
  1424  	// Avoid deploying charm if it's not valid for the model.
  1425  	if err := c.validateCharmSeries(series); err != nil {
  1426  		return nil, errors.Trace(err)
  1427  	}
  1428  	if err := c.validateResourcesNeededForLocalDeploy(ch.Meta()); err != nil {
  1429  		return nil, errors.Trace(err)
  1430  	}
  1431  
  1432  	return func(ctx *cmd.Context, apiRoot DeployAPI) error {
  1433  		if err := c.validateCharmFlags(); err != nil {
  1434  			return errors.Trace(err)
  1435  		}
  1436  
  1437  		if curl, err = apiRoot.AddLocalCharm(curl, ch, c.Force); err != nil {
  1438  			return errors.Trace(err)
  1439  		}
  1440  
  1441  		id := charmstore.CharmID{
  1442  			URL: curl,
  1443  			// Local charms don't need a channel.
  1444  		}
  1445  
  1446  		ctx.Infof("Deploying charm %q.", curl.String())
  1447  		return errors.Trace(c.deployCharm(
  1448  			id,
  1449  			(*macaroon.Macaroon)(nil), // local charms don't need one.
  1450  			curl.Series,
  1451  			ctx,
  1452  			apiRoot,
  1453  		))
  1454  	}, nil
  1455  }
  1456  
  1457  // URLResolver is the part of charmrepo.Charmstore that we need to
  1458  // resolve a charm url.
  1459  type URLResolver interface {
  1460  	ResolveWithChannel(*charm.URL) (*charm.URL, params.Channel, []string, error)
  1461  }
  1462  
  1463  // resolveBundleURL tries to interpret maybeBundle as a charmstore
  1464  // bundle. If it turns out to be a bundle, the resolved URL and
  1465  // channel are returned. If it isn't but there wasn't a problem
  1466  // checking it, it returns a nil charm URL.
  1467  func resolveBundleURL(store URLResolver, maybeBundle string) (*charm.URL, params.Channel, error) {
  1468  	userRequestedURL, err := charm.ParseURL(maybeBundle)
  1469  	if err != nil {
  1470  		return nil, "", errors.Trace(err)
  1471  	}
  1472  
  1473  	// Charm or bundle has been supplied as a URL so we resolve and
  1474  	// deploy using the store.
  1475  	storeCharmOrBundleURL, channel, _, err := resolveCharm(store.ResolveWithChannel, userRequestedURL)
  1476  	if err != nil {
  1477  		return nil, "", errors.Trace(err)
  1478  	}
  1479  	if storeCharmOrBundleURL.Series != "bundle" {
  1480  		logger.Debugf(
  1481  			`cannot interpret as charmstore bundle: %v (series) != "bundle"`,
  1482  			storeCharmOrBundleURL.Series,
  1483  		)
  1484  		return nil, "", errors.NotValidf("charmstore bundle %q", maybeBundle)
  1485  	}
  1486  	return storeCharmOrBundleURL, channel, nil
  1487  }
  1488  
  1489  func (c *DeployCommand) maybeReadCharmstoreBundleFn(apiRoot DeployAPI) func() (deployFn, error) {
  1490  	return func() (deployFn, error) {
  1491  		bundleURL, channel, err := resolveBundleURL(apiRoot, c.CharmOrBundle)
  1492  		if charm.IsUnsupportedSeriesError(errors.Cause(err)) {
  1493  			return nil, errors.Errorf("%v. Use --force to deploy the charm anyway.", err)
  1494  		}
  1495  		if errors.IsNotValid(err) {
  1496  			// The URL resolved alright, but not to a bundle.
  1497  			return nil, nil
  1498  		}
  1499  		if err != nil {
  1500  			return nil, errors.Trace(err)
  1501  		}
  1502  		if err := c.validateBundleFlags(); err != nil {
  1503  			return nil, errors.Trace(err)
  1504  		}
  1505  
  1506  		return func(ctx *cmd.Context, apiRoot DeployAPI) error {
  1507  			bundle, err := apiRoot.GetBundle(bundleURL)
  1508  			if err != nil {
  1509  				return errors.Trace(err)
  1510  			}
  1511  			ctx.Infof("Located bundle %q", bundleURL)
  1512  			data := bundle.Data()
  1513  
  1514  			return errors.Trace(c.deployBundle(
  1515  				ctx,
  1516  				"", // filepath
  1517  				data,
  1518  				bundleURL,
  1519  				channel,
  1520  				apiRoot,
  1521  				c.BundleStorage,
  1522  				c.BundleDevices,
  1523  			))
  1524  		}, nil
  1525  	}
  1526  }
  1527  
  1528  func (c *DeployCommand) getMeteringAPIURL(controllerAPIRoot api.Connection) (string, error) {
  1529  	controllerAPI := controller.NewClient(controllerAPIRoot)
  1530  	controllerCfg, err := controllerAPI.ControllerConfig()
  1531  	if err != nil {
  1532  		return "", errors.Trace(err)
  1533  	}
  1534  	return controllerCfg.MeteringURL(), nil
  1535  }
  1536  
  1537  func (c *DeployCommand) charmStoreCharm() (deployFn, error) {
  1538  	userRequestedURL, err := charm.ParseURL(c.CharmOrBundle)
  1539  	if err != nil {
  1540  		return nil, errors.Trace(err)
  1541  	}
  1542  
  1543  	return func(ctx *cmd.Context, apiRoot DeployAPI) error {
  1544  		// resolver.resolve potentially updates the series of anything
  1545  		// passed in. Store this for use in seriesSelector.
  1546  		userRequestedSeries := userRequestedURL.Series
  1547  
  1548  		modelCfg, err := getModelConfig(apiRoot)
  1549  		if err != nil {
  1550  			return errors.Trace(err)
  1551  		}
  1552  
  1553  		// Charm or bundle has been supplied as a URL so we resolve and deploy using the store.
  1554  		storeCharmOrBundleURL, channel, supportedSeries, err := resolveCharm(
  1555  			apiRoot.ResolveWithChannel, userRequestedURL,
  1556  		)
  1557  		if charm.IsUnsupportedSeriesError(err) {
  1558  			return errors.Errorf("%v. Use --force to deploy the charm anyway.", err)
  1559  		} else if err != nil {
  1560  			return errors.Trace(err)
  1561  		}
  1562  
  1563  		if err := c.validateCharmFlags(); err != nil {
  1564  			return errors.Trace(err)
  1565  		}
  1566  
  1567  		selector := seriesSelector{
  1568  			charmURLSeries:  userRequestedSeries,
  1569  			seriesFlag:      c.Series,
  1570  			supportedSeries: supportedSeries,
  1571  			force:           c.Force,
  1572  			conf:            modelCfg,
  1573  			fromBundle:      false,
  1574  		}
  1575  
  1576  		// Get the series to use.
  1577  		series, err := selector.charmSeries()
  1578  
  1579  		// Avoid deploying charm if it's not valid for the model.
  1580  		// We check this first before possibly suggesting --force.
  1581  		if err == nil {
  1582  			if err2 := c.validateCharmSeries(series); err2 != nil {
  1583  				return errors.Trace(err2)
  1584  			}
  1585  		}
  1586  
  1587  		if charm.IsUnsupportedSeriesError(err) {
  1588  			return errors.Errorf("%v. Use --force to deploy the charm anyway.", err)
  1589  		}
  1590  
  1591  		// Store the charm in the controller
  1592  		curl, csMac, err := addCharmFromURL(apiRoot, storeCharmOrBundleURL, channel, c.Force)
  1593  		if err != nil {
  1594  			if termErr, ok := errors.Cause(err).(*common.TermsRequiredError); ok {
  1595  				return errors.Trace(termErr.UserErr())
  1596  			}
  1597  			return errors.Annotatef(err, "storing charm for URL %q", storeCharmOrBundleURL)
  1598  		}
  1599  
  1600  		formattedCharmURL := curl.String()
  1601  		ctx.Infof("Located charm %q.", formattedCharmURL)
  1602  		ctx.Infof("Deploying charm %q.", formattedCharmURL)
  1603  		id := charmstore.CharmID{
  1604  			URL:     curl,
  1605  			Channel: channel,
  1606  		}
  1607  		return errors.Trace(c.deployCharm(
  1608  			id,
  1609  			csMac,
  1610  			series,
  1611  			ctx,
  1612  			apiRoot,
  1613  		))
  1614  	}, nil
  1615  }
  1616  
  1617  // getFlags returns the flags with the given names. Only flags that are set and
  1618  // whose name is included in flagNames are included.
  1619  func getFlags(flagSet *gnuflag.FlagSet, flagNames []string) []string {
  1620  	flags := make([]string, 0, flagSet.NFlag())
  1621  	flagSet.Visit(func(flag *gnuflag.Flag) {
  1622  		for _, name := range flagNames {
  1623  			if flag.Name == name {
  1624  				flags = append(flags, flagWithMinus(name))
  1625  			}
  1626  		}
  1627  	})
  1628  	return flags
  1629  }
  1630  
  1631  func flagWithMinus(name string) string {
  1632  	if len(name) > 1 {
  1633  		return "--" + name
  1634  	}
  1635  	return "-" + name
  1636  }