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  }