autoupdate

package
v0.0.6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 16, 2026 License: MIT Imports: 5 Imported by: 0

Documentation

Overview

Package autoupdate decides which devices in the fleet should be rolled forward, and when (Task 20331).

The decision is separated from the doing, and kept free of I/O, because the interesting part of an auto-updater is not the upgrade — pkg/executor/remote and pkg/executor/install already do that — but the restraint. An updater that simply asks every stale device to upgrade the moment it notices is not a feature, it is a scheduled outage: the fleet is what runs the work, and every device is unavailable for the minute it takes to restart.

So Plan enforces four rules, and each of them exists because the naive version of this feature breaks something real:

  1. Never interrupt running work. An upgrade restarts the agent, which kills whatever it is executing. A task that has been running for forty minutes is worth more than being current by an hour.

  2. Never take the whole fleet down at once. Upgrades are rolled out a few devices at a time, so a release that turns out to be bad strands a bounded number of machines — and so the fleet retains capacity to keep scheduling while the rest roll.

  3. Never act on a device the operator has taken out of service by hand. A cordon is a statement that a human is dealing with this machine, and rebooting it underneath them is exactly the wrong response.

  4. Never "upgrade" a device to something that is not an upgrade. Already current, newer than the target, or running an unparseable build: all three are left alone rather than forced, because forcing is how an automatic system turns one bad assumption into a fleet-wide rollback. Two more shapes of the same rule came from a hub running an unreleased build (Task 20371): a target that is not a release at all — the default target on such a hub is its own "dev+g…" version, which no device can download — and a release whose binaries speak an older protocol than the device already does. The second is judged by protocol before versions are compared, because a device on a dev build cannot be ordered against a release, and "cannot order" is not the reason it must be left alone.

Everything here is a pure function of its inputs, so the rules can be tested exhaustively without a device, a network or a clock.

Index

Constants

View Source
const DefaultMaxInFlight = 1

DefaultMaxInFlight is how many devices are upgraded at once when a policy does not say.

One. The conservative choice is right for the common fleet, which is small enough that "a few at a time" and "all of them" are the same thing — and an operator with three hundred edge devices who needs this to go faster is in a much better position to choose the number than this default is.

Variables

This section is empty.

Functions

This section is empty.

Types

type Device

type Device struct {
	ID   string
	Name string
	// Version is the build the device reports. Empty or the legacy placeholder
	// means it has not told us, which Plan treats as "leave alone".
	Version string
	// ProtocolVersion is the negotiated session version, used to tell a device
	// that can be upgraded remotely from one that must be done by hand.
	ProtocolVersion int
	// AgentProtocol is the protocol the agent advertised at hello — more than
	// ProtocolVersion when the device is newer than the hub. A target speaking
	// less than the larger of the two would move the device backwards.
	AgentProtocol int
	// Online is whether there is a live session to send the request on.
	Online bool
	// Busy is whether the device is running work right now.
	Busy bool
	// AdminHeld is whether an operator has cordoned or drained it.
	AdminHeld bool
	// Upgrading is whether this device has already been asked and has not yet
	// come back. It is what keeps MaxInFlight meaningful across sweeps.
	Upgrading bool
	// Channel is the update channel the device reports: "edge" lets it follow
	// the hub's own build, anything else is releases only (Task 20376).
	Channel string
	// Sequence is the device's build's place on main (Task 20380), zero when
	// it carries none. Its installer refuses anything earlier, so the planner
	// never asks for it.
	Sequence int
}

Device is what the planner needs to know about one executor. It is a flat snapshot rather than a live handle so that planning cannot race the fleet it is planning over.

type Hub added in v0.0.5

type Hub struct {
	Version  string
	Protocol int
	// Edge is what CI published of the hub's own build (Task 20376): nil when
	// the hub runs a release or a build no edge build can match, else whether
	// "edge:<commit>" exists and the protocol its manifest claims. It is what
	// "match the hub" means for a device on the edge channel.
	Edge *executor.EdgeBuild
}

Hub is what the planner needs to know about the control plane: the build it runs, which is the default target, and the protocol it speaks.

type Policy

type Policy struct {
	// Enabled turns automatic upgrades on. Off by default, and deliberately:
	// a hub that silently reboots edge devices because it was installed with a
	// helpful default is a hub nobody trusts with hardware.
	Enabled bool `json:"enabled"`

	// TargetVersion is the release every device should converge on. Empty
	// means the control plane's own build, which is the sane default — the
	// hub and its agents are two halves of one protocol, and "match the hub"
	// is the invariant an operator actually wants.
	//
	// A pinned tag is honoured as written, including when it is older than the
	// hub: staged rollouts and rollbacks both need the fleet to sit on a
	// version the control plane is not running.
	TargetVersion string `json:"target_version,omitempty"`

	// MaxInFlight bounds concurrent upgrades. Zero uses DefaultMaxInFlight.
	MaxInFlight int `json:"max_in_flight,omitempty"`
}

Policy is the operator's standing instruction for the fleet.

func (Policy) Resolve

func (p Policy) Resolve(hubVersion string) Policy

Resolve fills in the defaults, returning the policy as it will actually be applied. Callers render this rather than the stored value so the UI shows the effective setting rather than a blank that means something.

type Verdict

type Verdict struct {
	Device Device
	// Upgrade is whether to send the request now.
	Upgrade bool
	// Target is what to ask this device for when Upgrade is set. Per device,
	// because "match the hub" is the hub's edge build for a device on the edge
	// channel and is refused outright for one that is not.
	Target string
	// Reason explains the decision either way, in the operator's terms. It is
	// shown in the UI beside the device, so a fleet that is not converging can
	// be understood without reading logs.
	Reason string
}

Verdict is what the planner decided about one device.

func Plan

func Plan(p Policy, hub Hub, devices []Device) []Verdict

Plan decides what to do about each device, in a stable order.

It returns a verdict for every device rather than only the ones to act on, because "why is that machine still on the old build" is the question this feature generates and a list of the devices it skipped is the answer.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL