Documentation
¶
Overview ¶
Package upgrade implements agentenv's zero-downtime self-upgrade: a daemon polls a release source, verifies a newer build, and hot-swaps its own binary via an in-place execve that preserves the repo flock, the listening sockets, and — crucially — every running agent child process. See handoff.go for the re-exec mechanics and schedule.go for the polling/window coordinator.
Index ¶
- func Comparable(v string) bool
- func Download(ctx context.Context, url, dir string) (string, error)
- func ExtractBinary(tarball, dest string) error
- func IsReexec() bool
- func ListenerFromFD(fd int, name string) (net.Listener, error)
- func Newer(candidate, current string) bool
- func SmokeTest(bin, wantVer string) error
- func VerifyChecksum(path, wantName string, checksums []byte) error
- type Config
- type Coordinator
- type ProcManifest
- type Release
- type ResumeState
- type Source
- type Staged
- type Window
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Comparable ¶
Comparable reports whether v looks like a real semantic-version release tag we can order against another. It rejects the non-release version strings main.resolveVersion can produce — "dev", a bare VCS commit (no dots), and any "-dirty" working-tree build — so automatic upgrades never fire from an unversioned local build. A manual `ctl upgrade --version X` bypasses this.
func ExtractBinary ¶
ExtractBinary reads the gzip'd tarball and writes the "agentenv" entry to dest with mode 0755. Other archive members (docs) are skipped.
func IsReexec ¶
func IsReexec() bool
IsReexec reports whether this process was started by a hot-upgrade handoff.
func ListenerFromFD ¶
ListenerFromFD rebuilds a net.Listener from an inherited fd. net.FileListener dups the fd and registers it with the runtime netpoller; the passed os.File wrapper is closed afterward (the listener holds its own dup). It does NOT re-bind, so no "address already in use" and no listen-backlog gap.
func Newer ¶
Newer reports whether candidate is a strictly higher version than current. Both accept an optional leading "v". If current is not comparable we treat any comparable candidate as newer (so a dev build can still be told to move to a real release when explicitly targeted). Pre-release suffixes after "-" are ignored for the numeric comparison.
func SmokeTest ¶
SmokeTest runs `<bin> version` and confirms it exits 0 and reports wantVer. This is the "does the new build actually run on this box" gate before we commit to the execve — a broken/mismatched-arch binary is caught here while the old daemon is still healthy. `version` opens no repo, takes no lock, and starts no sandbox, so running it is side-effect free.
func VerifyChecksum ¶
VerifyChecksum recomputes the SHA256 of the tarball at path and checks it against the checksums.txt entry for wantName — the RELEASE artifact name (e.g. agentenv_0.4.0_linux_amd64.tar.gz), NOT path's base name, which is a throwaway temp file the download landed in. checksums.txt is the standard `"<hex> <filename>"` format. A mismatch or missing entry is a hard error — this is the authenticity gate before we ever execute the downloaded code.
Types ¶
type Config ¶
type Config struct {
RepoSlug string // GitHub "owner/name" (used only when BaseURL == "")
BaseURL string // mirror base URL; empty selects the GitHub source
Interval time.Duration // background poll cadence; 0 disables polling
Window *Window // maintenance window; nil = upgrade any time
Current string // the running binary's version (from cli.Run)
Poll bool // whether background polling is enabled
}
Config is the resolved upgrade configuration for one daemon.
func LoadConfig ¶
LoadConfig reads the AGENTENV_UPDATE_* environment into a Config. current is the running version string (main.resolveVersion, threaded through cli.Run). Invalid interval/window values are reported so a typo doesn't silently disable upgrades.
func (Config) AutoEnabled ¶
AutoEnabled reports whether the background poller should run: polling is on AND the running version is a real comparable release (so we never auto-upgrade away from a dev/commit build — that path is manual-only).
type Coordinator ¶
type Coordinator struct {
// contains filtered or unexported fields
}
Coordinator owns the upgrade lifecycle for one daemon: it polls the source, stages verified builds, and performs the in-place re-exec — either automatically inside the maintenance window or on demand via HandleUpgrade (the `ctl upgrade` / MCP path). It implements api.UpgradeHandler.
func NewCoordinator ¶
func NewCoordinator(cfg Config, r *repo.Repo, gate *api.Gate, installPath, root string, unixLn, httpLn net.Listener, lock *repo.Lock) *Coordinator
NewCoordinator builds a coordinator. installPath must be the resolved binary path (os.Readlink("/proc/self/exe") captured at startup); unixLn/httpLn are the daemon's own listeners (so their fds can be handed off); lock is the held repo flock (so its fd can be inherited). root is AGENTENV_ROOT (for the proc manifest scratch file).
func (*Coordinator) Gate ¶
func (co *Coordinator) Gate() *api.Gate
Gate returns the shared Gate the coordinator serializes upgrades against — the daemon passes it to api.ServeListener so mutating ops and a hot upgrade take the same writer lock.
func (*Coordinator) HandleUpgrade ¶
HandleUpgrade implements api.UpgradeHandler for the out-of-band `upgrade` op. Manual upgrades always bypass the maintenance window (the operator asked for it now). On success it re-execs and never returns; send has already delivered an ack frame that the client reads before the connection dies.
func (*Coordinator) Run ¶
func (co *Coordinator) Run(ctx context.Context)
Run is the background poller. It polls the source every cfg.Interval, stages a newer build when found, and checks once a minute whether a staged build may be applied (in-window). Returns when ctx is cancelled. Only started when cfg.AutoEnabled(); a successful handoff execs and never returns.
type ProcManifest ¶
type ProcManifest struct {
PID int `json:"pid"`
Args []string `json:"args"`
Started int64 `json:"started"` // unix nanoseconds
Log string `json:"log"`
}
ProcManifest is one adopted background process, serialized across the execve so the new image can re-register it (see repo.AdoptProc). The child itself keeps running untouched — this is only bookkeeping so `ps`/`kill`/`checkout` still see it.
type Release ¶
type Release struct {
Version string // canonical, with leading "v" (e.g. "v0.4.0")
TarballURL string
ChecksumsURL string
}
Release identifies one downloadable build: the version and the URLs of its tarball (for this GOOS/GOARCH) and the checksums file that authenticates it.
type ResumeState ¶
type ResumeState struct {
LockFD int
SockFD int
HTTPFD int // -1 when the old daemon had no HTTP listener
ProcsPath string
Procs []ProcManifest
}
ResumeState is the inherited state parsed from the handoff env on the new image's side.
func ReadResumeState ¶
func ReadResumeState() (ResumeState, error)
ReadResumeState parses the handoff env (fds + proc manifest path) and loads the manifest file. The caller is responsible for os.Remove(ProcsPath) once the procs are adopted.
type Source ¶
type Source interface {
// Latest returns the newest available release.
Latest(ctx context.Context) (Release, error)
// Resolve returns a specific version (for `ctl upgrade --version X`).
Resolve(ctx context.Context, version string) (Release, error)
}
Source resolves releases. Two implementations: GitHub (default) and a plain HTTP mirror (AGENTENV_UPDATE_URL) for private/air-gapped deployments.
type Staged ¶
Staged is a downloaded, checksum-verified, smoke-tested binary sitting on the SAME filesystem as the install path, ready for an atomic rename during handoff.
func Stage ¶
Stage runs the full download → checksum → extract → smoke-test pipeline for rel, leaving the verified binary at a temp path next to installPath (so a later os.Rename onto installPath is atomic — same filesystem). All of this happens WITHOUT touching the running daemon: no gate, no lock, no execve. A failure anywhere leaves the daemon untouched and returns an error; the caller just retries next poll.
type Window ¶
Window is a daily maintenance window in local-time minutes-of-day. When Start <= End it is a same-day interval [Start, End); when Start > End it wraps past midnight.
func ParseWindow ¶
ParseWindow parses "HH:MM-HH:MM" (24-hour, local time).