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