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