github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/cmd/juju/commands/upgradejuju.go (about)

     1  // Copyright 2012, 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package commands
     5  
     6  import (
     7  	"bufio"
     8  	stderrors "errors"
     9  	"fmt"
    10  	"io"
    11  	"os"
    12  	"path"
    13  	"strings"
    14  
    15  	"github.com/juju/cmd"
    16  	"github.com/juju/errors"
    17  	"github.com/juju/gnuflag"
    18  	"github.com/juju/utils/series"
    19  	"github.com/juju/version"
    20  
    21  	"github.com/juju/juju/api/controller"
    22  	"github.com/juju/juju/api/modelconfig"
    23  	"github.com/juju/juju/apiserver/params"
    24  	"github.com/juju/juju/cmd/juju/block"
    25  	"github.com/juju/juju/cmd/modelcmd"
    26  	"github.com/juju/juju/environs/config"
    27  	"github.com/juju/juju/environs/sync"
    28  	coretools "github.com/juju/juju/tools"
    29  	jujuversion "github.com/juju/juju/version"
    30  )
    31  
    32  var usageUpgradeJujuSummary = `
    33  Upgrades Juju on all machines in a model.`[1:]
    34  
    35  var usageUpgradeJujuDetails = `
    36  Juju provides agent software to every machine it creates. This command
    37  upgrades that software across an entire model, which is, by default, the
    38  current model.
    39  A model's agent version can be shown with `[1:] + "`juju model-config agent-\nversion`" + `.
    40  A version is denoted by: major.minor.patch
    41  The upgrade candidate will be auto-selected if '--agent-version' is not
    42  specified:
    43   - If the server major version matches the client major version, the
    44   version selected is minor+1. If such a minor version is not available then
    45   the next patch version is chosen.
    46   - If the server major version does not match the client major version,
    47   the version selected is that of the client version.
    48  If the controller is without internet access, the client must first supply
    49  the software to the controller's cache via the ` + "`juju sync-tools`" + ` command.
    50  The command will abort if an upgrade is in progress. It will also abort if
    51  a previous upgrade was not fully completed (e.g.: if one of the
    52  controllers in a high availability model failed to upgrade).
    53  If a failed upgrade has been resolved, '--reset-previous-upgrade' can be
    54  used to allow the upgrade to proceed.
    55  Backups are recommended prior to upgrading.
    56  
    57  Examples:
    58      juju upgrade-juju --dry-run
    59      juju upgrade-juju --agent-version 2.0.1
    60      
    61  See also: 
    62      sync-tools`
    63  
    64  func newUpgradeJujuCommand(minUpgradeVers map[int]version.Number, options ...modelcmd.WrapOption) cmd.Command {
    65  	if minUpgradeVers == nil {
    66  		minUpgradeVers = minMajorUpgradeVersion
    67  	}
    68  	return modelcmd.Wrap(&upgradeJujuCommand{minMajorUpgradeVersion: minUpgradeVers}, options...)
    69  }
    70  
    71  // upgradeJujuCommand upgrades the agents in a juju installation.
    72  type upgradeJujuCommand struct {
    73  	modelcmd.ModelCommandBase
    74  	vers          string
    75  	Version       version.Number
    76  	BuildAgent    bool
    77  	DryRun        bool
    78  	ResetPrevious bool
    79  	AssumeYes     bool
    80  
    81  	// minMajorUpgradeVersion maps known major numbers to
    82  	// the minimum version that can be upgraded to that
    83  	// major version.  For example, users must be running
    84  	// 1.25.4 or later in order to upgrade to 2.0.
    85  	minMajorUpgradeVersion map[int]version.Number
    86  }
    87  
    88  func (c *upgradeJujuCommand) Info() *cmd.Info {
    89  	return &cmd.Info{
    90  		Name:    "upgrade-juju",
    91  		Purpose: usageUpgradeJujuSummary,
    92  		Doc:     usageUpgradeJujuDetails,
    93  	}
    94  }
    95  
    96  func (c *upgradeJujuCommand) SetFlags(f *gnuflag.FlagSet) {
    97  	c.ModelCommandBase.SetFlags(f)
    98  	f.StringVar(&c.vers, "agent-version", "", "Upgrade to specific version")
    99  	f.BoolVar(&c.BuildAgent, "build-agent", false, "Build a local version of the agent binary; for development use only")
   100  	f.BoolVar(&c.DryRun, "dry-run", false, "Don't change anything, just report what would be changed")
   101  	f.BoolVar(&c.ResetPrevious, "reset-previous-upgrade", false, "Clear the previous (incomplete) upgrade status (use with care)")
   102  	f.BoolVar(&c.AssumeYes, "y", false, "Answer 'yes' to confirmation prompts")
   103  	f.BoolVar(&c.AssumeYes, "yes", false, "")
   104  }
   105  
   106  func (c *upgradeJujuCommand) Init(args []string) error {
   107  	if c.vers != "" {
   108  		vers, err := version.Parse(c.vers)
   109  		if err != nil {
   110  			return err
   111  		}
   112  		if c.BuildAgent && vers.Build != 0 {
   113  			// TODO(fwereade): when we start taking versions from actual built
   114  			// code, we should disable --agent-version when used with --build-agent.
   115  			// For now, it's the only way to experiment with version upgrade
   116  			// behaviour live, so the only restriction is that Build cannot
   117  			// be used (because its value needs to be chosen internally so as
   118  			// not to collide with existing tools).
   119  			return errors.New("cannot specify build number when building an agent")
   120  		}
   121  		c.Version = vers
   122  	}
   123  	return cmd.CheckEmpty(args)
   124  }
   125  
   126  var (
   127  	errUpToDate            = stderrors.New("no upgrades available")
   128  	downgradeErrMsg        = "cannot change version from %s to %s"
   129  	minMajorUpgradeVersion = map[int]version.Number{
   130  		2: version.MustParse("1.25.4"),
   131  	}
   132  )
   133  
   134  // canUpgradeRunningVersion determines if the version of the running
   135  // environment can be upgraded using this version of the
   136  // upgrade-juju command.  Only versions with a minor version
   137  // of 0 are expected to be able to upgrade environments running
   138  // the previous major version.
   139  //
   140  // This check is needed because we do not guarantee API
   141  // compatibility across major versions.  For example, a 3.3.0
   142  // version of the upgrade-juju command may not know how to upgrade
   143  // an environment running juju 4.0.0.
   144  //
   145  // The exception is that a N.0.* client must be able to upgrade
   146  // an environment one major version prior (N-1.*.*) so that
   147  // it can be used to upgrade the environment to N.0.*.  For
   148  // example, the 2.0.1 upgrade-juju command must be able to upgrade
   149  // environments running 1.* since it must be able to upgrade
   150  // environments from 1.25.4 -> 2.0.*.
   151  func canUpgradeRunningVersion(runningAgentVer version.Number) bool {
   152  	if runningAgentVer.Major == jujuversion.Current.Major {
   153  		return true
   154  	}
   155  	if jujuversion.Current.Minor == 0 && runningAgentVer.Major == (jujuversion.Current.Major-1) {
   156  		return true
   157  	}
   158  	return false
   159  }
   160  
   161  func formatTools(tools coretools.List) string {
   162  	formatted := make([]string, len(tools))
   163  	for i, tools := range tools {
   164  		formatted[i] = fmt.Sprintf("    %s", tools.Version.String())
   165  	}
   166  	return strings.Join(formatted, "\n")
   167  }
   168  
   169  type upgradeJujuAPI interface {
   170  	FindTools(majorVersion, minorVersion int, series, arch string) (result params.FindToolsResult, err error)
   171  	UploadTools(r io.ReadSeeker, vers version.Binary, additionalSeries ...string) (coretools.List, error)
   172  	AbortCurrentUpgrade() error
   173  	SetModelAgentVersion(version version.Number) error
   174  	Close() error
   175  }
   176  
   177  type modelConfigAPI interface {
   178  	ModelGet() (map[string]interface{}, error)
   179  	Close() error
   180  }
   181  
   182  type controllerAPI interface {
   183  	ModelConfig() (map[string]interface{}, error)
   184  	Close() error
   185  }
   186  
   187  var getUpgradeJujuAPI = func(c *upgradeJujuCommand) (upgradeJujuAPI, error) {
   188  	return c.NewAPIClient()
   189  }
   190  
   191  var getModelConfigAPI = func(c *upgradeJujuCommand) (modelConfigAPI, error) {
   192  	api, err := c.NewAPIRoot()
   193  	if err != nil {
   194  		return nil, errors.Trace(err)
   195  	}
   196  	return modelconfig.NewClient(api), nil
   197  }
   198  
   199  var getControllerAPI = func(c *upgradeJujuCommand) (controllerAPI, error) {
   200  	api, err := c.NewControllerAPIRoot()
   201  	if err != nil {
   202  		return nil, errors.Trace(err)
   203  	}
   204  	return controller.NewClient(api), nil
   205  }
   206  
   207  // Run changes the version proposed for the juju envtools.
   208  func (c *upgradeJujuCommand) Run(ctx *cmd.Context) (err error) {
   209  
   210  	client, err := getUpgradeJujuAPI(c)
   211  	if err != nil {
   212  		return err
   213  	}
   214  	defer client.Close()
   215  	modelConfigClient, err := getModelConfigAPI(c)
   216  	if err != nil {
   217  		return err
   218  	}
   219  	defer modelConfigClient.Close()
   220  	controllerClient, err := getControllerAPI(c)
   221  	if err != nil {
   222  		return err
   223  	}
   224  	defer controllerClient.Close()
   225  	defer func() {
   226  		if err == errUpToDate {
   227  			ctx.Infof(err.Error())
   228  			err = nil
   229  		}
   230  	}()
   231  
   232  	// Determine the version to upgrade to, uploading tools if necessary.
   233  	attrs, err := modelConfigClient.ModelGet()
   234  	if err != nil {
   235  		return err
   236  	}
   237  	cfg, err := config.New(config.NoDefaults, attrs)
   238  	if err != nil {
   239  		return err
   240  	}
   241  
   242  	controllerModelConfig, err := controllerClient.ModelConfig()
   243  	if err != nil {
   244  		return err
   245  	}
   246  	isControllerModel := cfg.UUID() == controllerModelConfig[config.UUIDKey]
   247  	if c.BuildAgent && !isControllerModel {
   248  		// For UploadTools, model must be the "controller" model,
   249  		// that is, modelUUID == controllerUUID
   250  		return errors.Errorf("--build-agent can only be used with the controller model")
   251  	}
   252  
   253  	agentVersion, ok := cfg.AgentVersion()
   254  	if !ok {
   255  		// Can't happen. In theory.
   256  		return errors.New("incomplete model configuration")
   257  	}
   258  
   259  	if c.BuildAgent && c.Version == version.Zero {
   260  		// Currently, uploading tools assumes the version to be
   261  		// the same as jujuversion.Current if not specified with
   262  		// --agent-version.
   263  		c.Version = jujuversion.Current
   264  	}
   265  	warnCompat := false
   266  	switch {
   267  	case !canUpgradeRunningVersion(agentVersion):
   268  		// This version of upgrade-juju cannot upgrade the running
   269  		// environment version (can't guarantee API compatibility).
   270  		return errors.Errorf("cannot upgrade a %s model with a %s client",
   271  			agentVersion, jujuversion.Current)
   272  	case c.Version != version.Zero && c.Version.Major < agentVersion.Major:
   273  		// The specified version would downgrade the environment.
   274  		// Don't upgrade and return an error.
   275  		return errors.Errorf(downgradeErrMsg, agentVersion, c.Version)
   276  	case agentVersion.Major != jujuversion.Current.Major:
   277  		// Running environment is the previous major version (a higher major
   278  		// version wouldn't have passed the check in canUpgradeRunningVersion).
   279  		if c.Version == version.Zero || c.Version.Major == agentVersion.Major {
   280  			// Not requesting an upgrade across major release boundary.
   281  			// Warn of incompatible CLI and filter on the prior major version
   282  			// when searching for available tools.
   283  			// TODO(cherylj) Add in a suggestion to upgrade to 2.0 if
   284  			// no matching tools are found (bug 1532670)
   285  			warnCompat = true
   286  			break
   287  		}
   288  		// User requested an upgrade to the next major version.
   289  		// Fallthrough to the next case to verify that the upgrade
   290  		// conditions are met.
   291  		fallthrough
   292  	case c.Version.Major > agentVersion.Major:
   293  		// User is requesting an upgrade to a new major number
   294  		// Only upgrade to a different major number if:
   295  		// 1 - Explicitly requested with --agent-version or using --build-agent, and
   296  		// 2 - The environment is running a valid version to upgrade from, and
   297  		// 3 - The upgrade is to a minor version of 0.
   298  		minVer, ok := c.minMajorUpgradeVersion[c.Version.Major]
   299  		if !ok {
   300  			return errors.Errorf("unknown version %q", c.Version)
   301  		}
   302  		retErr := false
   303  		if c.Version.Minor != 0 {
   304  			ctx.Infof("upgrades to %s must first go through juju %d.0",
   305  				c.Version, c.Version.Major)
   306  			retErr = true
   307  		}
   308  		if comp := agentVersion.Compare(minVer); comp < 0 {
   309  			ctx.Infof("upgrades to a new major version must first go through %s",
   310  				minVer)
   311  			retErr = true
   312  		}
   313  		if retErr {
   314  			return errors.New("unable to upgrade to requested version")
   315  		}
   316  	}
   317  
   318  	context, err := c.initVersions(client, cfg, agentVersion, warnCompat)
   319  	if err != nil {
   320  		return err
   321  	}
   322  	// If we're running a custom build or the user has asked for a new agent
   323  	// to be built, upload a local jujud binary if possible.
   324  	uploadLocalBinary := isControllerModel && c.Version == version.Zero && tryImplicitUpload(agentVersion)
   325  	if !warnCompat && (uploadLocalBinary || c.BuildAgent) && !c.DryRun {
   326  		if err := context.uploadTools(c.BuildAgent); err != nil {
   327  			// If we've explicitly asked to build an agent binary, or the upload failed
   328  			// because changes were blocked, we'll return an error.
   329  			if err2 := block.ProcessBlockedError(err, block.BlockChange); c.BuildAgent || err2 == cmd.ErrSilent {
   330  				return err2
   331  			}
   332  		}
   333  		builtMsg := ""
   334  		if c.BuildAgent {
   335  			builtMsg = " (built from source)"
   336  		}
   337  		fmt.Fprintf(ctx.Stdout, "no prepackaged tools available, using local agent binary %v%s\n", context.chosen, builtMsg)
   338  	}
   339  
   340  	// If there was an error implicitly uploading a binary, we'll still look for any packaged binaries
   341  	// since there may still be a valid upgrade and the user didn't ask for any local binary.
   342  	if err := context.validate(); err != nil {
   343  		return err
   344  	}
   345  	// TODO(fwereade): this list may be incomplete, pending envtools.Upload change.
   346  	ctx.Verbosef("available tools:\n%s", formatTools(context.tools))
   347  	ctx.Verbosef("best version:\n    %s", context.chosen)
   348  	if warnCompat {
   349  		fmt.Fprintf(ctx.Stderr, "version %s incompatible with this client (%s)\n", context.chosen, jujuversion.Current)
   350  	}
   351  	if c.DryRun {
   352  		fmt.Fprintf(ctx.Stderr, "upgrade to this version by running\n    juju upgrade-juju --agent-version=\"%s\"\n", context.chosen)
   353  	} else {
   354  		if c.ResetPrevious {
   355  			if ok, err := c.confirmResetPreviousUpgrade(ctx); !ok || err != nil {
   356  				const message = "previous upgrade not reset and no new upgrade triggered"
   357  				if err != nil {
   358  					return errors.Annotate(err, message)
   359  				}
   360  				return errors.New(message)
   361  			}
   362  			if err := client.AbortCurrentUpgrade(); err != nil {
   363  				return block.ProcessBlockedError(err, block.BlockChange)
   364  			}
   365  		}
   366  		if err := client.SetModelAgentVersion(context.chosen); err != nil {
   367  			if params.IsCodeUpgradeInProgress(err) {
   368  				return errors.Errorf("%s\n\n"+
   369  					"Please wait for the upgrade to complete or if there was a problem with\n"+
   370  					"the last upgrade that has been resolved, consider running the\n"+
   371  					"upgrade-juju command with the --reset-previous-upgrade flag.", err,
   372  				)
   373  			} else {
   374  				return block.ProcessBlockedError(err, block.BlockChange)
   375  			}
   376  		}
   377  		fmt.Fprintf(ctx.Stdout, "started upgrade to %s\n", context.chosen)
   378  	}
   379  	return nil
   380  }
   381  
   382  func tryImplicitUpload(agentVersion version.Number) bool {
   383  	newerAgent := jujuversion.Current.Compare(agentVersion) > 0
   384  	return newerAgent || agentVersion.Build > 0 || jujuversion.Current.Build > 0
   385  }
   386  
   387  const resetPreviousUpgradeMessage = `
   388  WARNING! using --reset-previous-upgrade when an upgrade is in progress
   389  will cause the upgrade to fail. Only use this option to clear an
   390  incomplete upgrade where the root cause has been resolved.
   391  
   392  Continue [y/N]? `
   393  
   394  func (c *upgradeJujuCommand) confirmResetPreviousUpgrade(ctx *cmd.Context) (bool, error) {
   395  	if c.AssumeYes {
   396  		return true, nil
   397  	}
   398  	fmt.Fprint(ctx.Stdout, resetPreviousUpgradeMessage)
   399  	scanner := bufio.NewScanner(ctx.Stdin)
   400  	scanner.Scan()
   401  	err := scanner.Err()
   402  	if err != nil && err != io.EOF {
   403  		return false, err
   404  	}
   405  	answer := strings.ToLower(scanner.Text())
   406  	return answer == "y" || answer == "yes", nil
   407  }
   408  
   409  // initVersions collects state relevant to an upgrade decision. The returned
   410  // agent and client versions, and the list of currently available tools, will
   411  // always be accurate; the chosen version, and the flag indicating development
   412  // mode, may remain blank until uploadTools or validate is called.
   413  func (c *upgradeJujuCommand) initVersions(client upgradeJujuAPI, cfg *config.Config, agentVersion version.Number, filterOnPrior bool) (*upgradeContext, error) {
   414  	if c.Version == agentVersion {
   415  		return nil, errUpToDate
   416  	}
   417  	filterVersion := jujuversion.Current
   418  	if c.Version != version.Zero {
   419  		filterVersion = c.Version
   420  	} else if filterOnPrior {
   421  		// Trying to find the latest of the prior major version.
   422  		// TODO (cherylj) if no tools found, suggest upgrade to
   423  		// the current client version.
   424  		filterVersion.Major--
   425  	}
   426  	logger.Debugf("searching for tools with major: %d", filterVersion.Major)
   427  	findResult, err := client.FindTools(filterVersion.Major, -1, "", "")
   428  	if err != nil {
   429  		return nil, err
   430  	}
   431  	err = findResult.Error
   432  	if findResult.Error != nil {
   433  		if !params.IsCodeNotFound(err) {
   434  			return nil, err
   435  		}
   436  		if !tryImplicitUpload(agentVersion) && !c.BuildAgent {
   437  			// No tools found and we shouldn't upload any, so if we are not asking for a
   438  			// major upgrade, pretend there is no more recent version available.
   439  			if c.Version == version.Zero && agentVersion.Major == filterVersion.Major {
   440  				return nil, errUpToDate
   441  			}
   442  			return nil, err
   443  		}
   444  	}
   445  	return &upgradeContext{
   446  		agent:     agentVersion,
   447  		client:    jujuversion.Current,
   448  		chosen:    c.Version,
   449  		tools:     findResult.List,
   450  		apiClient: client,
   451  		config:    cfg,
   452  	}, nil
   453  }
   454  
   455  // upgradeContext holds the version information for making upgrade decisions.
   456  type upgradeContext struct {
   457  	agent     version.Number
   458  	client    version.Number
   459  	chosen    version.Number
   460  	tools     coretools.List
   461  	config    *config.Config
   462  	apiClient upgradeJujuAPI
   463  }
   464  
   465  // uploadTools compiles jujud from $GOPATH and uploads it into the supplied
   466  // storage. If no version has been explicitly chosen, the version number
   467  // reported by the built tools will be based on the client version number.
   468  // In any case, the version number reported will have a build component higher
   469  // than that of any otherwise-matching available envtools.
   470  // uploadTools resets the chosen version and replaces the available tools
   471  // with the ones just uploaded.
   472  func (context *upgradeContext) uploadTools(buildAgent bool) (err error) {
   473  	// TODO(fwereade): this is kinda crack: we should not assume that
   474  	// jujuversion.Current matches whatever source happens to be built. The
   475  	// ideal would be:
   476  	//  1) compile jujud from $GOPATH into some build dir
   477  	//  2) get actual version with `jujud version`
   478  	//  3) check actual version for compatibility with CLI tools
   479  	//  4) generate unique build version with reference to available tools
   480  	//  5) force-version that unique version into the dir directly
   481  	//  6) archive and upload the build dir
   482  	// ...but there's no way we have time for that now. In the meantime,
   483  	// considering the use cases, this should work well enough; but it
   484  	// won't detect an incompatible major-version change, which is a shame.
   485  	//
   486  	// TODO(cherylj) If the determination of version changes, we will
   487  	// need to also change the upgrade version checks in Run() that check
   488  	// if a major upgrade is allowed.
   489  	if context.chosen == version.Zero {
   490  		context.chosen = context.client
   491  	}
   492  	context.chosen = uploadVersion(context.chosen, context.tools)
   493  
   494  	builtTools, err := sync.BuildAgentTarball(buildAgent, &context.chosen, "upgrade")
   495  	if err != nil {
   496  		return errors.Trace(err)
   497  	}
   498  	defer os.RemoveAll(builtTools.Dir)
   499  
   500  	uploadToolsVersion := builtTools.Version
   501  	uploadToolsVersion.Number = context.chosen
   502  	toolsPath := path.Join(builtTools.Dir, builtTools.StorageName)
   503  	logger.Infof("uploading agent binary %v (%dkB) to Juju controller", uploadToolsVersion, (builtTools.Size+512)/1024)
   504  	f, err := os.Open(toolsPath)
   505  	if err != nil {
   506  		return errors.Trace(err)
   507  	}
   508  	defer f.Close()
   509  	os, err := series.GetOSFromSeries(builtTools.Version.Series)
   510  	if err != nil {
   511  		return errors.Trace(err)
   512  	}
   513  	additionalSeries := series.OSSupportedSeries(os)
   514  	uploaded, err := context.apiClient.UploadTools(f, uploadToolsVersion, additionalSeries...)
   515  	if err != nil {
   516  		return errors.Trace(err)
   517  	}
   518  	context.tools = uploaded
   519  	return nil
   520  }
   521  
   522  // validate chooses an upgrade version, if one has not already been chosen,
   523  // and ensures the tools list contains no entries that do not have that version.
   524  // If validate returns no error, the environment agent-version can be set to
   525  // the value of the chosen field.
   526  func (context *upgradeContext) validate() (err error) {
   527  	if context.chosen == version.Zero {
   528  		// No explicitly specified version, so find the version to which we
   529  		// need to upgrade. We find next available stable release to upgrade
   530  		// to by incrementing the minor version, starting from the current
   531  		// agent version and doing major.minor+1.patch=0.
   532  
   533  		// Upgrading across a major release boundary requires that the version
   534  		// be specified with --agent-version.
   535  		nextVersion := context.agent
   536  		nextVersion.Minor += 1
   537  		nextVersion.Patch = 0
   538  
   539  		newestNextStable, found := context.tools.NewestCompatible(nextVersion)
   540  		if found {
   541  			logger.Debugf("found a more recent stable version %s", newestNextStable)
   542  			context.chosen = newestNextStable
   543  		} else {
   544  			newestCurrent, found := context.tools.NewestCompatible(context.agent)
   545  			if found {
   546  				logger.Debugf("found more recent current version %s", newestCurrent)
   547  				context.chosen = newestCurrent
   548  			} else {
   549  				if context.agent.Major != context.client.Major {
   550  					return errors.New("no compatible tools available")
   551  				} else {
   552  					return errors.New("no more recent supported versions available")
   553  				}
   554  			}
   555  		}
   556  	} else {
   557  		// If not completely specified already, pick a single tools version.
   558  		filter := coretools.Filter{Number: context.chosen}
   559  		if context.tools, err = context.tools.Match(filter); err != nil {
   560  			return err
   561  		}
   562  		context.chosen, context.tools = context.tools.Newest()
   563  	}
   564  	if context.chosen == context.agent {
   565  		return errUpToDate
   566  	}
   567  
   568  	// Disallow major.minor version downgrades.
   569  	if context.chosen.Major < context.agent.Major ||
   570  		context.chosen.Major == context.agent.Major && context.chosen.Minor < context.agent.Minor {
   571  		// TODO(fwereade): I'm a bit concerned about old agent/CLI tools even
   572  		// *connecting* to environments with higher agent-versions; but ofc they
   573  		// have to connect in order to discover they shouldn't. However, once
   574  		// any of our tools detect an incompatible version, they should act to
   575  		// minimize damage: the CLI should abort politely, and the agents should
   576  		// run an Upgrader but no other tasks.
   577  		return errors.Errorf(downgradeErrMsg, context.agent, context.chosen)
   578  	}
   579  
   580  	return nil
   581  }
   582  
   583  // uploadVersion returns a copy of the supplied version with a build number
   584  // higher than any of the supplied tools that share its major, minor and patch.
   585  func uploadVersion(vers version.Number, existing coretools.List) version.Number {
   586  	vers.Build++
   587  	for _, t := range existing {
   588  		if t.Version.Major != vers.Major || t.Version.Minor != vers.Minor || t.Version.Patch != vers.Patch {
   589  			continue
   590  		}
   591  		if t.Version.Build >= vers.Build {
   592  			vers.Build = t.Version.Build + 1
   593  		}
   594  	}
   595  	return vers
   596  }