github.com/mwhudson/juju@v0.0.0-20160512215208-90ff01f3497f/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  }