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