github.com/juju/juju@v0.0.0-20240327075706-a90865de2538/worker/upgrader/upgrader.go (about)

     1  // Copyright 2012, 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package upgrader
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"net/http"
    10  	"os"
    11  	"time"
    12  
    13  	"github.com/juju/errors"
    14  	jujuhttp "github.com/juju/http/v2"
    15  	"github.com/juju/names/v5"
    16  	"github.com/juju/version/v2"
    17  	"github.com/juju/worker/v3/catacomb"
    18  
    19  	"github.com/juju/juju/agent"
    20  	agenttools "github.com/juju/juju/agent/tools"
    21  	"github.com/juju/juju/api/agent/upgrader"
    22  	agenterrors "github.com/juju/juju/cmd/jujud/agent/errors"
    23  	"github.com/juju/juju/core/arch"
    24  	coreos "github.com/juju/juju/core/os"
    25  	coretools "github.com/juju/juju/tools"
    26  	"github.com/juju/juju/upgrades"
    27  	jujuversion "github.com/juju/juju/version"
    28  	"github.com/juju/juju/worker/gate"
    29  )
    30  
    31  const (
    32  	// shortDelay is the time we normally sleep for in the main loop
    33  	// when polling for changes to the model's version.
    34  	shortDelay = 5 * time.Second
    35  
    36  	// notEnoughSpaceDelay is how long we sleep when there's a new
    37  	// version of the agent that we need to download but there isn't
    38  	// enough available space to download and unpack it. Sleeping
    39  	// longer in that situation means we don't spam the log with disk
    40  	// space errors every 3 seconds, but still bring the message up
    41  	// regularly.
    42  	notEnoughSpaceDelay = time.Minute
    43  )
    44  
    45  // logger is here to stop the desire of creating a package level logger.
    46  // Don't do this, instead pass one through as config to the worker.
    47  type logger interface{}
    48  
    49  var _ logger = struct{}{}
    50  
    51  // Upgrader represents a worker that watches the state for upgrade
    52  // requests.
    53  type Upgrader struct {
    54  	catacomb catacomb.Catacomb
    55  	st       *upgrader.State
    56  	dataDir  string
    57  	tag      names.Tag
    58  	config   Config
    59  }
    60  
    61  // Config contains the items the worker needs to start.
    62  type Config struct {
    63  	Clock                       Clock
    64  	Logger                      Logger
    65  	State                       *upgrader.State
    66  	AgentConfig                 agent.Config
    67  	OrigAgentVersion            version.Number
    68  	UpgradeStepsWaiter          gate.Waiter
    69  	InitialUpgradeCheckComplete gate.Unlocker
    70  	CheckDiskSpace              func(string, uint64) error
    71  }
    72  
    73  // NewAgentUpgrader returns a new upgrader worker. It watches changes to the
    74  // current version of the current agent (with the given tag) and tries to
    75  // download the tools for any new version into the given data directory.  If
    76  // an upgrade is needed, the worker will exit with an UpgradeReadyError
    77  // holding details of the requested upgrade. The tools will have been
    78  // downloaded and unpacked.
    79  func NewAgentUpgrader(config Config) (*Upgrader, error) {
    80  	u := &Upgrader{
    81  		st:      config.State,
    82  		dataDir: config.AgentConfig.DataDir(),
    83  		tag:     config.AgentConfig.Tag(),
    84  		config:  config,
    85  	}
    86  	err := catacomb.Invoke(catacomb.Plan{
    87  		Site: &u.catacomb,
    88  		Work: u.loop,
    89  	})
    90  	if err != nil {
    91  		return nil, errors.Trace(err)
    92  	}
    93  	return u, nil
    94  }
    95  
    96  // Kill implements worker.Worker.Kill.
    97  func (u *Upgrader) Kill() {
    98  	u.catacomb.Kill(nil)
    99  }
   100  
   101  // Wait implements worker.Worker.Wait.
   102  func (u *Upgrader) Wait() error {
   103  	return u.catacomb.Wait()
   104  }
   105  
   106  // AllowedTargetVersion checks if targetVersion is too different from
   107  // curVersion to allow a downgrade.
   108  func AllowedTargetVersion(
   109  	curVersion version.Number,
   110  	targetVersion version.Number,
   111  ) bool {
   112  	// Don't allow downgrading from higher versions to version 1.x
   113  	if curVersion.Major >= 2 && targetVersion.Major == 1 {
   114  		return false
   115  	}
   116  	// Don't allow downgrading from higher major versions.
   117  	return curVersion.Major <= targetVersion.Major
   118  }
   119  
   120  func (u *Upgrader) loop() error {
   121  	logger := u.config.Logger
   122  	// Start by reporting current tools (which includes arch/os type, and is
   123  	// used by the controller in communicating the desired version below).
   124  	hostOSType := coreos.HostOSTypeName()
   125  	if err := u.st.SetVersion(u.tag.String(), toBinaryVersion(jujuversion.Current, hostOSType)); err != nil {
   126  		return errors.Annotatef(err, "cannot set agent version for %q", u.tag.String())
   127  	}
   128  
   129  	// We do not commence any actions until the upgrade-steps worker has
   130  	// confirmed that all steps are completed for getting us upgraded to the
   131  	// version that we currently on.
   132  	if u.config.UpgradeStepsWaiter != nil {
   133  		select {
   134  		case <-u.config.UpgradeStepsWaiter.Unlocked():
   135  		case <-u.catacomb.Dying():
   136  			return u.catacomb.ErrDying()
   137  		}
   138  	}
   139  
   140  	if u.config.UpgradeStepsWaiter == nil {
   141  		u.config.Logger.Infof("no waiter, upgrader is done")
   142  		return nil
   143  	}
   144  
   145  	versionWatcher, err := u.st.WatchAPIVersion(u.tag.String())
   146  	if err != nil {
   147  		return errors.Trace(err)
   148  	}
   149  
   150  	var retry <-chan time.Time
   151  	for {
   152  		select {
   153  		case <-retry:
   154  		case <-u.catacomb.Dying():
   155  			return u.catacomb.ErrDying()
   156  		case _, ok := <-versionWatcher.Changes():
   157  			if !ok {
   158  				return errors.New("version watcher closed")
   159  			}
   160  		}
   161  
   162  		wantVersion, err := u.st.DesiredVersion(u.tag.String())
   163  		if err != nil {
   164  			return err
   165  		}
   166  		logger.Infof("desired agent binary version: %v", wantVersion)
   167  
   168  		// If we have a desired version of Juju without the build number,
   169  		// i.e. it is not a user compiled version, reset the build number of
   170  		// the current version to remove the Jenkins build number.
   171  		// We don't care about the build number when checking for upgrade.
   172  		haveVersion := jujuversion.Current
   173  		if wantVersion.Build == 0 {
   174  			haveVersion.Build = 0
   175  		}
   176  
   177  		if wantVersion == haveVersion {
   178  			u.config.InitialUpgradeCheckComplete.Unlock()
   179  			continue
   180  		} else if !AllowedTargetVersion(haveVersion, wantVersion) {
   181  			logger.Infof("downgrade from %v to %v is not possible", haveVersion, wantVersion)
   182  			u.config.InitialUpgradeCheckComplete.Unlock()
   183  			continue
   184  		}
   185  		direction := "upgrade"
   186  		if wantVersion.Compare(haveVersion) == -1 {
   187  			direction = "downgrade"
   188  		}
   189  		logger.Infof("%s requested from %v to %v", direction, haveVersion, wantVersion)
   190  
   191  		// Check if tools have already been downloaded.
   192  		wantVersionBinary := toBinaryVersion(wantVersion, hostOSType)
   193  		if u.toolsAlreadyDownloaded(wantVersionBinary) {
   194  			return u.newUpgradeReadyError(haveVersion, wantVersionBinary, hostOSType)
   195  		}
   196  
   197  		// Check if tools are available for download.
   198  		wantToolsList, err := u.st.Tools(u.tag.String())
   199  		if err != nil {
   200  			// Not being able to lookup Tools is considered fatal
   201  			return err
   202  		}
   203  		// The worker cannot be stopped while we're downloading
   204  		// the tools - this means that even if the API is going down
   205  		// repeatedly (causing the agent to be stopped), as long
   206  		// as we have got as far as this, we will still be able to
   207  		// upgrade the agent.
   208  		delay := shortDelay
   209  		for _, wantTools := range wantToolsList {
   210  			if err := u.checkForSpace(); err != nil {
   211  				logger.Errorf("%s", err.Error())
   212  				delay = notEnoughSpaceDelay
   213  				break
   214  			}
   215  			err = u.ensureTools(wantTools)
   216  			if err == nil {
   217  				return u.newUpgradeReadyError(haveVersion, wantTools.Version, hostOSType)
   218  			}
   219  			logger.Errorf("failed to fetch agent binaries from %q: %v", wantTools.URL, err)
   220  		}
   221  		retry = u.config.Clock.After(delay)
   222  	}
   223  }
   224  
   225  func toBinaryVersion(vers version.Number, osType string) version.Binary {
   226  	outVers := version.Binary{
   227  		Number:  vers,
   228  		Arch:    arch.HostArch(),
   229  		Release: osType,
   230  	}
   231  	return outVers
   232  }
   233  
   234  func (u *Upgrader) toolsAlreadyDownloaded(wantVersion version.Binary) bool {
   235  	_, err := agenttools.ReadTools(u.dataDir, wantVersion)
   236  	return err == nil
   237  }
   238  
   239  func (u *Upgrader) newUpgradeReadyError(haveVersion version.Number, newVersion version.Binary, osType string) *agenterrors.UpgradeReadyError {
   240  	return &agenterrors.UpgradeReadyError{
   241  		OldTools:  toBinaryVersion(haveVersion, osType),
   242  		NewTools:  newVersion,
   243  		AgentName: u.tag.String(),
   244  		DataDir:   u.dataDir,
   245  	}
   246  }
   247  
   248  func (u *Upgrader) ensureTools(agentTools *coretools.Tools) error {
   249  	u.config.Logger.Infof("fetching agent binaries from %q", agentTools.URL)
   250  	// The reader MUST verify the tools' hash, so there is no
   251  	// need to validate the peer. We cannot anyway: see http://pad.lv/1261780.
   252  	client := jujuhttp.NewClient(jujuhttp.WithSkipHostnameVerification(true))
   253  	resp, err := client.Get(context.TODO(), agentTools.URL)
   254  	if err != nil {
   255  		return err
   256  	}
   257  	defer func() { _ = resp.Body.Close() }()
   258  	if resp.StatusCode != http.StatusOK {
   259  		return fmt.Errorf("bad HTTP response: %v", resp.Status)
   260  	}
   261  	err = agenttools.UnpackTools(u.dataDir, agentTools, resp.Body)
   262  	if err != nil {
   263  		return fmt.Errorf("cannot unpack agent binaries: %v", err)
   264  	}
   265  	u.config.Logger.Infof("unpacked agent binaries %s to %s", agentTools.Version, u.dataDir)
   266  	return nil
   267  }
   268  
   269  func (u *Upgrader) checkForSpace() error {
   270  	u.config.Logger.Debugf("checking available space before downloading")
   271  	err := u.config.CheckDiskSpace(u.dataDir, upgrades.MinDiskSpaceMib)
   272  	if err != nil {
   273  		return errors.Trace(err)
   274  	}
   275  	err = u.config.CheckDiskSpace(os.TempDir(), upgrades.MinDiskSpaceMib)
   276  	if err != nil {
   277  		return errors.Trace(err)
   278  	}
   279  	return nil
   280  }