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 }