upgrade

package
v0.3.1 Latest Latest
Warning

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

Go to latest
Published: Jul 2, 2026 License: MIT Imports: 23 Imported by: 0

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

Constants

This section is empty.

Variables

This section is empty.

Functions

func Comparable

func Comparable(v string) bool

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 Download

func Download(ctx context.Context, url, dir string) (string, error)

Download streams rel's tarball to a temp file in dir and returns its path.

func ExtractBinary

func ExtractBinary(tarball, dest string) error

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

func ListenerFromFD(fd int, name string) (net.Listener, error)

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

func Newer(candidate, current string) bool

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

func SmokeTest(bin, wantVer string) error

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

func VerifyChecksum(path, wantName string, checksums []byte) error

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

func LoadConfig(current string) (Config, error)

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

func (c Config) AutoEnabled() bool

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

func (co *Coordinator) HandleUpgrade(req protocol.Request, send func(protocol.Response) error)

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.

func NewSource

func NewSource(c Config) Source

NewSource picks the HTTP-mirror source when a base URL is configured, otherwise the GitHub source.

type Staged

type Staged struct {
	Version string
	Path    string
}

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

func Stage(ctx context.Context, src Source, rel Release, installPath string) (Staged, error)

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

type Window struct {
	StartMin int
	EndMin   int
}

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

func ParseWindow(s string) (*Window, error)

ParseWindow parses "HH:MM-HH:MM" (24-hour, local time).

func (*Window) Contains

func (w *Window) Contains(t time.Time) bool

Contains reports whether time t (its local hour:minute) falls in the window.

Jump to

Keyboard shortcuts

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