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