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 }