github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cmd/juju/application/upgradecharm.go (about) 1 // Copyright 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package application 5 6 import ( 7 "fmt" 8 "os" 9 10 "github.com/juju/cmd" 11 "github.com/juju/errors" 12 "github.com/juju/gnuflag" 13 "gopkg.in/juju/charm.v6" 14 charmresource "gopkg.in/juju/charm.v6/resource" 15 "gopkg.in/juju/charmrepo.v3" 16 csclientparams "gopkg.in/juju/charmrepo.v3/csclient/params" 17 "gopkg.in/juju/names.v2" 18 "gopkg.in/juju/worker.v1/catacomb" 19 "gopkg.in/macaroon-bakery.v2-unstable/httpbakery" 20 "gopkg.in/macaroon.v2-unstable" 21 22 "github.com/juju/juju/api" 23 "github.com/juju/juju/api/application" 24 "github.com/juju/juju/api/base" 25 "github.com/juju/juju/api/charms" 26 "github.com/juju/juju/api/controller" 27 "github.com/juju/juju/api/modelconfig" 28 "github.com/juju/juju/apiserver/params" 29 "github.com/juju/juju/charmstore" 30 jujucmd "github.com/juju/juju/cmd" 31 "github.com/juju/juju/cmd/juju/block" 32 "github.com/juju/juju/cmd/juju/common" 33 "github.com/juju/juju/cmd/modelcmd" 34 "github.com/juju/juju/core/model" 35 "github.com/juju/juju/environs/config" 36 "github.com/juju/juju/resource" 37 "github.com/juju/juju/resource/resourceadapters" 38 "github.com/juju/juju/storage" 39 ) 40 41 // NewUpgradeCharmCommand returns a command which upgrades application's charm. 42 func NewUpgradeCharmCommand() cmd.Command { 43 cmd := &upgradeCharmCommand{ 44 DeployResources: resourceadapters.DeployResources, 45 ResolveCharm: resolveCharm, 46 NewCharmAdder: newCharmAdder, 47 NewCharmClient: func(conn base.APICallCloser) CharmClient { 48 return charms.NewClient(conn) 49 }, 50 NewCharmUpgradeClient: func(conn base.APICallCloser) CharmAPIClient { 51 return application.NewClient(conn) 52 }, 53 NewModelConfigGetter: func(conn base.APICallCloser) ModelConfigGetter { 54 return modelconfig.NewClient(conn) 55 }, 56 NewResourceLister: func(conn base.APICallCloser) (ResourceLister, error) { 57 resclient, err := resourceadapters.NewAPIClient(conn) 58 if err != nil { 59 return nil, err 60 } 61 return resclient, nil 62 }, 63 CharmStoreURLGetter: getCharmStoreAPIURL, 64 } 65 return modelcmd.Wrap(cmd) 66 } 67 68 // CharmAPIClient defines a subset of the application facade that deals with 69 // charm related upgrades. 70 type CharmAPIClient interface { 71 CharmUpgradeClient 72 } 73 74 // CharmUpgradeClient defines a subset of the application facade, as required 75 // by the upgrade-charm command. 76 type CharmUpgradeClient interface { 77 GetCharmURL(model.GenerationVersion, string) (*charm.URL, error) 78 Get(model.GenerationVersion, string) (*params.ApplicationGetResults, error) 79 SetCharm(model.GenerationVersion, application.SetCharmConfig) error 80 } 81 82 // CharmClient defines a subset of the charms facade, as required 83 // by the upgrade-charm command. 84 type CharmClient interface { 85 CharmInfo(string) (*charms.CharmInfo, error) 86 } 87 88 // ResourceLister defines a subset of the resources facade, as required 89 // by the upgrade-charm command. 90 type ResourceLister interface { 91 ListResources([]string) ([]resource.ApplicationResources, error) 92 } 93 94 // NewCharmAdderFunc is the type of a function used to construct 95 // a new CharmAdder. 96 type NewCharmAdderFunc func( 97 api.Connection, 98 *httpbakery.Client, 99 string, // Charmstore API URL 100 csclientparams.Channel, 101 ) CharmAdder 102 103 // UpgradeCharm is responsible for upgrading an application's charm. 104 type upgradeCharmCommand struct { 105 modelcmd.ModelCommandBase 106 107 DeployResources resourceadapters.DeployResourcesFunc 108 ResolveCharm ResolveCharmFunc 109 NewCharmAdder NewCharmAdderFunc 110 NewCharmClient func(base.APICallCloser) CharmClient 111 NewCharmUpgradeClient func(base.APICallCloser) CharmAPIClient 112 NewModelConfigGetter func(base.APICallCloser) ModelConfigGetter 113 NewResourceLister func(base.APICallCloser) (ResourceLister, error) 114 CharmStoreURLGetter func(base.APICallCloser) (string, error) 115 116 ApplicationName string 117 // Force should be ubiquitous and we should eventually deprecate both 118 // ForceUnits and ForceSeries; instead just using "force" 119 Force bool 120 ForceUnits bool 121 ForceSeries bool 122 SwitchURL string 123 CharmPath string 124 Revision int // defaults to -1 (latest) 125 126 // Resources is a map of resource name to filename to be uploaded on upgrade. 127 Resources map[string]string 128 129 // Channel holds the charmstore channel to use when obtaining 130 // the charm to be upgraded to. 131 Channel csclientparams.Channel 132 133 // Config is a config file variable, pointing at a YAML file containing 134 // the application config to update. 135 Config cmd.FileVar 136 137 // Storage is a map of storage constraints, keyed on the storage name 138 // defined in charm storage metadata, to add or update during upgrade. 139 Storage map[string]storage.Constraints 140 141 catacomb catacomb.Catacomb 142 plan catacomb.Plan 143 } 144 145 const upgradeCharmDoc = ` 146 When no options are set, the application's charm will be upgraded to the latest 147 revision available in the repository from which it was originally deployed. An 148 explicit revision can be chosen with the --revision option. 149 150 A path will need to be supplied to allow an updated copy of the charm 151 to be located. 152 153 Deploying from a path is intended to suit the workflow of a charm author working 154 on a single client machine; use of this deployment method from multiple clients 155 is not supported and may lead to confusing behaviour. Each local charm gets 156 uploaded with the revision specified in the charm, if possible, otherwise it 157 gets a unique revision (highest in state + 1). 158 159 When deploying from a path, the --path option is used to specify the location from 160 which to load the updated charm. Note that the directory containing the charm must 161 match what was originally used to deploy the charm as a superficial check that the 162 updated charm is compatible. 163 164 Resources may be uploaded at upgrade time by specifying the --resource option. 165 Following the resource option should be name=filepath pair. This option may be 166 repeated more than once to upload more than one resource. 167 168 juju upgrade-charm foo --resource bar=/some/file.tgz --resource baz=./docs/cfg.xml 169 170 Where bar and baz are resources named in the metadata for the foo charm. 171 172 Storage constraints may be added or updated at upgrade time by specifying 173 the --storage option, with the same format as specified in "juju deploy". 174 If new required storage is added by the new charm revision, then you must 175 specify constraints or the defaults will be applied. 176 177 juju upgrade-charm foo --storage cache=ssd,10G 178 179 Charm settings may be added or updated at upgrade time by specifying the 180 --config option, pointing to a YAML-encoded application config file. 181 182 juju upgrade-charm foo --config config.yaml 183 184 If the new version of a charm does not explicitly support the application's series, the 185 upgrade is disallowed unless the --force-series option is used. This option should be 186 used with caution since using a charm on a machine running an unsupported series may 187 cause unexpected behavior. 188 189 The --switch option allows you to replace the charm with an entirely different one. 190 The new charm's URL and revision are inferred as they would be when running a 191 deploy command. 192 193 Please note that --switch is dangerous, because juju only has limited 194 information with which to determine compatibility; the operation will succeed, 195 regardless of potential havoc, so long as the following conditions hold: 196 197 - The new charm must declare all relations that the application is currently 198 participating in. 199 - All config settings shared by the old and new charms must 200 have the same types. 201 202 The new charm may add new relations and configuration settings. 203 204 --switch and --path are mutually exclusive. 205 206 --path and --revision are mutually exclusive. The revision of the updated charm 207 is determined by the contents of the charm at the specified path. 208 209 --switch and --revision are mutually exclusive. To specify a given revision 210 number with --switch, give it in the charm URL, for instance "cs:wordpress-5" 211 would specify revision number 5 of the wordpress charm. 212 213 Use of the --force-units option is not generally recommended; units upgraded while in an 214 error state will not have upgrade-charm hooks executed, and may cause unexpected 215 behavior. 216 217 --force option for LXD Profiles is not generally recommended when upgrading an 218 application; overriding profiles on the container may cause unexpected 219 behavior. 220 ` 221 222 func (c *upgradeCharmCommand) Info() *cmd.Info { 223 return jujucmd.Info(&cmd.Info{ 224 Name: "upgrade-charm", 225 Args: "<application>", 226 Purpose: "Upgrade an application's charm.", 227 Doc: upgradeCharmDoc, 228 }) 229 } 230 231 func (c *upgradeCharmCommand) SetFlags(f *gnuflag.FlagSet) { 232 c.ModelCommandBase.SetFlags(f) 233 f.BoolVar(&c.Force, "force", false, "Allow a charm to be upgraded which bypasses LXD profile allow list") 234 f.BoolVar(&c.ForceUnits, "force-units", false, "Upgrade all units immediately, even if in error state") 235 f.StringVar((*string)(&c.Channel), "channel", "", "Channel to use when getting the charm or bundle from the charm store") 236 f.BoolVar(&c.ForceSeries, "force-series", false, "Upgrade even if series of deployed applications are not supported by the new charm") 237 f.StringVar(&c.SwitchURL, "switch", "", "Crossgrade to a different charm") 238 f.StringVar(&c.CharmPath, "path", "", "Upgrade to a charm located at path") 239 f.IntVar(&c.Revision, "revision", -1, "Explicit revision of current charm") 240 f.Var(stringMap{&c.Resources}, "resource", "Resource to be uploaded to the controller") 241 f.Var(storageFlag{&c.Storage, nil}, "storage", "Charm storage constraints") 242 f.Var(&c.Config, "config", "Path to yaml-formatted application config") 243 } 244 245 func (c *upgradeCharmCommand) Init(args []string) error { 246 switch len(args) { 247 case 1: 248 if !names.IsValidApplication(args[0]) { 249 return errors.Errorf("invalid application name %q", args[0]) 250 } 251 c.ApplicationName = args[0] 252 case 0: 253 return errors.Errorf("no application specified") 254 default: 255 return cmd.CheckEmpty(args[1:]) 256 } 257 if c.SwitchURL != "" && c.Revision != -1 { 258 return errors.Errorf("--switch and --revision are mutually exclusive") 259 } 260 if c.CharmPath != "" && c.Revision != -1 { 261 return errors.Errorf("--path and --revision are mutually exclusive") 262 } 263 if c.SwitchURL != "" && c.CharmPath != "" { 264 return errors.Errorf("--switch and --path are mutually exclusive") 265 } 266 return nil 267 } 268 269 // Run connects to the specified environment and starts the charm 270 // upgrade process. 271 func (c *upgradeCharmCommand) Run(ctx *cmd.Context) error { 272 apiRoot, err := c.NewAPIRoot() 273 if err != nil { 274 return errors.Trace(err) 275 } 276 defer func() { _ = apiRoot.Close() }() 277 278 // If the user has specified config or storage constraints, 279 // make sure the server has facade version 2 at a minimum. 280 if c.Config.Path != "" || len(c.Storage) > 0 { 281 action := "updating config" 282 if c.Config.Path == "" { 283 action = "updating storage constraints" 284 } 285 if apiRoot.BestFacadeVersion("Application") < 2 { 286 suffix := "this server" 287 if version, ok := apiRoot.ServerVersion(); ok { 288 suffix = fmt.Sprintf("server version %s", version) 289 } 290 return errors.New(action + " at upgrade-charm time is not supported by " + suffix) 291 } 292 } 293 294 generation, err := c.ModelGeneration() 295 if err != nil { 296 return errors.Trace(err) 297 } 298 charmUpgradeClient := c.NewCharmUpgradeClient(apiRoot) 299 oldURL, err := charmUpgradeClient.GetCharmURL(generation, c.ApplicationName) 300 if err != nil { 301 return errors.Trace(err) 302 } 303 304 newRef := c.SwitchURL 305 if newRef == "" { 306 newRef = c.CharmPath 307 } 308 if c.SwitchURL == "" && c.CharmPath == "" { 309 // If the charm we are upgrading is local, then we must 310 // specify a path or switch url to upgrade with. 311 if oldURL.Schema == "local" { 312 return errors.New("upgrading a local charm requires either --path or --switch") 313 } 314 // No new URL specified, but revision might have been. 315 newRef = oldURL.WithRevision(c.Revision).String() 316 } 317 318 // First, ensure the charm is added to the model. 319 modelConfigGetter := c.NewModelConfigGetter(apiRoot) 320 modelConfig, err := getModelConfig(modelConfigGetter) 321 if err != nil { 322 return errors.Trace(err) 323 } 324 bakeryClient, err := c.BakeryClient() 325 if err != nil { 326 return errors.Trace(err) 327 } 328 conAPIRoot, err := c.NewControllerAPIRoot() 329 if err != nil { 330 return errors.Trace(err) 331 } 332 csURL, err := c.CharmStoreURLGetter(conAPIRoot) 333 if err != nil { 334 return errors.Trace(err) 335 } 336 337 applicationInfo, err := charmUpgradeClient.Get(generation, c.ApplicationName) 338 if err != nil { 339 return errors.Trace(err) 340 } 341 342 if c.Channel == "" { 343 c.Channel = csclientparams.Channel(applicationInfo.Channel) 344 } 345 charmAdder := c.NewCharmAdder(apiRoot, bakeryClient, csURL, c.Channel) 346 charmRepo := c.getCharmStore(bakeryClient, csURL, modelConfig) 347 348 deployedSeries := applicationInfo.Series 349 350 chID, csMac, err := c.addCharm(charmAdder, charmRepo, modelConfig, oldURL, newRef, deployedSeries, c.Force) 351 if err != nil { 352 if termErr, ok := errors.Cause(err).(*common.TermsRequiredError); ok { 353 return errors.Trace(termErr.UserErr()) 354 } 355 return block.ProcessBlockedError(err, block.BlockChange) 356 } 357 ctx.Infof("Added charm %q to the model.", chID.URL) 358 359 // Next, upgrade resources. 360 charmsClient := c.NewCharmClient(apiRoot) 361 resourceLister, err := c.NewResourceLister(apiRoot) 362 if err != nil { 363 return errors.Trace(err) 364 } 365 ids, err := c.upgradeResources(apiRoot, charmsClient, resourceLister, chID, csMac) 366 if err != nil { 367 return errors.Trace(err) 368 } 369 370 // Finally, upgrade the application. 371 var configYAML []byte 372 if c.Config.Path != "" { 373 configYAML, err = c.Config.Read(ctx) 374 if err != nil { 375 return errors.Trace(err) 376 } 377 } 378 cfg := application.SetCharmConfig{ 379 ApplicationName: c.ApplicationName, 380 CharmID: chID, 381 ConfigSettingsYAML: string(configYAML), 382 Force: c.Force, 383 ForceSeries: c.ForceSeries, 384 ForceUnits: c.ForceUnits, 385 ResourceIDs: ids, 386 StorageConstraints: c.Storage, 387 } 388 return block.ProcessBlockedError(charmUpgradeClient.SetCharm(generation, cfg), block.BlockChange) 389 } 390 391 // upgradeResources pushes metadata up to the server for each resource defined 392 // in the new charm's metadata and returns a map of resource names to pending 393 // IDs to include in the upgrage-charm call. 394 // 395 // TODO(axw) apiRoot is passed in here because DeployResources requires it, 396 // DeployResources should accept a resource-specific client instead. 397 func (c *upgradeCharmCommand) upgradeResources( 398 apiRoot base.APICallCloser, 399 charmsClient CharmClient, 400 resourceLister ResourceLister, 401 chID charmstore.CharmID, 402 csMac *macaroon.Macaroon, 403 ) (map[string]string, error) { 404 filtered, err := getUpgradeResources( 405 charmsClient, 406 resourceLister, 407 c.ApplicationName, 408 chID.URL, 409 c.Resources, 410 ) 411 if err != nil { 412 return nil, errors.Trace(err) 413 } 414 if len(filtered) == 0 { 415 return nil, nil 416 } 417 418 // Note: the validity of user-supplied resources to be uploaded will be 419 // checked further down the stack. 420 ids, err := c.DeployResources( 421 c.ApplicationName, 422 chID, 423 csMac, 424 c.Resources, 425 filtered, 426 apiRoot, 427 ) 428 return ids, errors.Trace(err) 429 } 430 431 func getUpgradeResources( 432 charmsClient CharmClient, 433 resourceLister ResourceLister, 434 applicationID string, 435 charmURL *charm.URL, 436 cliResources map[string]string, 437 ) (map[string]charmresource.Meta, error) { 438 meta, err := getMetaResources(charmURL, charmsClient) 439 if err != nil { 440 return nil, errors.Trace(err) 441 } 442 if len(meta) == 0 { 443 return nil, nil 444 } 445 446 current, err := getResources(applicationID, resourceLister) 447 if err != nil { 448 return nil, errors.Trace(err) 449 } 450 filtered := filterResources(meta, current, cliResources) 451 return filtered, nil 452 } 453 454 func getMetaResources(charmURL *charm.URL, client CharmClient) (map[string]charmresource.Meta, error) { 455 charmInfo, err := client.CharmInfo(charmURL.String()) 456 if err != nil { 457 return nil, errors.Trace(err) 458 } 459 return charmInfo.Meta.Resources, nil 460 } 461 462 func getResources(applicationID string, resourceLister ResourceLister) (map[string]resource.Resource, error) { 463 svcs, err := resourceLister.ListResources([]string{applicationID}) 464 if err != nil { 465 return nil, errors.Trace(err) 466 } 467 return resource.AsMap(svcs[0].Resources), nil 468 } 469 470 func filterResources( 471 meta map[string]charmresource.Meta, 472 current map[string]resource.Resource, 473 uploads map[string]string, 474 ) map[string]charmresource.Meta { 475 filtered := make(map[string]charmresource.Meta) 476 for name, res := range meta { 477 if shouldUpgradeResource(res, uploads, current) { 478 filtered[name] = res 479 } 480 } 481 return filtered 482 } 483 484 // shouldUpgradeResource reports whether we should upload the metadata for the given 485 // resource. This is always true for resources we're adding with the --resource 486 // flag. For resources we're not adding with --resource, we only upload metadata 487 // for charmstore resources. Previously uploaded resources stay pinned to the 488 // data the user uploaded. 489 func shouldUpgradeResource(res charmresource.Meta, uploads map[string]string, current map[string]resource.Resource) bool { 490 // Always upload metadata for resources the user is uploading during 491 // upgrade-charm. 492 if _, ok := uploads[res.Name]; ok { 493 return true 494 } 495 cur, ok := current[res.Name] 496 if !ok { 497 // If there's no information on the server, there should be. 498 return true 499 } 500 // Never override existing resources a user has already uploaded. 501 if cur.Origin == charmresource.OriginUpload { 502 return false 503 } 504 return true 505 } 506 507 func newCharmAdder( 508 api api.Connection, 509 bakeryClient *httpbakery.Client, 510 csURL string, 511 channel csclientparams.Channel, 512 ) CharmAdder { 513 csClient := newCharmStoreClient(bakeryClient, csURL).WithChannel(channel) 514 515 // TODO(katco): This anonymous adapter should go away in favor of 516 // a comprehensive API passed into the upgrade-charm command. 517 charmstoreAdapter := &struct { 518 *charmstoreClient 519 *apiClient 520 }{ 521 charmstoreClient: &charmstoreClient{Client: csClient}, 522 apiClient: &apiClient{Client: api.Client()}, 523 } 524 return charmstoreAdapter 525 } 526 527 func (c *upgradeCharmCommand) getCharmStore( 528 bakeryClient *httpbakery.Client, 529 csURL string, 530 modelConfig *config.Config, 531 ) *charmrepo.CharmStore { 532 csClient := newCharmStoreClient(bakeryClient, csURL).WithChannel(c.Channel) 533 return config.SpecializeCharmRepo( 534 charmrepo.NewCharmStoreFromClient(csClient), 535 modelConfig, 536 ).(*charmrepo.CharmStore) 537 } 538 539 // getCharmStoreAPIURL consults the controller config for the charmstore api url to use. 540 var getCharmStoreAPIURL = func(conAPIRoot base.APICallCloser) (string, error) { 541 controllerAPI := controller.NewClient(conAPIRoot) 542 controllerCfg, err := controllerAPI.ControllerConfig() 543 if err != nil { 544 return "", errors.Trace(err) 545 } 546 return controllerCfg.CharmStoreURL(), nil 547 } 548 549 // addCharm interprets the new charmRef and adds the specified charm if 550 // the new charm is different to what's already deployed as specified by 551 // oldURL. 552 func (c *upgradeCharmCommand) addCharm( 553 charmAdder CharmAdder, 554 charmRepo *charmrepo.CharmStore, 555 config *config.Config, 556 oldURL *charm.URL, 557 charmRef string, 558 deployedSeries string, 559 force bool, 560 ) (charmstore.CharmID, *macaroon.Macaroon, error) { 561 var id charmstore.CharmID 562 // Charm may have been supplied via a path reference. If so, build a 563 // local charm URL from the deployed series. 564 ch, newURL, err := charmrepo.NewCharmAtPathForceSeries(charmRef, deployedSeries, c.ForceSeries) 565 if err == nil { 566 newName := ch.Meta().Name 567 if newName != oldURL.Name { 568 return id, nil, errors.Errorf("cannot upgrade %q to %q", oldURL.Name, newName) 569 } 570 addedURL, err := charmAdder.AddLocalCharm(newURL, ch, force) 571 id.URL = addedURL 572 return id, nil, err 573 } 574 if _, ok := err.(*charmrepo.NotFoundError); ok { 575 return id, nil, errors.Errorf("no charm found at %q", charmRef) 576 } 577 // If we get a "not exists" or invalid path error then we attempt to interpret 578 // the supplied charm reference as a URL below, otherwise we return the error. 579 if err != os.ErrNotExist && !charmrepo.IsInvalidPathError(err) { 580 return id, nil, err 581 } 582 583 refURL, err := charm.ParseURL(charmRef) 584 if err != nil { 585 return id, nil, errors.Trace(err) 586 } 587 588 // Charm has been supplied as a URL so we resolve and deploy using the store. 589 newURL, channel, supportedSeries, err := c.ResolveCharm(charmRepo.ResolveWithChannel, refURL) 590 if err != nil { 591 return id, nil, errors.Trace(err) 592 } 593 id.Channel = channel 594 _, seriesSupportedErr := charm.SeriesForCharm(deployedSeries, supportedSeries) 595 if !c.ForceSeries && deployedSeries != "" && newURL.Series == "" && seriesSupportedErr != nil { 596 series := []string{"no series"} 597 if len(supportedSeries) > 0 { 598 series = supportedSeries 599 } 600 return id, nil, errors.Errorf( 601 "cannot upgrade from single series %q charm to a charm supporting %q. Use --force-series to override.", 602 deployedSeries, series, 603 ) 604 } 605 // If no explicit revision was set with either SwitchURL 606 // or Revision flags, discover the latest. 607 if *newURL == *oldURL { 608 if refURL.Revision != -1 { 609 return id, nil, errors.Errorf("already running specified charm %q", newURL) 610 } 611 // No point in trying to upgrade a charm store charm when 612 // we just determined that's the latest revision 613 // available. 614 return id, nil, errors.Errorf("already running latest charm %q", newURL) 615 } 616 617 curl, csMac, err := addCharmFromURL(charmAdder, newURL, channel, force) 618 if err != nil { 619 return id, nil, errors.Trace(err) 620 } 621 id.URL = curl 622 return id, csMac, nil 623 }