github.com/cloudbase/juju-core@v0.0.0-20140504232958-a7271ac7912f/cmd/juju/upgradecharm.go (about)

     1  // Copyright 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package main
     5  
     6  import (
     7  	"errors"
     8  	"fmt"
     9  	"os"
    10  
    11  	"launchpad.net/gnuflag"
    12  
    13  	"launchpad.net/juju-core/charm"
    14  	"launchpad.net/juju-core/cmd"
    15  	"launchpad.net/juju-core/environs/config"
    16  	"launchpad.net/juju-core/juju"
    17  	"launchpad.net/juju-core/names"
    18  	"launchpad.net/juju-core/state/api/params"
    19  )
    20  
    21  // UpgradeCharm is responsible for upgrading a service's charm.
    22  type UpgradeCharmCommand struct {
    23  	cmd.EnvCommandBase
    24  	ServiceName string
    25  	Force       bool
    26  	RepoPath    string // defaults to JUJU_REPOSITORY
    27  	SwitchURL   string
    28  	Revision    int // defaults to -1 (latest)
    29  }
    30  
    31  const upgradeCharmDoc = `
    32  When no flags are set, the service's charm will be upgraded to the latest
    33  revision available in the repository from which it was originally deployed. An
    34  explicit revision can be chosen with the --revision flag.
    35  
    36  If the charm came from a local repository, its path will be assumed to be
    37  $JUJU_REPOSITORY unless overridden by --repository.
    38  
    39  The local repository behaviour is tuned specifically to the workflow of a charm
    40  author working on a single client machine; use of local repositories from
    41  multiple clients is not supported and may lead to confusing behaviour. Each
    42  local charm gets uploaded with the revision specified in the charm, if possible,
    43  otherwise it gets a unique revision (highest in state + 1).
    44  
    45  The --switch flag allows you to replace the charm with an entirely different
    46  one. The new charm's URL and revision are inferred as they would be when running
    47  a deploy command.
    48  
    49  Please note that --switch is dangerous, because juju only has limited
    50  information with which to determine compatibility; the operation will succeed,
    51  regardless of potential havoc, so long as the following conditions hold:
    52  
    53  - The new charm must declare all relations that the service is currently
    54  participating in.
    55  - All config settings shared by the old and new charms must
    56  have the same types.
    57  
    58  The new charm may add new relations and configuration settings.
    59  
    60  --switch and --revision are mutually exclusive. To specify a given revision
    61  number with --switch, give it in the charm URL, for instance "cs:wordpress-5"
    62  would specify revision number 5 of the wordpress charm.
    63  
    64  Use of the --force flag is not generally recommended; units upgraded while in an
    65  error state will not have upgrade-charm hooks executed, and may cause unexpected
    66  behavior.
    67  `
    68  
    69  func (c *UpgradeCharmCommand) Info() *cmd.Info {
    70  	return &cmd.Info{
    71  		Name:    "upgrade-charm",
    72  		Args:    "<service>",
    73  		Purpose: "upgrade a service's charm",
    74  		Doc:     upgradeCharmDoc,
    75  	}
    76  }
    77  
    78  func (c *UpgradeCharmCommand) SetFlags(f *gnuflag.FlagSet) {
    79  	c.EnvCommandBase.SetFlags(f)
    80  	f.BoolVar(&c.Force, "force", false, "upgrade all units immediately, even if in error state")
    81  	f.StringVar(&c.RepoPath, "repository", os.Getenv("JUJU_REPOSITORY"), "local charm repository path")
    82  	f.StringVar(&c.SwitchURL, "switch", "", "crossgrade to a different charm")
    83  	f.IntVar(&c.Revision, "revision", -1, "explicit revision of current charm")
    84  }
    85  
    86  func (c *UpgradeCharmCommand) Init(args []string) error {
    87  	switch len(args) {
    88  	case 1:
    89  		if !names.IsService(args[0]) {
    90  			return fmt.Errorf("invalid service name %q", args[0])
    91  		}
    92  		c.ServiceName = args[0]
    93  	case 0:
    94  		return errors.New("no service specified")
    95  	default:
    96  		return cmd.CheckEmpty(args[1:])
    97  	}
    98  	if c.SwitchURL != "" && c.Revision != -1 {
    99  		return fmt.Errorf("--switch and --revision are mutually exclusive")
   100  	}
   101  	return nil
   102  }
   103  
   104  // Run connects to the specified environment and starts the charm
   105  // upgrade process.
   106  func (c *UpgradeCharmCommand) Run(ctx *cmd.Context) error {
   107  	client, err := juju.NewAPIClientFromName(c.EnvName)
   108  	if err != nil {
   109  		return err
   110  	}
   111  	defer client.Close()
   112  	oldURL, err := client.ServiceGetCharmURL(c.ServiceName)
   113  	if params.IsCodeNotImplemented(err) {
   114  		logger.Infof("ServiceGetCharmURL is not implemented by the API server, switching to 1.16 compatibility mode (direct DB connection).")
   115  		return c.run1dot16(ctx)
   116  	}
   117  	if err != nil {
   118  		return err
   119  	}
   120  
   121  	attrs, err := client.EnvironmentGet()
   122  	if err != nil {
   123  		return err
   124  	}
   125  	conf, err := config.New(config.NoDefaults, attrs)
   126  	if err != nil {
   127  		return err
   128  	}
   129  
   130  	var newURL *charm.URL
   131  	if c.SwitchURL != "" {
   132  		// A new charm URL was explicitly specified.
   133  		newURL, err = charm.InferURL(c.SwitchURL, conf.DefaultSeries())
   134  		if err != nil {
   135  			return err
   136  		}
   137  	} else {
   138  		// No new URL specified, but revision might have been.
   139  		newURL = oldURL.WithRevision(c.Revision)
   140  	}
   141  	repo, err := charm.InferRepository(newURL, ctx.AbsPath(c.RepoPath))
   142  	if err != nil {
   143  		return err
   144  	}
   145  
   146  	repo = config.SpecializeCharmRepo(repo, conf)
   147  
   148  	// If no explicit revision was set with either SwitchURL
   149  	// or Revision flags, discover the latest.
   150  	explicitRevision := true
   151  	if newURL.Revision == -1 {
   152  		explicitRevision = false
   153  		latest, err := charm.Latest(repo, newURL)
   154  		if err != nil {
   155  			return err
   156  		}
   157  		newURL = newURL.WithRevision(latest)
   158  	}
   159  	if *newURL == *oldURL {
   160  		if explicitRevision {
   161  			return fmt.Errorf("already running specified charm %q", newURL)
   162  		} else if newURL.Schema == "cs" {
   163  			// No point in trying to upgrade a charm store charm when
   164  			// we just determined that's the latest revision
   165  			// available.
   166  			return fmt.Errorf("already running latest charm %q", newURL)
   167  		}
   168  	}
   169  
   170  	addedURL, err := addCharmViaAPI(client, ctx, newURL, repo)
   171  	if err != nil {
   172  		return err
   173  	}
   174  
   175  	return client.ServiceSetCharm(c.ServiceName, addedURL.String(), c.Force)
   176  }
   177  
   178  // run1dot16 perfoms the charm upgrade using a 1.16 compatible code
   179  // path, with a direct state connection. Remove once the support for
   180  // 1.16 is dropped.
   181  func (c *UpgradeCharmCommand) run1dot16(ctx *cmd.Context) error {
   182  	conn, err := juju.NewConnFromName(c.EnvName)
   183  	if err != nil {
   184  		return err
   185  	}
   186  	defer conn.Close()
   187  	service, err := conn.State.Service(c.ServiceName)
   188  	if err != nil {
   189  		return err
   190  	}
   191  
   192  	conf, err := conn.State.EnvironConfig()
   193  	if err != nil {
   194  		return err
   195  	}
   196  
   197  	oldURL, _ := service.CharmURL()
   198  	var newURL *charm.URL
   199  	if c.SwitchURL != "" {
   200  		// A new charm URL was explicitly specified.
   201  		conf, err := conn.State.EnvironConfig()
   202  		if err != nil {
   203  			return err
   204  		}
   205  		newURL, err = charm.InferURL(c.SwitchURL, conf.DefaultSeries())
   206  		if err != nil {
   207  			return err
   208  		}
   209  	} else {
   210  		// No new URL specified, but revision might have been.
   211  		newURL = oldURL.WithRevision(c.Revision)
   212  	}
   213  	repo, err := charm.InferRepository(newURL, ctx.AbsPath(c.RepoPath))
   214  	if err != nil {
   215  		return err
   216  	}
   217  
   218  	repo = config.SpecializeCharmRepo(repo, conf)
   219  
   220  	// If no explicit revision was set with either SwitchURL
   221  	// or Revision flags, discover the latest.
   222  	explicitRevision := true
   223  	if newURL.Revision == -1 {
   224  		explicitRevision = false
   225  		latest, err := charm.Latest(repo, newURL)
   226  		if err != nil {
   227  			return err
   228  		}
   229  		newURL = newURL.WithRevision(latest)
   230  	}
   231  	bumpRevision := false
   232  	if *newURL == *oldURL {
   233  		if explicitRevision {
   234  			return fmt.Errorf("already running specified charm %q", newURL)
   235  		}
   236  		// Only try bumping the revision when necessary (local dir charm).
   237  		if _, isLocal := repo.(*charm.LocalRepository); !isLocal {
   238  			// TODO(dimitern): If the --force flag is set to something
   239  			// different to before, we might actually want to allow this
   240  			// case (and the other error below). LP bug #1174287
   241  			return fmt.Errorf("already running latest charm %q", newURL)
   242  		}
   243  		// This is a local repository.
   244  		if ch, err := repo.Get(newURL); err != nil {
   245  			return err
   246  		} else if _, bumpRevision = ch.(*charm.Dir); !bumpRevision {
   247  			// Only bump the revision when it's a directory.
   248  			return fmt.Errorf("cannot increment revision of charm %q: not a directory", newURL)
   249  		}
   250  	}
   251  	sch, err := conn.PutCharm(newURL, repo, bumpRevision)
   252  	if err != nil {
   253  		return err
   254  	}
   255  	return service.SetCharm(sch, c.Force)
   256  }