github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/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  	"os"
     9  	"path/filepath"
    10  	"strings"
    11  
    12  	"github.com/juju/cmd"
    13  	"github.com/juju/errors"
    14  	"github.com/juju/gnuflag"
    15  	"gopkg.in/juju/charm.v6-unstable"
    16  	"gopkg.in/juju/charmrepo.v2-unstable"
    17  	"gopkg.in/juju/charmrepo.v2-unstable/csclient"
    18  	"gopkg.in/juju/charmrepo.v2-unstable/csclient/params"
    19  	"gopkg.in/juju/names.v2"
    20  	"gopkg.in/macaroon-bakery.v1/httpbakery"
    21  	"gopkg.in/macaroon.v1"
    22  
    23  	"github.com/juju/juju/api"
    24  	"github.com/juju/juju/api/annotations"
    25  	"github.com/juju/juju/api/application"
    26  	apicharms "github.com/juju/juju/api/charms"
    27  	"github.com/juju/juju/api/modelconfig"
    28  	apiparams "github.com/juju/juju/apiserver/params"
    29  	"github.com/juju/juju/charmstore"
    30  	"github.com/juju/juju/cmd/juju/block"
    31  	"github.com/juju/juju/cmd/juju/common"
    32  	"github.com/juju/juju/cmd/modelcmd"
    33  	"github.com/juju/juju/constraints"
    34  	"github.com/juju/juju/environs/config"
    35  	"github.com/juju/juju/instance"
    36  	"github.com/juju/juju/resource/resourceadapters"
    37  	"github.com/juju/juju/storage"
    38  )
    39  
    40  var planURL = "https://api.jujucharms.com/omnibus/v2"
    41  
    42  type CharmAdder interface {
    43  	AddLocalCharm(*charm.URL, charm.Charm) (*charm.URL, error)
    44  	AddCharm(*charm.URL, params.Channel) error
    45  	AddCharmWithAuthorization(*charm.URL, params.Channel, *macaroon.Macaroon) error
    46  	AuthorizeCharmstoreEntity(*charm.URL) (*macaroon.Macaroon, error)
    47  }
    48  
    49  type ApplicationAPI interface {
    50  	AddMachines(machineParams []apiparams.AddMachineParams) ([]apiparams.AddMachinesResult, error)
    51  	AddRelation(endpoints ...string) (*apiparams.AddRelationResults, error)
    52  	AddUnits(application string, numUnits int, placement []*instance.Placement) ([]string, error)
    53  	Expose(application string) error
    54  	GetCharmURL(serviceName string) (*charm.URL, error)
    55  	SetAnnotation(annotations map[string]map[string]string) ([]apiparams.ErrorResult, error)
    56  	SetCharm(application.SetCharmConfig) error
    57  	SetConstraints(application string, constraints constraints.Value) error
    58  	Update(apiparams.ApplicationUpdate) error
    59  }
    60  
    61  type ModelAPI interface {
    62  	ModelUUID() (string, bool)
    63  	ModelGet() (map[string]interface{}, error)
    64  }
    65  
    66  // MeteredDeployAPI represents the methods of the API the deploy
    67  // command needs for metered charms.
    68  type MeteredDeployAPI interface {
    69  	IsMetered(charmURL string) (bool, error)
    70  	SetMetricCredentials(service string, credentials []byte) error
    71  }
    72  
    73  // DeployAPI represents the methods of the API the deploy
    74  // command needs.
    75  type DeployAPI interface {
    76  	// TODO(katco): Pair DeployAPI down to only the methods required
    77  	// by the deploy command.
    78  	api.Connection
    79  	CharmAdder
    80  	MeteredDeployAPI
    81  	ApplicationAPI
    82  	ModelAPI
    83  
    84  	// ApplicationClient
    85  	CharmInfo(string) (*apicharms.CharmInfo, error)
    86  	Deploy(application.DeployArgs) error
    87  	Status(patterns []string) (*apiparams.FullStatus, error)
    88  
    89  	Resolve(*config.Config, *charm.URL) (*charm.URL, params.Channel, []string, error)
    90  
    91  	GetBundle(*charm.URL) (charm.Bundle, error)
    92  
    93  	WatchAll() (*api.AllWatcher, error)
    94  
    95  	// AddPendingResources(client.AddPendingResourcesArgs) (ids []string, _ error)
    96  	// DeployResources(cmd.DeployResourcesArgs) (ids []string, _ error)
    97  }
    98  
    99  // The following structs exist purely because Go cannot create a
   100  // struct with a field named the same as a method name. The DeployAPI
   101  // needs to both embed a *<package>.Client and provide the
   102  // api.Connection Client method.
   103  //
   104  // Once we pair down DeployAPI, this will not longer be a problem.
   105  
   106  type apiClient struct {
   107  	*api.Client
   108  }
   109  
   110  type charmsClient struct {
   111  	*apicharms.Client
   112  }
   113  
   114  type applicationClient struct {
   115  	*application.Client
   116  }
   117  
   118  type modelConfigClient struct {
   119  	*modelconfig.Client
   120  }
   121  
   122  type charmRepoClient struct {
   123  	*charmrepo.CharmStore
   124  }
   125  
   126  type charmstoreClient struct {
   127  	*csclient.Client
   128  }
   129  
   130  type annotationsClient struct {
   131  	*annotations.Client
   132  }
   133  
   134  func (a *charmstoreClient) AuthorizeCharmstoreEntity(url *charm.URL) (*macaroon.Macaroon, error) {
   135  	return authorizeCharmStoreEntity(a.Client, url)
   136  }
   137  
   138  type deployAPIAdapter struct {
   139  	api.Connection
   140  	*apiClient
   141  	*charmsClient
   142  	*applicationClient
   143  	*modelConfigClient
   144  	*charmRepoClient
   145  	*charmstoreClient
   146  	*annotationsClient
   147  }
   148  
   149  func (a *deployAPIAdapter) Client() *api.Client {
   150  	return a.apiClient.Client
   151  }
   152  
   153  func (a *deployAPIAdapter) ModelUUID() (string, bool) {
   154  	return a.apiClient.ModelUUID()
   155  }
   156  
   157  func (a *deployAPIAdapter) Deploy(args application.DeployArgs) error {
   158  	for i, p := range args.Placement {
   159  		if p.Scope == "model-uuid" {
   160  			p.Scope = a.applicationClient.ModelUUID()
   161  		}
   162  		args.Placement[i] = p
   163  	}
   164  
   165  	return errors.Trace(a.applicationClient.Deploy(args))
   166  }
   167  
   168  func (a *deployAPIAdapter) Resolve(cfg *config.Config, url *charm.URL) (
   169  	*charm.URL,
   170  	params.Channel,
   171  	[]string,
   172  	error,
   173  ) {
   174  	return resolveCharm(a.charmRepoClient.ResolveWithChannel, cfg, url)
   175  }
   176  
   177  func (a *deployAPIAdapter) Get(url *charm.URL) (charm.Charm, error) {
   178  	return a.charmRepoClient.Get(url)
   179  }
   180  
   181  func (a *deployAPIAdapter) SetAnnotation(annotations map[string]map[string]string) ([]apiparams.ErrorResult, error) {
   182  	return a.annotationsClient.Set(annotations)
   183  }
   184  
   185  type NewAPIRootFn func() (DeployAPI, error)
   186  
   187  func NewDefaultDeployCommand() cmd.Command {
   188  	return NewDeployCommandWithDefaultAPI([]DeployStep{
   189  		&RegisterMeteredCharm{
   190  			RegisterURL: planURL + "/plan/authorize",
   191  			QueryURL:    planURL + "/charm",
   192  		},
   193  	})
   194  }
   195  
   196  func NewDeployCommandWithDefaultAPI(steps []DeployStep) cmd.Command {
   197  	deployCmd := &DeployCommand{Steps: steps}
   198  	cmd := modelcmd.Wrap(deployCmd)
   199  	deployCmd.NewAPIRoot = func() (DeployAPI, error) {
   200  		apiRoot, err := deployCmd.ModelCommandBase.NewAPIRoot()
   201  		if err != nil {
   202  			return nil, errors.Trace(err)
   203  		}
   204  		bakeryClient, err := deployCmd.BakeryClient()
   205  		if err != nil {
   206  			return nil, errors.Trace(err)
   207  		}
   208  		cstoreClient := newCharmStoreClient(bakeryClient).WithChannel(deployCmd.Channel)
   209  
   210  		adapter := &deployAPIAdapter{
   211  			Connection:        apiRoot,
   212  			apiClient:         &apiClient{Client: apiRoot.Client()},
   213  			charmsClient:      &charmsClient{Client: apicharms.NewClient(apiRoot)},
   214  			applicationClient: &applicationClient{Client: application.NewClient(apiRoot)},
   215  			modelConfigClient: &modelConfigClient{Client: modelconfig.NewClient(apiRoot)},
   216  			charmstoreClient:  &charmstoreClient{Client: cstoreClient},
   217  			annotationsClient: &annotationsClient{Client: annotations.NewClient(apiRoot)},
   218  			charmRepoClient:   &charmRepoClient{CharmStore: charmrepo.NewCharmStoreFromClient(cstoreClient)},
   219  		}
   220  
   221  		return adapter, nil
   222  	}
   223  	return cmd
   224  }
   225  
   226  // NewDeployCommand returns a command to deploy services.
   227  func NewDeployCommand(newAPIRoot NewAPIRootFn, steps []DeployStep) cmd.Command {
   228  	return modelcmd.Wrap(&DeployCommand{
   229  		Steps:      steps,
   230  		NewAPIRoot: newAPIRoot,
   231  	})
   232  }
   233  
   234  type DeployCommand struct {
   235  	modelcmd.ModelCommandBase
   236  	UnitCommandBase
   237  
   238  	// CharmOrBundle is either a charm URL, a path where a charm can be found,
   239  	// or a bundle name.
   240  	CharmOrBundle string
   241  
   242  	// Channel holds the charmstore channel to use when obtaining
   243  	// the charm to be deployed.
   244  	Channel params.Channel
   245  
   246  	// Series is the series of the charm to deploy.
   247  	Series string
   248  
   249  	// Force is used to allow a charm to be deployed onto a machine
   250  	// running an unsupported series.
   251  	Force bool
   252  
   253  	ApplicationName string
   254  	Config          cmd.FileVar
   255  	ConstraintsStr  string
   256  	Constraints     constraints.Value
   257  	BindToSpaces    string
   258  
   259  	// TODO(axw) move this to UnitCommandBase once we support --storage
   260  	// on add-unit too.
   261  	//
   262  	// Storage is a map of storage constraints, keyed on the storage name
   263  	// defined in charm storage metadata.
   264  	Storage map[string]storage.Constraints
   265  
   266  	// BundleStorage maps application names to maps of storage constraints keyed on
   267  	// the storage name defined in that application's charm storage metadata.
   268  	BundleStorage map[string]map[string]storage.Constraints
   269  
   270  	// Resources is a map of resource name to filename to be uploaded on deploy.
   271  	Resources map[string]string
   272  
   273  	Bindings map[string]string
   274  	Steps    []DeployStep
   275  
   276  	// NewAPIRoot stores a function which returns a new API root.
   277  	NewAPIRoot NewAPIRootFn
   278  
   279  	flagSet *gnuflag.FlagSet
   280  }
   281  
   282  const deployDoc = `
   283  <charm or bundle> can be a charm/bundle URL, or an unambiguously condensed
   284  form of it; assuming a current series of "trusty", the following forms will be
   285  accepted:
   286  
   287  For cs:trusty/mysql
   288    mysql
   289    trusty/mysql
   290  
   291  For cs:~user/trusty/mysql
   292    ~user/mysql
   293  
   294  For cs:bundle/mediawiki-single
   295    mediawiki-single
   296    bundle/mediawiki-single
   297  
   298  The current series for charms is determined first by the 'default-series' model
   299  setting, followed by the preferred series for the charm in the charm store.
   300  
   301  In these cases, a versioned charm URL will be expanded as expected (for
   302  example, mysql-33 becomes cs:precise/mysql-33).
   303  
   304  Charms may also be deployed from a user specified path. In this case, the path
   305  to the charm is specified along with an optional series.
   306  
   307    juju deploy /path/to/charm --series trusty
   308  
   309  If '--series' is not specified, the charm's default series is used. The default
   310  series for a charm is the first one specified in the charm metadata. If the
   311  specified series is not supported by the charm, this results in an error,
   312  unless '--force' is used.
   313  
   314    juju deploy /path/to/charm --series wily --force
   315  
   316  Local bundles are specified with a direct path to a bundle.yaml file.
   317  For example:
   318  
   319    juju deploy /path/to/bundle/openstack/bundle.yaml
   320  
   321  If an 'application name' is not provided, the application name used is the
   322  'charm or bundle' name.
   323  
   324  Constraints can be specified by specifying the '--constraints' option. If the
   325  application is later scaled out with ` + "`juju add-unit`" + `, provisioned machines
   326  will use the same constraints (unless changed by ` + "`juju set-constraints`" + `).
   327  
   328  Resources may be uploaded by specifying the '--resource' option followed by a
   329  name=filepath pair. This option may be repeated more than once to upload more
   330  than one resource.
   331  
   332    juju deploy foo --resource bar=/some/file.tgz --resource baz=./docs/cfg.xml
   333  
   334  Where 'bar' and 'baz' are resources named in the metadata for the 'foo' charm.
   335  
   336  When using a placement directive to deploy to an existing machine or container
   337  ('--to' option), the ` + "`juju status`" + ` command should be used for guidance. A few
   338  placement directives are provider-dependent (e.g.: 'zone').
   339  
   340  In more complex scenarios, Juju's network spaces are used to partition the
   341  cloud networking layer into sets of subnets. Instances hosting units inside the
   342  same space can communicate with each other without any firewalls. Traffic
   343  crossing space boundaries could be subject to firewall and access restrictions.
   344  Using spaces as deployment targets, rather than their individual subnets,
   345  allows Juju to perform automatic distribution of units across availability zones
   346  to support high availability for applications. Spaces help isolate applications
   347  and their units, both for security purposes and to manage both traffic
   348  segregation and congestion.
   349  
   350  When deploying an application or adding machines, the 'spaces' constraint can
   351  be used to define a comma-delimited list of required and forbidden spaces (the
   352  latter prefixed with "^", similar to the 'tags' constraint).
   353  
   354  
   355  Examples:
   356      juju deploy mysql --to 23       (deploy to machine 23)
   357      juju deploy mysql --to 24/lxd/3 (deploy to lxd container 3 on machine 24)
   358      juju deploy mysql --to lxd:25   (deploy to a new lxd container on machine 25)
   359      juju deploy mysql --to lxd      (deploy to a new lxd container on a new machine)
   360  
   361      juju deploy mysql --to zone=us-east-1a
   362      (provider-dependent; deploy to a specific AZ)
   363  
   364      juju deploy mysql --to host.maas
   365      (deploy to a specific MAAS node)
   366  
   367      juju deploy mysql -n 5 --constraints mem=8G
   368      (deploy 5 units to machines with at least 8 GB of memory)
   369  
   370      juju deploy haproxy -n 2 --constraints spaces=dmz,^cms,^database
   371      (deploy 2 units to machines that are part of the 'dmz' space but not of the
   372      'cmd' or the 'database' spaces)
   373  
   374  See also:
   375      spaces
   376      constraints
   377      add-unit
   378      set-config
   379      get-config
   380      set-constraints
   381      get-constraints
   382  `
   383  
   384  // DeployStep is an action that needs to be taken during charm deployment.
   385  type DeployStep interface {
   386  
   387  	// Set flags necessary for the deploy step.
   388  	SetFlags(*gnuflag.FlagSet)
   389  
   390  	// RunPre runs before the call is made to add the charm to the environment.
   391  	RunPre(MeteredDeployAPI, *httpbakery.Client, *cmd.Context, DeploymentInfo) error
   392  
   393  	// RunPost runs after the call is made to add the charm to the environment.
   394  	// The error parameter is used to notify the step of a previously occurred error.
   395  	RunPost(MeteredDeployAPI, *httpbakery.Client, *cmd.Context, DeploymentInfo, error) error
   396  }
   397  
   398  // DeploymentInfo is used to maintain all deployment information for
   399  // deployment steps.
   400  type DeploymentInfo struct {
   401  	CharmID         charmstore.CharmID
   402  	ApplicationName string
   403  	ModelUUID       string
   404  	CharmInfo       *apicharms.CharmInfo
   405  }
   406  
   407  func (c *DeployCommand) Info() *cmd.Info {
   408  	return &cmd.Info{
   409  		Name:    "deploy",
   410  		Args:    "<charm or bundle> [<application name>]",
   411  		Purpose: "Deploy a new application or bundle.",
   412  		Doc:     deployDoc,
   413  	}
   414  }
   415  
   416  var (
   417  	// charmOnlyFlags and bundleOnlyFlags are used to validate flags based on
   418  	// whether we are deploying a charm or a bundle.
   419  	charmOnlyFlags        = []string{"bind", "config", "constraints", "force", "n", "num-units", "series", "to", "resource"}
   420  	bundleOnlyFlags       = []string{}
   421  	modelCommandBaseFlags = []string{"B", "no-browser-login"}
   422  )
   423  
   424  func (c *DeployCommand) SetFlags(f *gnuflag.FlagSet) {
   425  	// Keep above charmOnlyFlags and bundleOnlyFlags lists updated when adding
   426  	// new flags.
   427  	c.UnitCommandBase.SetFlags(f)
   428  	c.ModelCommandBase.SetFlags(f)
   429  	f.IntVar(&c.NumUnits, "n", 1, "Number of application units to deploy for principal charms")
   430  	f.StringVar((*string)(&c.Channel), "channel", "", "Channel to use when getting the charm or bundle from the charm store")
   431  	f.Var(&c.Config, "config", "Path to yaml-formatted application config")
   432  	f.StringVar(&c.ConstraintsStr, "constraints", "", "Set application constraints")
   433  	f.StringVar(&c.Series, "series", "", "The series on which to deploy")
   434  	f.BoolVar(&c.Force, "force", false, "Allow a charm to be deployed to a machine running an unsupported series")
   435  	f.Var(storageFlag{&c.Storage, &c.BundleStorage}, "storage", "Charm storage constraints")
   436  	f.Var(stringMap{&c.Resources}, "resource", "Resource to be uploaded to the controller")
   437  	f.StringVar(&c.BindToSpaces, "bind", "", "Configure application endpoint bindings to spaces")
   438  
   439  	for _, step := range c.Steps {
   440  		step.SetFlags(f)
   441  	}
   442  	c.flagSet = f
   443  }
   444  
   445  func (c *DeployCommand) Init(args []string) error {
   446  	if c.Force && c.Series == "" && c.PlacementSpec == "" {
   447  		return errors.New("--force is only used with --series")
   448  	}
   449  	switch len(args) {
   450  	case 2:
   451  		if !names.IsValidApplication(args[1]) {
   452  			return errors.Errorf("invalid application name %q", args[1])
   453  		}
   454  		c.ApplicationName = args[1]
   455  		fallthrough
   456  	case 1:
   457  		c.CharmOrBundle = args[0]
   458  	case 0:
   459  		return errors.New("no charm or bundle specified")
   460  	default:
   461  		return cmd.CheckEmpty(args[2:])
   462  	}
   463  
   464  	if err := c.parseBind(); err != nil {
   465  		return err
   466  	}
   467  	return c.UnitCommandBase.Init(args)
   468  }
   469  
   470  type ModelConfigGetter interface {
   471  	ModelGet() (map[string]interface{}, error)
   472  }
   473  
   474  var getModelConfig = func(api ModelConfigGetter) (*config.Config, error) {
   475  	// Separated into a variable for easy overrides
   476  	attrs, err := api.ModelGet()
   477  	if err != nil {
   478  		return nil, errors.Wrap(err, errors.New("cannot fetch model settings"))
   479  	}
   480  
   481  	return config.New(config.NoDefaults, attrs)
   482  }
   483  
   484  func (c *DeployCommand) deployBundle(
   485  	ctx *cmd.Context,
   486  	filePath string,
   487  	data *charm.BundleData,
   488  	channel params.Channel,
   489  	apiRoot DeployAPI,
   490  	bundleStorage map[string]map[string]storage.Constraints,
   491  ) error {
   492  	// TODO(ericsnow) Do something with the CS macaroons that were returned?
   493  	if _, err := deployBundle(
   494  		filePath,
   495  		data,
   496  		channel,
   497  		apiRoot,
   498  		ctx,
   499  		bundleStorage,
   500  	); err != nil {
   501  		return errors.Trace(err)
   502  	}
   503  	ctx.Infof("Deploy of bundle completed.")
   504  	return nil
   505  }
   506  
   507  func (c *DeployCommand) deployCharm(
   508  	id charmstore.CharmID,
   509  	csMac *macaroon.Macaroon,
   510  	series string,
   511  	ctx *cmd.Context,
   512  	apiRoot DeployAPI,
   513  ) (rErr error) {
   514  	charmInfo, err := apiRoot.CharmInfo(id.URL.String())
   515  	if err != nil {
   516  		return err
   517  	}
   518  
   519  	numUnits := c.NumUnits
   520  	if charmInfo.Meta.Subordinate {
   521  		if !constraints.IsEmpty(&c.Constraints) {
   522  			return errors.New("cannot use --constraints with subordinate application")
   523  		}
   524  		if numUnits == 1 && c.PlacementSpec == "" {
   525  			numUnits = 0
   526  		} else {
   527  			return errors.New("cannot use --num-units or --to with subordinate application")
   528  		}
   529  	}
   530  	serviceName := c.ApplicationName
   531  	if serviceName == "" {
   532  		serviceName = charmInfo.Meta.Name
   533  	}
   534  
   535  	var configYAML []byte
   536  	if c.Config.Path != "" {
   537  		configYAML, err = c.Config.Read(ctx)
   538  		if err != nil {
   539  			return errors.Trace(err)
   540  		}
   541  	}
   542  
   543  	bakeryClient, err := c.BakeryClient()
   544  	if err != nil {
   545  		return errors.Trace(err)
   546  	}
   547  
   548  	uuid, ok := apiRoot.ModelUUID()
   549  	if !ok {
   550  		return errors.New("API connection is controller-only (should never happen)")
   551  	}
   552  
   553  	deployInfo := DeploymentInfo{
   554  		CharmID:         id,
   555  		ApplicationName: serviceName,
   556  		ModelUUID:       uuid,
   557  		CharmInfo:       charmInfo,
   558  	}
   559  
   560  	for _, step := range c.Steps {
   561  		err = step.RunPre(apiRoot, bakeryClient, ctx, deployInfo)
   562  		if err != nil {
   563  			return errors.Trace(err)
   564  		}
   565  	}
   566  
   567  	defer func() {
   568  		for _, step := range c.Steps {
   569  			err = errors.Trace(step.RunPost(apiRoot, bakeryClient, ctx, deployInfo, rErr))
   570  			if err != nil {
   571  				rErr = err
   572  			}
   573  		}
   574  	}()
   575  
   576  	if id.URL != nil && id.URL.Schema != "local" && len(charmInfo.Meta.Terms) > 0 {
   577  		ctx.Infof("Deployment under prior agreement to terms: %s",
   578  			strings.Join(charmInfo.Meta.Terms, " "))
   579  	}
   580  
   581  	ids, err := resourceadapters.DeployResources(
   582  		serviceName,
   583  		id,
   584  		csMac,
   585  		c.Resources,
   586  		charmInfo.Meta.Resources,
   587  		apiRoot,
   588  	)
   589  	if err != nil {
   590  		return errors.Trace(err)
   591  	}
   592  
   593  	return errors.Trace(apiRoot.Deploy(application.DeployArgs{
   594  		CharmID:          id,
   595  		Cons:             c.Constraints,
   596  		ApplicationName:  serviceName,
   597  		Series:           series,
   598  		NumUnits:         numUnits,
   599  		ConfigYAML:       string(configYAML),
   600  		Placement:        c.Placement,
   601  		Storage:          c.Storage,
   602  		Resources:        ids,
   603  		EndpointBindings: c.Bindings,
   604  	}))
   605  }
   606  
   607  const parseBindErrorPrefix = "--bind must be in the form '[<default-space>] [<endpoint-name>=<space> ...]'. "
   608  
   609  // parseBind parses the --bind option. Valid forms are:
   610  // * relation-name=space-name
   611  // * extra-binding-name=space-name
   612  // * space-name (equivalent to binding all endpoints to the same space, i.e. application-default)
   613  // * The above in a space separated list to specify multiple bindings,
   614  //   e.g. "rel1=space1 ext1=space2 space3"
   615  func (c *DeployCommand) parseBind() error {
   616  	bindings := make(map[string]string)
   617  	if c.BindToSpaces == "" {
   618  		return nil
   619  	}
   620  
   621  	for _, s := range strings.Split(c.BindToSpaces, " ") {
   622  		s = strings.TrimSpace(s)
   623  		if s == "" {
   624  			continue
   625  		}
   626  
   627  		v := strings.Split(s, "=")
   628  		var endpoint, space string
   629  		switch len(v) {
   630  		case 1:
   631  			endpoint = ""
   632  			space = v[0]
   633  		case 2:
   634  			if v[0] == "" {
   635  				return errors.New(parseBindErrorPrefix + "Found = without endpoint name. Use a lone space name to set the default.")
   636  			}
   637  			endpoint = v[0]
   638  			space = v[1]
   639  		default:
   640  			return errors.New(parseBindErrorPrefix + "Found multiple = in binding. Did you forget to space-separate the binding list?")
   641  		}
   642  
   643  		if !names.IsValidSpace(space) {
   644  			return errors.New(parseBindErrorPrefix + "Space name invalid.")
   645  		}
   646  		bindings[endpoint] = space
   647  	}
   648  	c.Bindings = bindings
   649  	return nil
   650  }
   651  
   652  func (c *DeployCommand) Run(ctx *cmd.Context) error {
   653  	var err error
   654  	c.Constraints, err = common.ParseConstraints(ctx, c.ConstraintsStr)
   655  	if err != nil {
   656  		return err
   657  	}
   658  	apiRoot, err := c.NewAPIRoot()
   659  	if err != nil {
   660  		return errors.Trace(err)
   661  	}
   662  	defer apiRoot.Close()
   663  
   664  	deploy, err := findDeployerFIFO(
   665  		c.maybeReadLocalBundle,
   666  		c.maybeReadLocalCharm,
   667  		c.maybePredeployedLocalCharm,
   668  		c.maybeReadCharmstoreBundleFn(apiRoot),
   669  		c.charmStoreCharm, // This always returns a deployer
   670  	)
   671  	if err != nil {
   672  		return errors.Trace(err)
   673  	}
   674  
   675  	return block.ProcessBlockedError(deploy(ctx, apiRoot), block.BlockChange)
   676  }
   677  
   678  func findDeployerFIFO(maybeDeployers ...func() (deployFn, error)) (deployFn, error) {
   679  	for _, d := range maybeDeployers {
   680  		if deploy, err := d(); err != nil {
   681  			return nil, errors.Trace(err)
   682  		} else if deploy != nil {
   683  			return deploy, nil
   684  		}
   685  	}
   686  	return nil, errors.NotFoundf("suitable deployer")
   687  }
   688  
   689  type deployFn func(*cmd.Context, DeployAPI) error
   690  
   691  func (c *DeployCommand) validateBundleFlags() error {
   692  	if flags := getFlags(c.flagSet, charmOnlyFlags); len(flags) > 0 {
   693  		return errors.Errorf("Flags provided but not supported when deploying a bundle: %s.", strings.Join(flags, ", "))
   694  	}
   695  	return nil
   696  }
   697  
   698  func (c *DeployCommand) validateCharmFlags() error {
   699  	if flags := getFlags(c.flagSet, bundleOnlyFlags); len(flags) > 0 {
   700  		return errors.Errorf("Flags provided but not supported when deploying a charm: %s.", strings.Join(flags, ", "))
   701  	}
   702  	return nil
   703  }
   704  
   705  func (c *DeployCommand) maybePredeployedLocalCharm() (deployFn, error) {
   706  	// If the charm's schema is local, we should definitively attempt
   707  	// to deploy a charm that's already deployed in the
   708  	// environment.
   709  	userCharmURL, err := charm.ParseURL(c.CharmOrBundle)
   710  	if err != nil {
   711  		return nil, errors.Trace(err)
   712  	} else if userCharmURL.Schema != "local" {
   713  		logger.Debugf("cannot interpret as a redeployment of a local charm from the controller")
   714  		return nil, nil
   715  	}
   716  
   717  	return func(ctx *cmd.Context, api DeployAPI) error {
   718  		formattedCharmURL := userCharmURL.String()
   719  		ctx.Infof("Located charm %q.", formattedCharmURL)
   720  		ctx.Infof("Deploying charm %q.", formattedCharmURL)
   721  		return errors.Trace(c.deployCharm(
   722  			charmstore.CharmID{URL: userCharmURL},
   723  			(*macaroon.Macaroon)(nil),
   724  			userCharmURL.Series,
   725  			ctx,
   726  			api,
   727  		))
   728  	}, nil
   729  }
   730  
   731  func (c *DeployCommand) maybeReadLocalBundle() (deployFn, error) {
   732  	bundleFile := c.CharmOrBundle
   733  	var (
   734  		bundleFilePath                string
   735  		resolveRelativeBundleFilePath bool
   736  	)
   737  
   738  	bundleData, err := charmrepo.ReadBundleFile(bundleFile)
   739  	if err != nil {
   740  		// We may have been given a local bundle archive or exploded directory.
   741  		bundle, url, pathErr := charmrepo.NewBundleAtPath(bundleFile)
   742  		if charmrepo.IsInvalidPathError(pathErr) {
   743  			return nil, errors.Errorf(""+
   744  				"The charm or bundle %q is ambiguous.\n"+
   745  				"To deploy a local charm or bundle, run `juju deploy ./%[1]s`.\n"+
   746  				"To deploy a charm or bundle from the store, run `juju deploy cs:%[1]s`.",
   747  				c.CharmOrBundle,
   748  			)
   749  		}
   750  		if pathErr != nil {
   751  			// If the bundle files existed but we couldn't read them,
   752  			// then return that error rather than trying to interpret
   753  			// as a charm.
   754  			if info, statErr := os.Stat(c.CharmOrBundle); statErr == nil {
   755  				if info.IsDir() {
   756  					if _, ok := pathErr.(*charmrepo.NotFoundError); !ok {
   757  						return nil, pathErr
   758  					}
   759  				}
   760  			}
   761  
   762  			logger.Debugf("cannot interpret as local bundle: %v", err)
   763  			return nil, nil
   764  		}
   765  
   766  		bundleData = bundle.Data()
   767  		bundleFile = url.String()
   768  		if info, err := os.Stat(bundleFile); err == nil && info.IsDir() {
   769  			bundleFilePath = bundleFile
   770  		}
   771  	} else {
   772  		resolveRelativeBundleFilePath = true
   773  	}
   774  
   775  	if err := c.validateBundleFlags(); err != nil {
   776  		return nil, errors.Trace(err)
   777  	}
   778  
   779  	return func(ctx *cmd.Context, apiRoot DeployAPI) error {
   780  		// For local bundles, we extract the local path of the bundle
   781  		// directory.
   782  		if resolveRelativeBundleFilePath {
   783  			bundleFilePath = filepath.Dir(ctx.AbsPath(bundleFile))
   784  		}
   785  
   786  		return errors.Trace(c.deployBundle(
   787  			ctx,
   788  			bundleFilePath,
   789  			bundleData,
   790  			c.Channel,
   791  			apiRoot,
   792  			c.BundleStorage,
   793  		))
   794  	}, nil
   795  }
   796  
   797  func (c *DeployCommand) maybeReadLocalCharm() (deployFn, error) {
   798  	// Charm may have been supplied via a path reference.
   799  	ch, curl, err := charmrepo.NewCharmAtPathForceSeries(c.CharmOrBundle, c.Series, c.Force)
   800  	// We check for several types of known error which indicate
   801  	// that the supplied reference was indeed a path but there was
   802  	// an issue reading the charm located there.
   803  	if charm.IsMissingSeriesError(err) {
   804  		return nil, err
   805  	} else if charm.IsUnsupportedSeriesError(err) {
   806  		return nil, errors.Errorf("%v. Use --force to deploy the charm anyway.", err)
   807  	} else if errors.Cause(err) == zip.ErrFormat {
   808  		return nil, errors.Errorf("invalid charm or bundle provided at %q", c.CharmOrBundle)
   809  	} else if _, ok := err.(*charmrepo.NotFoundError); ok {
   810  		return nil, errors.Wrap(err, errors.NotFoundf("charm or bundle at %q", c.CharmOrBundle))
   811  	} else if err != nil && err != os.ErrNotExist {
   812  		// If we get a "not exists" error then we attempt to interpret
   813  		// the supplied charm reference as a URL elsewhere, otherwise
   814  		// we return the error.
   815  		return nil, errors.Trace(err)
   816  	} else if err != nil {
   817  		logger.Debugf("cannot interpret as local charm: %v", err)
   818  		return nil, nil
   819  	}
   820  
   821  	return func(ctx *cmd.Context, apiRoot DeployAPI) error {
   822  		if curl, err = apiRoot.AddLocalCharm(curl, ch); err != nil {
   823  			return errors.Trace(err)
   824  		}
   825  
   826  		id := charmstore.CharmID{
   827  			URL: curl,
   828  			// Local charms don't need a channel.
   829  		}
   830  
   831  		ctx.Infof("Deploying charm %q.", curl.String())
   832  		return errors.Trace(c.deployCharm(
   833  			id,
   834  			(*macaroon.Macaroon)(nil), // local charms don't need one.
   835  			curl.Series,
   836  			ctx,
   837  			apiRoot,
   838  		))
   839  	}, nil
   840  }
   841  
   842  func (c *DeployCommand) maybeReadCharmstoreBundleFn(apiRoot DeployAPI) func() (deployFn, error) {
   843  	return func() (deployFn, error) {
   844  		userRequestedURL, err := charm.ParseURL(c.CharmOrBundle)
   845  		if err != nil {
   846  			return nil, errors.Trace(err)
   847  		}
   848  
   849  		modelCfg, err := getModelConfig(apiRoot)
   850  		if err != nil {
   851  			return nil, errors.Trace(err)
   852  		}
   853  
   854  		// Charm or bundle has been supplied as a URL so we resolve and
   855  		// deploy using the store.
   856  		storeCharmOrBundleURL, channel, _, err := apiRoot.Resolve(modelCfg, userRequestedURL)
   857  		if charm.IsUnsupportedSeriesError(err) {
   858  			return nil, errors.Errorf("%v. Use --force to deploy the charm anyway.", err)
   859  		} else if err != nil {
   860  			return nil, errors.Trace(err)
   861  		} else if storeCharmOrBundleURL.Series != "bundle" {
   862  			logger.Debugf(
   863  				`cannot interpret as charmstore bundle: %v (series) != "bundle"`,
   864  				storeCharmOrBundleURL.Series,
   865  			)
   866  			return nil, nil
   867  		}
   868  
   869  		if err := c.validateBundleFlags(); err != nil {
   870  			return nil, errors.Trace(err)
   871  		}
   872  
   873  		return func(ctx *cmd.Context, apiRoot DeployAPI) error {
   874  			bundle, err := apiRoot.GetBundle(storeCharmOrBundleURL)
   875  			if err != nil {
   876  				return errors.Trace(err)
   877  			}
   878  			ctx.Infof("Located bundle %q", storeCharmOrBundleURL)
   879  			data := bundle.Data()
   880  
   881  			return errors.Trace(c.deployBundle(
   882  				ctx,
   883  				"", // filepath
   884  				data,
   885  				channel,
   886  				apiRoot,
   887  				c.BundleStorage,
   888  			))
   889  		}, nil
   890  	}
   891  }
   892  
   893  func (c *DeployCommand) charmStoreCharm() (deployFn, error) {
   894  	userRequestedURL, err := charm.ParseURL(c.CharmOrBundle)
   895  	if err != nil {
   896  		return nil, errors.Trace(err)
   897  	}
   898  
   899  	return func(ctx *cmd.Context, apiRoot DeployAPI) error {
   900  		// resolver.resolve potentially updates the series of anything
   901  		// passed in. Store this for use in seriesSelector.
   902  		userRequestedSeries := userRequestedURL.Series
   903  
   904  		modelCfg, err := getModelConfig(apiRoot)
   905  		if err != nil {
   906  			return errors.Trace(err)
   907  		}
   908  
   909  		// Charm or bundle has been supplied as a URL so we resolve and deploy using the store.
   910  		storeCharmOrBundleURL, channel, supportedSeries, err := apiRoot.Resolve(modelCfg, userRequestedURL)
   911  		if charm.IsUnsupportedSeriesError(err) {
   912  			return errors.Errorf("%v. Use --force to deploy the charm anyway.", err)
   913  		} else if err != nil {
   914  			return errors.Trace(err)
   915  		}
   916  
   917  		if err := c.validateCharmFlags(); err != nil {
   918  			return errors.Trace(err)
   919  		}
   920  
   921  		selector := seriesSelector{
   922  			charmURLSeries:  userRequestedSeries,
   923  			seriesFlag:      c.Series,
   924  			supportedSeries: supportedSeries,
   925  			force:           c.Force,
   926  			conf:            modelCfg,
   927  			fromBundle:      false,
   928  		}
   929  
   930  		// Get the series to use.
   931  		series, err := selector.charmSeries()
   932  		if charm.IsUnsupportedSeriesError(err) {
   933  			return errors.Errorf("%v. Use --force to deploy the charm anyway.", err)
   934  		}
   935  
   936  		// Store the charm in the controller
   937  		curl, csMac, err := addCharmFromURL(apiRoot, storeCharmOrBundleURL, channel)
   938  		if err != nil {
   939  			if err1, ok := errors.Cause(err).(*termsRequiredError); ok {
   940  				terms := strings.Join(err1.Terms, " ")
   941  				return errors.Errorf(`Declined: please agree to the following terms %s. Try: "juju agree %s"`, terms, terms)
   942  			}
   943  			return errors.Annotatef(err, "storing charm for URL %q", storeCharmOrBundleURL)
   944  		}
   945  
   946  		formattedCharmURL := curl.String()
   947  		ctx.Infof("Located charm %q.", formattedCharmURL)
   948  		ctx.Infof("Deploying charm %q.", formattedCharmURL)
   949  		id := charmstore.CharmID{
   950  			URL:     curl,
   951  			Channel: channel,
   952  		}
   953  		return errors.Trace(c.deployCharm(
   954  			id,
   955  			csMac,
   956  			series,
   957  			ctx,
   958  			apiRoot,
   959  		))
   960  	}, nil
   961  }
   962  
   963  // getFlags returns the flags with the given names. Only flags that are set and
   964  // whose name is included in flagNames are included.
   965  func getFlags(flagSet *gnuflag.FlagSet, flagNames []string) []string {
   966  	flags := make([]string, 0, flagSet.NFlag())
   967  	flagSet.Visit(func(flag *gnuflag.Flag) {
   968  		for _, name := range flagNames {
   969  			if flag.Name == name {
   970  				flags = append(flags, flagWithMinus(name))
   971  			}
   972  		}
   973  	})
   974  	return flags
   975  }
   976  
   977  func flagWithMinus(name string) string {
   978  	if len(name) > 1 {
   979  		return "--" + name
   980  	}
   981  	return "-" + name
   982  }