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 }