github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/cmd/juju/service/upgradecharm.go (about) 1 // Copyright 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package service 5 6 import ( 7 "fmt" 8 "os" 9 "path/filepath" 10 11 "github.com/juju/cmd" 12 "github.com/juju/errors" 13 "github.com/juju/names" 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/macaroon.v1" 19 "launchpad.net/gnuflag" 20 21 "github.com/juju/juju/api" 22 apiservice "github.com/juju/juju/api/service" 23 "github.com/juju/juju/charmstore" 24 "github.com/juju/juju/cmd/juju/block" 25 "github.com/juju/juju/cmd/modelcmd" 26 "github.com/juju/juju/resource" 27 "github.com/juju/juju/resource/resourceadapters" 28 ) 29 30 // NewUpgradeCharmCommand returns a command which upgrades service's charm. 31 func NewUpgradeCharmCommand() cmd.Command { 32 return modelcmd.Wrap(&upgradeCharmCommand{}) 33 } 34 35 // UpgradeCharm is responsible for upgrading a service's charm. 36 type upgradeCharmCommand struct { 37 modelcmd.ModelCommandBase 38 ServiceName string 39 ForceUnits bool 40 ForceSeries bool 41 SwitchURL string 42 CharmPath string 43 Revision int // defaults to -1 (latest) 44 // Resources is a map of resource name to filename to be uploaded on upgrade. 45 Resources map[string]string 46 47 // Channel holds the charmstore channel to use when obtaining 48 // the charm to be upgraded to. 49 Channel csclientparams.Channel 50 } 51 52 const upgradeCharmDoc = ` 53 When no flags are set, the service's charm will be upgraded to the latest 54 revision available in the repository from which it was originally deployed. An 55 explicit revision can be chosen with the --revision flag. 56 57 A path will need to be supplied to allow an updated copy of the charm 58 to be located. 59 60 Deploying from a path is intended to suit the workflow of a charm author working 61 on a single client machine; use of this deployment method from multiple clients 62 is not supported and may lead to confusing behaviour. Each local charm gets 63 uploaded with the revision specified in the charm, if possible, otherwise it 64 gets a unique revision (highest in state + 1). 65 66 When deploying from a path, the --path flag is used to specify the location from 67 which to load the updated charm. Note that the directory containing the charm must 68 match what was originally used to deploy the charm as a superficial check that the 69 updated charm is compatible. 70 71 Resources may be uploaded at upgrade time by specifying the --resource flag. 72 Following the resource flag should be name=filepath pair. This flag may be 73 repeated more than once to upload more than one resource. 74 75 juju upgrade-charm foo --resource bar=/some/file.tgz --resource baz=./docs/cfg.xml 76 77 Where bar and baz are resources named in the metadata for the foo charm. 78 79 If the new version of a charm does not explicitly support the service's series, the 80 upgrade is disallowed unless the --force-series flag is used. This option should be 81 used with caution since using a charm on a machine running an unsupported series may 82 cause unexpected behavior. 83 84 The --switch flag allows you to replace the charm with an entirely different one. 85 The new charm's URL and revision are inferred as they would be when running a 86 deploy command. 87 88 Please note that --switch is dangerous, because juju only has limited 89 information with which to determine compatibility; the operation will succeed, 90 regardless of potential havoc, so long as the following conditions hold: 91 92 - The new charm must declare all relations that the service is currently 93 participating in. 94 - All config settings shared by the old and new charms must 95 have the same types. 96 97 The new charm may add new relations and configuration settings. 98 99 --switch and --path are mutually exclusive. 100 101 --path and --revision are mutually exclusive. The revision of the updated charm 102 is determined by the contents of the charm at the specified path. 103 104 --switch and --revision are mutually exclusive. To specify a given revision 105 number with --switch, give it in the charm URL, for instance "cs:wordpress-5" 106 would specify revision number 5 of the wordpress charm. 107 108 Use of the --force-units flag is not generally recommended; units upgraded while in an 109 error state will not have upgrade-charm hooks executed, and may cause unexpected 110 behavior. 111 ` 112 113 func (c *upgradeCharmCommand) Info() *cmd.Info { 114 return &cmd.Info{ 115 Name: "upgrade-charm", 116 Args: "<service>", 117 Purpose: "upgrade a service's charm", 118 Doc: upgradeCharmDoc, 119 } 120 } 121 122 func (c *upgradeCharmCommand) SetFlags(f *gnuflag.FlagSet) { 123 f.BoolVar(&c.ForceUnits, "force-units", false, "upgrade all units immediately, even if in error state") 124 f.StringVar((*string)(&c.Channel), "channel", "", "channel to use when getting the charm or bundle from the charm store") 125 f.BoolVar(&c.ForceSeries, "force-series", false, "upgrade even if series of deployed services are not supported by the new charm") 126 f.StringVar(&c.SwitchURL, "switch", "", "crossgrade to a different charm") 127 f.StringVar(&c.CharmPath, "path", "", "upgrade to a charm located at path") 128 f.IntVar(&c.Revision, "revision", -1, "explicit revision of current charm") 129 f.Var(stringMap{&c.Resources}, "resource", "resource to be uploaded to the controller") 130 } 131 132 func (c *upgradeCharmCommand) Init(args []string) error { 133 switch len(args) { 134 case 1: 135 if !names.IsValidService(args[0]) { 136 return fmt.Errorf("invalid service name %q", args[0]) 137 } 138 c.ServiceName = args[0] 139 case 0: 140 return fmt.Errorf("no service specified") 141 default: 142 return cmd.CheckEmpty(args[1:]) 143 } 144 if c.SwitchURL != "" && c.Revision != -1 { 145 return fmt.Errorf("--switch and --revision are mutually exclusive") 146 } 147 if c.CharmPath != "" && c.Revision != -1 { 148 return fmt.Errorf("--path and --revision are mutually exclusive") 149 } 150 if c.SwitchURL != "" && c.CharmPath != "" { 151 return fmt.Errorf("--switch and --path are mutually exclusive") 152 } 153 return nil 154 } 155 156 func (c *upgradeCharmCommand) newServiceAPIClient() (*apiservice.Client, error) { 157 root, err := c.NewAPIRoot() 158 if err != nil { 159 return nil, errors.Trace(err) 160 } 161 return apiservice.NewClient(root), nil 162 } 163 164 // Run connects to the specified environment and starts the charm 165 // upgrade process. 166 func (c *upgradeCharmCommand) Run(ctx *cmd.Context) error { 167 client, err := c.NewAPIClient() 168 if err != nil { 169 return err 170 } 171 defer client.Close() 172 173 serviceClient, err := c.newServiceAPIClient() 174 if err != nil { 175 return err 176 } 177 178 oldURL, err := serviceClient.GetCharmURL(c.ServiceName) 179 if err != nil { 180 return err 181 } 182 183 newRef := c.SwitchURL 184 if newRef == "" { 185 newRef = c.CharmPath 186 } 187 if c.SwitchURL == "" && c.CharmPath == "" { 188 // If the charm we are upgrading is local, then we must 189 // specify a path or switch url to upgrade with. 190 if oldURL.Schema == "local" { 191 return errors.New("upgrading a local charm requires either --path or --switch") 192 } 193 // No new URL specified, but revision might have been. 194 newRef = oldURL.WithRevision(c.Revision).String() 195 } 196 197 bakeryClient, err := c.BakeryClient() 198 if err != nil { 199 return errors.Trace(err) 200 } 201 csClient := newCharmStoreClient(bakeryClient).WithChannel(c.Channel) 202 203 conf, err := getClientConfig(client) 204 if err != nil { 205 return errors.Trace(err) 206 } 207 resolver := newCharmURLResolver(conf, csClient) 208 chID, csMac, err := c.addCharm(oldURL, newRef, client, resolver) 209 if err != nil { 210 return block.ProcessBlockedError(err, block.BlockChange) 211 } 212 ctx.Infof("Added charm %q to the model.", chID.URL) 213 214 ids, err := c.upgradeResources(client, chID, csMac) 215 if err != nil { 216 return errors.Trace(err) 217 } 218 219 cfg := apiservice.SetCharmConfig{ 220 ServiceName: c.ServiceName, 221 CharmID: chID, 222 ForceSeries: c.ForceSeries, 223 ForceUnits: c.ForceUnits, 224 ResourceIDs: ids, 225 } 226 227 return block.ProcessBlockedError(serviceClient.SetCharm(cfg), block.BlockChange) 228 } 229 230 // upgradeResources pushes metadata up to the server for each resource defined 231 // in the new charm's metadata and returns a map of resource names to pending 232 // IDs to include in the upgrage-charm call. 233 func (c *upgradeCharmCommand) upgradeResources(client *api.Client, chID charmstore.CharmID, csMac *macaroon.Macaroon) (map[string]string, error) { 234 filtered, err := getUpgradeResources(c, c.ServiceName, chID.URL, client, c.Resources) 235 if err != nil { 236 return nil, errors.Trace(err) 237 } 238 if len(filtered) == 0 { 239 return nil, nil 240 } 241 242 // Note: the validity of user-supplied resources to be uploaded will be 243 // checked further down the stack. 244 return handleResources(c, c.Resources, c.ServiceName, chID, csMac, filtered) 245 } 246 247 // TODO(ericsnow) Move these helpers into handleResources()? 248 249 func getUpgradeResources(c APICmd, serviceID string, cURL *charm.URL, client *api.Client, cliResources map[string]string) (map[string]charmresource.Meta, error) { 250 meta, err := getMetaResources(cURL, client) 251 if err != nil { 252 return nil, errors.Trace(err) 253 } 254 if len(meta) == 0 { 255 return nil, nil 256 } 257 258 current, err := getResources(serviceID, c.NewAPIRoot) 259 if err != nil { 260 return nil, errors.Trace(err) 261 } 262 filtered := filterResources(meta, current, cliResources) 263 return filtered, nil 264 } 265 266 func getMetaResources(cURL *charm.URL, client *api.Client) (map[string]charmresource.Meta, error) { 267 // this gets the charm info that was added to the controller using addcharm. 268 charmInfo, err := client.CharmInfo(cURL.String()) 269 if err != nil { 270 return nil, errors.Trace(err) 271 } 272 return charmInfo.Meta.Resources, nil 273 } 274 275 func getResources(serviceID string, newAPIRoot func() (api.Connection, error)) (map[string]resource.Resource, error) { 276 resclient, err := resourceadapters.NewAPIClient(newAPIRoot) 277 if err != nil { 278 return nil, errors.Trace(err) 279 } 280 svcs, err := resclient.ListResources([]string{serviceID}) 281 if err != nil { 282 return nil, errors.Trace(err) 283 } 284 // ListResources guarantees a number of values returned == number of 285 // services passed in. 286 return resource.AsMap(svcs[0].Resources), nil 287 } 288 289 // TODO(ericsnow) Move filterResources() and shouldUploadMeta() 290 // somewhere more general under the "resource" package? 291 292 func filterResources(meta map[string]charmresource.Meta, current map[string]resource.Resource, uploads map[string]string) map[string]charmresource.Meta { 293 filtered := make(map[string]charmresource.Meta) 294 for name, res := range meta { 295 if shouldUpgradeResource(res, uploads, current) { 296 filtered[name] = res 297 } 298 } 299 return filtered 300 } 301 302 // shouldUpgradeResource reports whether we should upload the metadata for the given 303 // resource. This is always true for resources we're adding with the --resource 304 // flag. For resources we're not adding with --resource, we only upload metadata 305 // for charmstore resources. Previously uploaded resources stay pinned to the 306 // data the user uploaded. 307 func shouldUpgradeResource(res charmresource.Meta, uploads map[string]string, current map[string]resource.Resource) bool { 308 // Always upload metadata for resources the user is uploading during 309 // upgrade-charm. 310 if _, ok := uploads[res.Name]; ok { 311 return true 312 } 313 cur, ok := current[res.Name] 314 if !ok { 315 // If there's no information on the server, there should be. 316 return true 317 } 318 // Never override existing resources a user has already uploaded. 319 if cur.Origin == charmresource.OriginUpload { 320 return false 321 } 322 return true 323 } 324 325 // addCharm interprets the new charmRef and adds the specified charm if the new charm is different 326 // to what's already deployed as specified by oldURL. 327 func (c *upgradeCharmCommand) addCharm( 328 oldURL *charm.URL, 329 charmRef string, 330 client *api.Client, 331 resolver *charmURLResolver, 332 ) (charmstore.CharmID, *macaroon.Macaroon, error) { 333 var id charmstore.CharmID 334 // Charm may have been supplied via a path reference. 335 ch, newURL, err := charmrepo.NewCharmAtPathForceSeries(charmRef, oldURL.Series, c.ForceSeries) 336 if err == nil { 337 _, newName := filepath.Split(charmRef) 338 if newName != oldURL.Name { 339 return id, nil, fmt.Errorf("cannot upgrade %q to %q", oldURL.Name, newName) 340 } 341 addedURL, err := client.AddLocalCharm(newURL, ch) 342 id.URL = addedURL 343 return id, nil, err 344 } 345 if _, ok := err.(*charmrepo.NotFoundError); ok { 346 return id, nil, errors.Errorf("no charm found at %q", charmRef) 347 } 348 // If we get a "not exists" or invalid path error then we attempt to interpret 349 // the supplied charm reference as a URL below, otherwise we return the error. 350 if err != os.ErrNotExist && !charmrepo.IsInvalidPathError(err) { 351 return id, nil, err 352 } 353 354 // Charm has been supplied as a URL so we resolve and deploy using the store. 355 newURL, channel, supportedSeries, store, err := resolver.resolve(charmRef) 356 if err != nil { 357 return id, nil, errors.Trace(err) 358 } 359 id.Channel = channel 360 if !c.ForceSeries && oldURL.Series != "" && newURL.Series == "" && !isSeriesSupported(oldURL.Series, supportedSeries) { 361 series := []string{"no series"} 362 if len(supportedSeries) > 0 { 363 series = supportedSeries 364 } 365 return id, nil, errors.Errorf( 366 "cannot upgrade from single series %q charm to a charm supporting %q. Use --force-series to override.", 367 oldURL.Series, series, 368 ) 369 } 370 // If no explicit revision was set with either SwitchURL 371 // or Revision flags, discover the latest. 372 if *newURL == *oldURL { 373 newRef, _ := charm.ParseURL(charmRef) 374 if newRef.Revision != -1 { 375 return id, nil, fmt.Errorf("already running specified charm %q", newURL) 376 } 377 // No point in trying to upgrade a charm store charm when 378 // we just determined that's the latest revision 379 // available. 380 return id, nil, fmt.Errorf("already running latest charm %q", newURL) 381 } 382 383 curl, csMac, err := addCharmFromURL(client, newURL, channel, store.Client()) 384 if err != nil { 385 return id, nil, errors.Trace(err) 386 } 387 id.URL = curl 388 return id, csMac, nil 389 }