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  }