github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/cmd/juju/service/deploy.go (about)

     1  // Copyright 2012, 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package service
     5  
     6  import (
     7  	"archive/zip"
     8  	"fmt"
     9  	"os"
    10  	"path/filepath"
    11  	"strings"
    12  
    13  	"github.com/juju/cmd"
    14  	"github.com/juju/errors"
    15  	"github.com/juju/names"
    16  	"gopkg.in/juju/charm.v6-unstable"
    17  	charmresource "gopkg.in/juju/charm.v6-unstable/resource"
    18  	"gopkg.in/juju/charmrepo.v2-unstable"
    19  	csclientparams "gopkg.in/juju/charmrepo.v2-unstable/csclient/params"
    20  	"gopkg.in/macaroon-bakery.v1/httpbakery"
    21  	"gopkg.in/macaroon.v1"
    22  	"launchpad.net/gnuflag"
    23  
    24  	"github.com/juju/juju/api"
    25  	apiannotations "github.com/juju/juju/api/annotations"
    26  	apiservice "github.com/juju/juju/api/service"
    27  	"github.com/juju/juju/charmstore"
    28  	"github.com/juju/juju/cmd/juju/block"
    29  	"github.com/juju/juju/cmd/modelcmd"
    30  	"github.com/juju/juju/constraints"
    31  	"github.com/juju/juju/environs/config"
    32  	"github.com/juju/juju/instance"
    33  	"github.com/juju/juju/resource/resourceadapters"
    34  	"github.com/juju/juju/storage"
    35  )
    36  
    37  var planURL = "https://api.jujucharms.com/omnibus/v2"
    38  
    39  // NewDeployCommand returns a command to deploy services.
    40  func NewDeployCommand() cmd.Command {
    41  	return modelcmd.Wrap(&DeployCommand{
    42  		Steps: []DeployStep{
    43  			&RegisterMeteredCharm{
    44  				RegisterURL: planURL + "/plan/authorize",
    45  				QueryURL:    planURL + "/charm",
    46  			},
    47  		}})
    48  }
    49  
    50  type DeployCommand struct {
    51  	modelcmd.ModelCommandBase
    52  	UnitCommandBase
    53  	// CharmOrBundle is either a charm URL, a path where a charm can be found,
    54  	// or a bundle name.
    55  	CharmOrBundle string
    56  
    57  	// Channel holds the charmstore channel to use when obtaining
    58  	// the charm to be deployed.
    59  	Channel csclientparams.Channel
    60  
    61  	Series string
    62  
    63  	// Force is used to allow a charm to be deployed onto a machine
    64  	// running an unsupported series.
    65  	Force bool
    66  
    67  	ServiceName  string
    68  	Config       cmd.FileVar
    69  	Constraints  constraints.Value
    70  	BindToSpaces string
    71  
    72  	// TODO(axw) move this to UnitCommandBase once we support --storage
    73  	// on add-unit too.
    74  	//
    75  	// Storage is a map of storage constraints, keyed on the storage name
    76  	// defined in charm storage metadata.
    77  	Storage map[string]storage.Constraints
    78  
    79  	// BundleStorage maps service names to maps of storage constraints keyed on
    80  	// the storage name defined in that service's charm storage metadata.
    81  	BundleStorage map[string]map[string]storage.Constraints
    82  
    83  	// Resources is a map of resource name to filename to be uploaded on deploy.
    84  	Resources map[string]string
    85  
    86  	Bindings map[string]string
    87  	Steps    []DeployStep
    88  
    89  	flagSet *gnuflag.FlagSet
    90  }
    91  
    92  const deployDoc = `
    93  <charm or bundle> can be a charm/bundle URL, or an unambiguously condensed
    94  form of it; assuming a current series of "trusty", the following forms will be
    95  accepted:
    96  
    97  For cs:trusty/mysql
    98    mysql
    99    trusty/mysql
   100  
   101  For cs:~user/trusty/mysql
   102    cs:~user/mysql
   103  
   104  For cs:bundle/mediawiki-single
   105    mediawiki-single
   106    bundle/mediawiki-single
   107  
   108  The current series for charms is determined first by the default-series model
   109  setting, followed by the preferred series for the charm in the charm store.
   110  
   111  In these cases, a versioned charm URL will be expanded as expected (for example,
   112  mysql-33 becomes cs:precise/mysql-33).
   113  
   114  Charms may also be deployed from a user specified path. In this case, the
   115  path to the charm is specified along with an optional series.
   116  
   117     juju deploy /path/to/charm --series trusty
   118  
   119  If series is not specified, the charm's default series is used. The default series
   120  for a charm is the first one specified in the charm metadata. If the specified series
   121  is not supported by the charm, this results in an error, unless --force is used.
   122  
   123     juju deploy /path/to/charm --series wily --force
   124  
   125  Local bundles are specified with a direct path to a bundle.yaml file.
   126  For example:
   127  
   128    juju deploy /path/to/bundle/openstack/bundle.yaml
   129  
   130  <service name>, if omitted, will be derived from <charm name>.
   131  
   132  Constraints can be specified when using deploy by specifying the --constraints
   133  flag.  When used with deploy, service-specific constraints are set so that later
   134  machines provisioned with add-unit will use the same constraints (unless changed
   135  by set-constraints).
   136  
   137  Resources may be uploaded at deploy time by specifying the --resource flag.
   138  Following the resource flag should be name=filepath pair.  This flag may be
   139  repeated more than once to upload more than one resource.
   140  
   141    juju deploy foo --resource bar=/some/file.tgz --resource baz=./docs/cfg.xml
   142  
   143  Where bar and baz are resources named in the metadata for the foo charm.
   144  
   145  Charms can be deployed to a specific machine using the --to argument.
   146  If the destination is an LXC container the default is to use lxc-clone
   147  to create the container where possible. For Ubuntu deployments, lxc-clone
   148  is supported for the trusty OS series and later. A 'template' container is
   149  created with the name
   150    juju-<series>-template
   151  where <series> is the OS series, for example 'juju-trusty-template'.
   152  
   153  You can override the use of clone by changing the provider configuration:
   154    lxc-clone: false
   155  
   156  In more complex scenarios, Juju's network spaces are used to partition the cloud
   157  networking layer into sets of subnets. Instances hosting units inside the
   158  same space can communicate with each other without any firewalls. Traffic
   159  crossing space boundaries could be subject to firewall and access restrictions.
   160  Using spaces as deployment targets, rather than their individual subnets allows
   161  Juju to perform automatic distribution of units across availability zones to
   162  support high availability for services. Spaces help isolate services and their
   163  units, both for security purposes and to manage both traffic segregation and
   164  congestion.
   165  
   166  When deploying a service or adding machines, the "spaces" constraint can be
   167  used to define a comma-delimited list of required and forbidden spaces
   168  (the latter prefixed with "^", similar to the "tags" constraint).
   169  
   170  If you have the main container directory mounted on a btrfs partition,
   171  then the clone will be using btrfs snapshots to create the containers.
   172  This means that clones use up much less disk space.  If you do not have btrfs,
   173  lxc will attempt to use aufs (an overlay type filesystem). You can
   174  explicitly ask Juju to create full containers and not overlays by specifying
   175  the following in the provider configuration:
   176    lxc-clone-aufs: false
   177  
   178  Examples:
   179     juju deploy mysql --to 23       (deploy to machine 23)
   180     juju deploy mysql --to 24/lxc/3 (deploy to lxc container 3 on host machine 24)
   181     juju deploy mysql --to lxc:25   (deploy to a new lxc container on host machine 25)
   182  
   183     juju deploy mysql -n 5 --constraints mem=8G
   184     (deploy 5 instances of mysql with at least 8 GB of RAM each)
   185  
   186     juju deploy haproxy -n 2 --constraints spaces=dmz,^cms,^database
   187     (deploy 2 instances of haproxy on cloud instances being part of the dmz
   188      space but not of the cmd and the database space)
   189  
   190  See Also:
   191     juju help spaces
   192     juju help constraints
   193     juju help set-constraints
   194     juju help get-constraints
   195  `
   196  
   197  // DeployStep is an action that needs to be taken during charm deployment.
   198  type DeployStep interface {
   199  	// Set flags necessary for the deploy step.
   200  	SetFlags(*gnuflag.FlagSet)
   201  	// RunPre runs before the call is made to add the charm to the environment.
   202  	RunPre(api.Connection, *httpbakery.Client, *cmd.Context, DeploymentInfo) error
   203  	// RunPost runs after the call is made to add the charm to the environment.
   204  	// The error parameter is used to notify the step of a previously occurred error.
   205  	RunPost(api.Connection, *httpbakery.Client, *cmd.Context, DeploymentInfo, error) error
   206  }
   207  
   208  // DeploymentInfo is used to maintain all deployment information for
   209  // deployment steps.
   210  type DeploymentInfo struct {
   211  	CharmID     charmstore.CharmID
   212  	ServiceName string
   213  	ModelUUID   string
   214  }
   215  
   216  func (c *DeployCommand) Info() *cmd.Info {
   217  	return &cmd.Info{
   218  		Name:    "deploy",
   219  		Args:    "<charm or bundle> [<service name>]",
   220  		Purpose: "deploy a new service or bundle",
   221  		Doc:     deployDoc,
   222  	}
   223  }
   224  
   225  var (
   226  	// charmOnlyFlags and bundleOnlyFlags are used to validate flags based on
   227  	// whether we are deploying a charm or a bundle.
   228  	charmOnlyFlags  = []string{"bind", "config", "constraints", "force", "n", "num-units", "series", "to", "resource"}
   229  	bundleOnlyFlags = []string{}
   230  )
   231  
   232  func (c *DeployCommand) SetFlags(f *gnuflag.FlagSet) {
   233  	// Keep above charmOnlyFlags and bundleOnlyFlags lists updated when adding
   234  	// new flags.
   235  	c.UnitCommandBase.SetFlags(f)
   236  	f.IntVar(&c.NumUnits, "n", 1, "number of service units to deploy for principal charms")
   237  	f.StringVar((*string)(&c.Channel), "channel", "", "channel to use when getting the charm or bundle from the charm store")
   238  	f.Var(&c.Config, "config", "path to yaml-formatted service config")
   239  	f.Var(constraints.ConstraintsValue{Target: &c.Constraints}, "constraints", "set service constraints")
   240  	f.StringVar(&c.Series, "series", "", "the series on which to deploy")
   241  	f.BoolVar(&c.Force, "force", false, "allow a charm to be deployed to a machine running an unsupported series")
   242  	f.Var(storageFlag{&c.Storage, &c.BundleStorage}, "storage", "charm storage constraints")
   243  	f.Var(stringMap{&c.Resources}, "resource", "resource to be uploaded to the controller")
   244  	f.StringVar(&c.BindToSpaces, "bind", "", "Configure service endpoint bindings to spaces")
   245  
   246  	for _, step := range c.Steps {
   247  		step.SetFlags(f)
   248  	}
   249  	c.flagSet = f
   250  }
   251  
   252  func (c *DeployCommand) Init(args []string) error {
   253  	if c.Force && c.Series == "" && c.PlacementSpec == "" {
   254  		return errors.New("--force is only used with --series")
   255  	}
   256  	switch len(args) {
   257  	case 2:
   258  		if !names.IsValidService(args[1]) {
   259  			return fmt.Errorf("invalid service name %q", args[1])
   260  		}
   261  		c.ServiceName = args[1]
   262  		fallthrough
   263  	case 1:
   264  		c.CharmOrBundle = args[0]
   265  	case 0:
   266  		return errors.New("no charm or bundle specified")
   267  	default:
   268  		return cmd.CheckEmpty(args[2:])
   269  	}
   270  	err := c.parseBind()
   271  	if err != nil {
   272  		return err
   273  	}
   274  	return c.UnitCommandBase.Init(args)
   275  }
   276  
   277  type ModelConfigGetter interface {
   278  	ModelGet() (map[string]interface{}, error)
   279  }
   280  
   281  var getClientConfig = func(client ModelConfigGetter) (*config.Config, error) {
   282  	// Separated into a variable for easy overrides
   283  	attrs, err := client.ModelGet()
   284  	if err != nil {
   285  		return nil, err
   286  	}
   287  
   288  	return config.New(config.NoDefaults, attrs)
   289  }
   290  
   291  func (c *DeployCommand) maybeReadLocalBundleData(ctx *cmd.Context) (
   292  	_ *charm.BundleData, bundleFile string, bundleFilePath string, _ error,
   293  ) {
   294  	bundleFile = c.CharmOrBundle
   295  	bundleData, err := charmrepo.ReadBundleFile(bundleFile)
   296  	if err == nil {
   297  		// For local bundles, we extract the local path of
   298  		// the bundle directory.
   299  		bundleFilePath = filepath.Dir(ctx.AbsPath(bundleFile))
   300  	} else {
   301  		// We may have been given a local bundle archive or exploded directory.
   302  		if bundle, burl, pathErr := charmrepo.NewBundleAtPath(bundleFile); pathErr == nil {
   303  			bundleData = bundle.Data()
   304  			bundleFile = burl.String()
   305  			if info, err := os.Stat(bundleFile); err == nil && info.IsDir() {
   306  				bundleFilePath = bundleFile
   307  			}
   308  			err = nil
   309  		} else {
   310  			err = pathErr
   311  		}
   312  	}
   313  	return bundleData, bundleFile, bundleFilePath, err
   314  }
   315  
   316  func (c *DeployCommand) deployCharmOrBundle(ctx *cmd.Context, client *api.Client) error {
   317  	deployer := serviceDeployer{ctx, c}
   318  
   319  	// We may have been given a local bundle file.
   320  	bundleData, bundleIdent, bundleFilePath, err := c.maybeReadLocalBundleData(ctx)
   321  	// If the bundle files existed but we couldn't read them, then
   322  	// return that error rather than trying to interpret as a charm.
   323  	if err != nil {
   324  		if info, statErr := os.Stat(c.CharmOrBundle); statErr == nil {
   325  			if info.IsDir() {
   326  				if _, ok := err.(*charmrepo.NotFoundError); !ok {
   327  					return err
   328  				}
   329  			}
   330  		}
   331  	}
   332  
   333  	// If not a bundle then maybe a local charm.
   334  	if err != nil {
   335  		// Charm may have been supplied via a path reference.
   336  		ch, curl, charmErr := charmrepo.NewCharmAtPathForceSeries(c.CharmOrBundle, c.Series, c.Force)
   337  		if charmErr == nil {
   338  			if curl, charmErr = client.AddLocalCharm(curl, ch); charmErr != nil {
   339  				return charmErr
   340  			}
   341  			id := charmstore.CharmID{
   342  				URL: curl,
   343  				// Local charms don't need a channel.
   344  			}
   345  			var csMac *macaroon.Macaroon // local charms don't need one.
   346  			return c.deployCharm(deployCharmArgs{
   347  				id:       id,
   348  				csMac:    csMac,
   349  				series:   curl.Series,
   350  				ctx:      ctx,
   351  				client:   client,
   352  				deployer: &deployer,
   353  			})
   354  		}
   355  		// We check for several types of known error which indicate
   356  		// that the supplied reference was indeed a path but there was
   357  		// an issue reading the charm located there.
   358  		if charm.IsMissingSeriesError(charmErr) {
   359  			return charmErr
   360  		}
   361  		if charm.IsUnsupportedSeriesError(charmErr) {
   362  			return errors.Errorf("%v. Use --force to deploy the charm anyway.", charmErr)
   363  		}
   364  		if errors.Cause(charmErr) == zip.ErrFormat {
   365  			return errors.Errorf("invalid charm or bundle provided at %q", c.CharmOrBundle)
   366  		}
   367  		err = charmErr
   368  	}
   369  	if _, ok := err.(*charmrepo.NotFoundError); ok {
   370  		return errors.Errorf("no charm or bundle found at %q", c.CharmOrBundle)
   371  	}
   372  	// If we get a "not exists" error then we attempt to interpret the supplied
   373  	// charm or bundle reference as a URL below, otherwise we return the error.
   374  	if err != nil && err != os.ErrNotExist {
   375  		return err
   376  	}
   377  
   378  	conf, err := getClientConfig(client)
   379  	if err != nil {
   380  		return err
   381  	}
   382  
   383  	bakeryClient, err := c.BakeryClient()
   384  	if err != nil {
   385  		return errors.Trace(err)
   386  	}
   387  	csClient := newCharmStoreClient(bakeryClient).WithChannel(c.Channel)
   388  
   389  	resolver := newCharmURLResolver(conf, csClient)
   390  
   391  	var storeCharmOrBundleURL *charm.URL
   392  	var store *charmrepo.CharmStore
   393  	var supportedSeries []string
   394  	// If we don't already have a bundle loaded, we try the charm store for a charm or bundle.
   395  	if bundleData == nil {
   396  		// Charm or bundle has been supplied as a URL so we resolve and deploy using the store.
   397  		storeCharmOrBundleURL, c.Channel, supportedSeries, store, err = resolver.resolve(c.CharmOrBundle)
   398  		if charm.IsUnsupportedSeriesError(err) {
   399  			return errors.Errorf("%v. Use --force to deploy the charm anyway.", err)
   400  		}
   401  		if err != nil {
   402  			return errors.Trace(err)
   403  		}
   404  		if storeCharmOrBundleURL.Series == "bundle" {
   405  			// Load the bundle entity.
   406  			bundle, err := store.GetBundle(storeCharmOrBundleURL)
   407  			if err != nil {
   408  				return errors.Trace(err)
   409  			}
   410  			bundleData = bundle.Data()
   411  			bundleIdent = storeCharmOrBundleURL.String()
   412  		}
   413  	}
   414  	// Handle a bundle.
   415  	if bundleData != nil {
   416  		if flags := getFlags(c.flagSet, charmOnlyFlags); len(flags) > 0 {
   417  			return errors.Errorf("Flags provided but not supported when deploying a bundle: %s.", strings.Join(flags, ", "))
   418  		}
   419  		// TODO(ericsnow) Do something with the CS macaroons that were returned?
   420  		if _, err := deployBundle(
   421  			bundleFilePath, bundleData, c.Channel, client, &deployer, resolver, ctx, c.BundleStorage,
   422  		); err != nil {
   423  			return errors.Trace(err)
   424  		}
   425  		ctx.Infof("deployment of bundle %q completed", bundleIdent)
   426  		return nil
   427  	}
   428  	// Handle a charm.
   429  	if flags := getFlags(c.flagSet, bundleOnlyFlags); len(flags) > 0 {
   430  		return errors.Errorf("Flags provided but not supported when deploying a charm: %s.", strings.Join(flags, ", "))
   431  	}
   432  	// Get the series to use.
   433  	series, message, err := charmSeries(c.Series, storeCharmOrBundleURL.Series, supportedSeries, c.Force, conf, deployFromCharm)
   434  	if charm.IsUnsupportedSeriesError(err) {
   435  		return errors.Errorf("%v. Use --force to deploy the charm anyway.", err)
   436  	}
   437  	// Store the charm in state.
   438  	curl, csMac, err := addCharmFromURL(client, storeCharmOrBundleURL, c.Channel, csClient)
   439  	if err != nil {
   440  		if err1, ok := errors.Cause(err).(*termsRequiredError); ok {
   441  			terms := strings.Join(err1.Terms, " ")
   442  			return errors.Errorf(`Declined: please agree to the following terms %s. Try: "juju agree %s"`, terms, terms)
   443  		}
   444  		return errors.Annotatef(err, "storing charm for URL %q", storeCharmOrBundleURL)
   445  	}
   446  	ctx.Infof("Added charm %q to the model.", curl)
   447  	ctx.Infof("Deploying charm %q %v.", curl, fmt.Sprintf(message, series))
   448  	id := charmstore.CharmID{
   449  		URL:     curl,
   450  		Channel: c.Channel,
   451  	}
   452  	return c.deployCharm(deployCharmArgs{
   453  		id:       id,
   454  		csMac:    csMac,
   455  		series:   series,
   456  		ctx:      ctx,
   457  		client:   client,
   458  		deployer: &deployer,
   459  	})
   460  }
   461  
   462  const (
   463  	msgUserRequestedSeries = "with the user specified series %q"
   464  	msgBundleSeries        = "with the series %q defined by the bundle"
   465  	msgSingleCharmSeries   = "with the charm series %q"
   466  	msgDefaultCharmSeries  = "with the default charm metadata series %q"
   467  	msgDefaultModelSeries  = "with the configured model default series %q"
   468  	msgLatestLTSSeries     = "with the latest LTS series %q"
   469  )
   470  
   471  const (
   472  	// deployFromBundle is passed to charmSeries when deploying from a bundle.
   473  	deployFromBundle = true
   474  
   475  	// deployFromCharm is passed to charmSeries when deploying a charm.
   476  	deployFromCharm = false
   477  )
   478  
   479  // charmSeries determine what series to use with a charm.
   480  // Order of preference is:
   481  // - user requested or defined by bundle when deploying
   482  // - default from charm metadata supported series
   483  // - model default
   484  // - charm store default
   485  func charmSeries(
   486  	requestedSeries, seriesFromCharm string,
   487  	supportedSeries []string,
   488  	force bool,
   489  	conf *config.Config,
   490  	fromBundle bool,
   491  ) (string, string, error) {
   492  	// User has requested a series and we have a new charm with series in metadata.
   493  	if requestedSeries != "" && seriesFromCharm == "" {
   494  		if !force && !isSeriesSupported(requestedSeries, supportedSeries) {
   495  			return "", "", charm.NewUnsupportedSeriesError(requestedSeries, supportedSeries)
   496  		}
   497  		if fromBundle {
   498  			return requestedSeries, msgBundleSeries, nil
   499  		} else {
   500  			return requestedSeries, msgUserRequestedSeries, nil
   501  		}
   502  	}
   503  
   504  	// User has requested a series and it's an old charm for a single series.
   505  	if seriesFromCharm != "" {
   506  		if !force && requestedSeries != "" && requestedSeries != seriesFromCharm {
   507  			return "", "", charm.NewUnsupportedSeriesError(requestedSeries, []string{seriesFromCharm})
   508  		}
   509  		if requestedSeries != "" {
   510  			if fromBundle {
   511  				return requestedSeries, msgBundleSeries, nil
   512  			} else {
   513  				return requestedSeries, msgUserRequestedSeries, nil
   514  			}
   515  		}
   516  		return seriesFromCharm, msgSingleCharmSeries, nil
   517  	}
   518  
   519  	// Use charm default.
   520  	if len(supportedSeries) > 0 {
   521  		return supportedSeries[0], msgDefaultCharmSeries, nil
   522  	}
   523  
   524  	// Use model default supported series.
   525  	if defaultSeries, ok := conf.DefaultSeries(); ok {
   526  		if !force && !isSeriesSupported(defaultSeries, supportedSeries) {
   527  			return "", "", charm.NewUnsupportedSeriesError(defaultSeries, supportedSeries)
   528  		}
   529  		return defaultSeries, msgDefaultModelSeries, nil
   530  	}
   531  
   532  	// Use latest LTS.
   533  	latestLtsSeries := config.LatestLtsSeries()
   534  	if !force && !isSeriesSupported(latestLtsSeries, supportedSeries) {
   535  		return "", "", charm.NewUnsupportedSeriesError(latestLtsSeries, supportedSeries)
   536  	}
   537  	return latestLtsSeries, msgLatestLTSSeries, nil
   538  }
   539  
   540  type deployCharmArgs struct {
   541  	id       charmstore.CharmID
   542  	csMac    *macaroon.Macaroon
   543  	series   string
   544  	ctx      *cmd.Context
   545  	client   *api.Client
   546  	deployer *serviceDeployer
   547  }
   548  
   549  func (c *DeployCommand) deployCharm(args deployCharmArgs) (rErr error) {
   550  	charmInfo, err := args.client.CharmInfo(args.id.URL.String())
   551  	if err != nil {
   552  		return err
   553  	}
   554  
   555  	numUnits := c.NumUnits
   556  	if charmInfo.Meta.Subordinate {
   557  		if !constraints.IsEmpty(&c.Constraints) {
   558  			return errors.New("cannot use --constraints with subordinate service")
   559  		}
   560  		if numUnits == 1 && c.PlacementSpec == "" {
   561  			numUnits = 0
   562  		} else {
   563  			return errors.New("cannot use --num-units or --to with subordinate service")
   564  		}
   565  	}
   566  	serviceName := c.ServiceName
   567  	if serviceName == "" {
   568  		serviceName = charmInfo.Meta.Name
   569  	}
   570  
   571  	var configYAML []byte
   572  	if c.Config.Path != "" {
   573  		configYAML, err = c.Config.Read(args.ctx)
   574  		if err != nil {
   575  			return err
   576  		}
   577  	}
   578  
   579  	state, err := c.NewAPIRoot()
   580  	if err != nil {
   581  		return errors.Trace(err)
   582  	}
   583  	bakeryClient, err := c.BakeryClient()
   584  	if err != nil {
   585  		return errors.Trace(err)
   586  	}
   587  
   588  	deployInfo := DeploymentInfo{
   589  		CharmID:     args.id,
   590  		ServiceName: serviceName,
   591  		ModelUUID:   args.client.ModelUUID(),
   592  	}
   593  
   594  	for _, step := range c.Steps {
   595  		err = step.RunPre(state, bakeryClient, args.ctx, deployInfo)
   596  		if err != nil {
   597  			return err
   598  		}
   599  	}
   600  
   601  	defer func() {
   602  		for _, step := range c.Steps {
   603  			err = step.RunPost(state, bakeryClient, args.ctx, deployInfo, rErr)
   604  			if err != nil {
   605  				rErr = err
   606  			}
   607  		}
   608  	}()
   609  
   610  	if args.id.URL != nil && args.id.URL.Schema != "local" && len(charmInfo.Meta.Terms) > 0 {
   611  		args.ctx.Infof("Deployment under prior agreement to terms: %s",
   612  			strings.Join(charmInfo.Meta.Terms, " "))
   613  	}
   614  
   615  	ids, err := handleResources(c, c.Resources, serviceName, args.id, args.csMac, charmInfo.Meta.Resources)
   616  	if err != nil {
   617  		return errors.Trace(err)
   618  	}
   619  
   620  	params := serviceDeployParams{
   621  		charmID:       args.id,
   622  		serviceName:   serviceName,
   623  		series:        args.series,
   624  		numUnits:      numUnits,
   625  		configYAML:    string(configYAML),
   626  		constraints:   c.Constraints,
   627  		placement:     c.Placement,
   628  		storage:       c.Storage,
   629  		spaceBindings: c.Bindings,
   630  		resources:     ids,
   631  	}
   632  	return args.deployer.serviceDeploy(params)
   633  }
   634  
   635  type APICmd interface {
   636  	NewAPIRoot() (api.Connection, error)
   637  }
   638  
   639  func handleResources(c APICmd, resources map[string]string, serviceName string, chID charmstore.CharmID, csMac *macaroon.Macaroon, metaResources map[string]charmresource.Meta) (map[string]string, error) {
   640  	if len(resources) == 0 && len(metaResources) == 0 {
   641  		return nil, nil
   642  	}
   643  
   644  	api, err := c.NewAPIRoot()
   645  	if err != nil {
   646  		return nil, errors.Trace(err)
   647  	}
   648  
   649  	ids, err := resourceadapters.DeployResources(serviceName, chID, csMac, resources, metaResources, api)
   650  	if err != nil {
   651  		return nil, errors.Trace(err)
   652  	}
   653  
   654  	return ids, nil
   655  }
   656  
   657  const parseBindErrorPrefix = "--bind must be in the form '[<default-space>] [<endpoint-name>=<space> ...]'. "
   658  
   659  // parseBind parses the --bind option. Valid forms are:
   660  // * relation-name=space-name
   661  // * extra-binding-name=space-name
   662  // * space-name (equivalent to binding all endpoints to the same space, i.e. service-default)
   663  // * The above in a space separated list to specify multiple bindings,
   664  //   e.g. "rel1=space1 ext1=space2 space3"
   665  func (c *DeployCommand) parseBind() error {
   666  	bindings := make(map[string]string)
   667  	if c.BindToSpaces == "" {
   668  		return nil
   669  	}
   670  
   671  	for _, s := range strings.Split(c.BindToSpaces, " ") {
   672  		s = strings.TrimSpace(s)
   673  		if s == "" {
   674  			continue
   675  		}
   676  
   677  		v := strings.Split(s, "=")
   678  		var endpoint, space string
   679  		switch len(v) {
   680  		case 1:
   681  			endpoint = ""
   682  			space = v[0]
   683  		case 2:
   684  			if v[0] == "" {
   685  				return errors.New(parseBindErrorPrefix + "Found = without endpoint name. Use a lone space name to set the default.")
   686  			}
   687  			endpoint = v[0]
   688  			space = v[1]
   689  		default:
   690  			return errors.New(parseBindErrorPrefix + "Found multiple = in binding. Did you forget to space-separate the binding list?")
   691  		}
   692  
   693  		if !names.IsValidSpace(space) {
   694  			return errors.New(parseBindErrorPrefix + "Space name invalid.")
   695  		}
   696  		bindings[endpoint] = space
   697  	}
   698  	c.Bindings = bindings
   699  	return nil
   700  }
   701  
   702  type serviceDeployParams struct {
   703  	charmID       charmstore.CharmID
   704  	serviceName   string
   705  	series        string
   706  	numUnits      int
   707  	configYAML    string
   708  	constraints   constraints.Value
   709  	placement     []*instance.Placement
   710  	storage       map[string]storage.Constraints
   711  	spaceBindings map[string]string
   712  	resources     map[string]string
   713  }
   714  
   715  type serviceDeployer struct {
   716  	ctx *cmd.Context
   717  	api APICmd
   718  }
   719  
   720  func (d *serviceDeployer) newServiceAPIClient() (*apiservice.Client, error) {
   721  	root, err := d.api.NewAPIRoot()
   722  	if err != nil {
   723  		return nil, errors.Trace(err)
   724  	}
   725  	return apiservice.NewClient(root), nil
   726  }
   727  
   728  func (d *serviceDeployer) newAnnotationsAPIClient() (*apiannotations.Client, error) {
   729  	root, err := d.api.NewAPIRoot()
   730  	if err != nil {
   731  		return nil, errors.Trace(err)
   732  	}
   733  	return apiannotations.NewClient(root), nil
   734  }
   735  
   736  func (c *serviceDeployer) serviceDeploy(args serviceDeployParams) error {
   737  	serviceClient, err := c.newServiceAPIClient()
   738  	if err != nil {
   739  		return err
   740  	}
   741  	defer serviceClient.Close()
   742  	for i, p := range args.placement {
   743  		if p.Scope == "model-uuid" {
   744  			p.Scope = serviceClient.ModelUUID()
   745  		}
   746  		args.placement[i] = p
   747  	}
   748  
   749  	clientArgs := apiservice.DeployArgs{
   750  		CharmID:          args.charmID,
   751  		ServiceName:      args.serviceName,
   752  		Series:           args.series,
   753  		NumUnits:         args.numUnits,
   754  		ConfigYAML:       args.configYAML,
   755  		Cons:             args.constraints,
   756  		Placement:        args.placement,
   757  		Storage:          args.storage,
   758  		EndpointBindings: args.spaceBindings,
   759  		Resources:        args.resources,
   760  	}
   761  
   762  	return serviceClient.Deploy(clientArgs)
   763  }
   764  
   765  func (c *DeployCommand) Run(ctx *cmd.Context) error {
   766  	client, err := c.NewAPIClient()
   767  	if err != nil {
   768  		return err
   769  	}
   770  	defer client.Close()
   771  
   772  	err = c.deployCharmOrBundle(ctx, client)
   773  	return block.ProcessBlockedError(err, block.BlockChange)
   774  }
   775  
   776  type metricCredentialsAPI interface {
   777  	SetMetricCredentials(string, []byte) error
   778  	Close() error
   779  }
   780  
   781  type metricsCredentialsAPIImpl struct {
   782  	api   *apiservice.Client
   783  	state api.Connection
   784  }
   785  
   786  // SetMetricCredentials sets the credentials on the service.
   787  func (s *metricsCredentialsAPIImpl) SetMetricCredentials(serviceName string, data []byte) error {
   788  	return s.api.SetMetricCredentials(serviceName, data)
   789  }
   790  
   791  // Close closes the api connection
   792  func (s *metricsCredentialsAPIImpl) Close() error {
   793  	err := s.state.Close()
   794  	if err != nil {
   795  		return errors.Trace(err)
   796  	}
   797  	return nil
   798  }
   799  
   800  var getMetricCredentialsAPI = func(state api.Connection) (metricCredentialsAPI, error) {
   801  	return &metricsCredentialsAPIImpl{api: apiservice.NewClient(state), state: state}, nil
   802  }
   803  
   804  // getFlags returns the flags with the given names. Only flags that are set and
   805  // whose name is included in flagNames are included.
   806  func getFlags(flagSet *gnuflag.FlagSet, flagNames []string) []string {
   807  	flags := make([]string, 0, flagSet.NFlag())
   808  	flagSet.Visit(func(flag *gnuflag.Flag) {
   809  		for _, name := range flagNames {
   810  			if flag.Name == name {
   811  				flags = append(flags, flagWithMinus(name))
   812  			}
   813  		}
   814  	})
   815  	return flags
   816  }
   817  
   818  func flagWithMinus(name string) string {
   819  	if len(name) > 1 {
   820  		return "--" + name
   821  	}
   822  	return "-" + name
   823  }