install

package
v0.0.5 Latest Latest
Warning

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

Go to latest
Published: Oct 12, 2026 License: MIT Imports: 22 Imported by: 0

Documentation

Overview

Package install turns an enrollment bundle into a running, restart-on-boot, hardened cloop executor agent on an edge device.

Enrollment (Task 20158) ends where the real work begins. It hands the operator a `cloop executor agent --server … --token …` string and stops there, so every device costs the same manual sequence: get the binary onto the box, decide which user runs it, keep the token out of the shell history and out of `ps`, arrange for a restart after a power cut, and put the logs somewhere. Done by hand on the tenth device, at least one of those steps is done differently — and the one that gets skipped is usually the credential's file mode.

So this package makes the whole shape a single artifact. The important properties are structural rather than clever:

  • The enrollment token never appears in ExecStart. It is written to a 0600 file that only the service user can read, and the command line carries a path. Anyone on the device can read a unit file, and `systemctl show` and `ps` will both print an argv; neither may be a place where a credential is legible. See credentialsArtifact.
  • The service runs as a dedicated non-login user with an empty capability bounding set, a read-only filesystem, and no ability to gain privilege. A harness that a compromised control plane persuades to run `sudo` finds nothing there to escalate into.
  • The SPKI pin from Task 20166 is baked into the unit, so a device that reboots keeps verifying the hub's identity rather than falling back to whatever DNS says on the day.

Rendering is separated from applying on purpose: BuildPlan is pure, so --dry-run is the same code path as a real install minus the writes, and the generated unit can be tested without a systemd on the machine running the tests.

Index

Constants

View Source
const (
	DefaultServiceName = "cloop-executor"
	DefaultUser        = "cloop-executor"
	DefaultBinaryPath  = "/usr/local/bin/cloop"
	DefaultStateRoot   = "/var/lib"
	DefaultUnitDir     = "/etc/systemd/system"
	DefaultInitDir     = "/etc/init.d"
	DefaultImage       = "ghcr.io/blechschmidt/cloop:latest"

	// CredentialFileMode is the mode of the file holding enrollment
	// material. It is the single most important number in this package:
	// everything else is defence in depth around the fact that this file is
	// a bearer credential.
	CredentialFileMode os.FileMode = 0o600

	// StateDirMode keeps the credential's directory unreadable to other
	// local users, so a 0600 file cannot be reached by a directory listing
	// plus a race on a later re-write.
	StateDirMode os.FileMode = 0o700

	// SystemDirMode is for shared system directories (/etc/systemd/system,
	// /etc/init.d). They hold no secret and every other service on the box
	// traverses them, so creating one at StateDirMode would break far more
	// than it protected.
	SystemDirMode os.FileMode = 0o755

	// UnitFileMode is world-readable on purpose: systemd reads it as root,
	// operators read it to debug, and it contains no secrets — which is the
	// invariant credentialsArtifact exists to preserve.
	UnitFileMode os.FileMode = 0o644

	// ScriptFileMode is for the generated init script, which must be
	// executable by root and readable for debugging.
	ScriptFileMode os.FileMode = 0o755

	// BinaryMode is for the cloop binary an upgrade installs: executable by
	// everyone, writable only by its owner. Asserted explicitly rather than
	// inherited, because CreateTemp makes 0600 and a binary left at 0600 is a
	// service that fails to start as its own unprivileged user — the exact
	// failure mode an upgrade must not introduce.
	BinaryMode os.FileMode = 0o755
)

Defaults for an install that the operator did not otherwise specify. They are deliberately conventional: an operator debugging a device at 3am should find the unit and the state where the FHS says they will be.

View Source
const ChannelEnv = "CLOOP_UPDATE_CHANNEL"

ChannelEnv is the environment variable the channel drop-in sets, and the agent reads.

View Source
const MaxSettleSeconds = 600

MaxSettleSeconds bounds the settle time a request may ask for: the helper's unit gives the whole upgrade TimeoutStartSec=15min, and a settle window that outlived it would have systemd kill the helper before it could roll back.

View Source
const PacketFilterHint = "the agent holds no CAP_NET_ADMIN, so a sandbox with a firewall is refused " +
	"on this device; re-run with --packet-filter to grant it"

PacketFilterHint is what an upgrade says about a device that cannot install sandbox firewalls, naming the flag that changes that.

View Source
const ProbeTimeout = 20 * time.Second

ProbeTimeout bounds one execution of a candidate binary.

Generous relative to what `cloop version` costs (milliseconds) because the failure it guards against is a binary that never returns at all, not a slow one. A device under memory pressure can take seconds to fault in a 40 MB executable, and killing a good binary for being slow would be the same class of mistake as installing a bad one.

View Source
const RequestMaxAge = 15 * time.Minute

RequestMaxAge is how old a request may be and still be carried out. A request left behind — by a crash, or a device switched off mid-flight — is not an instruction to upgrade at the next boot; the hub asks again.

View Source
const UpgradeRequestFileName = "upgrade-request.json"

UpgradeRequestFileName is the request's file name in the agent's state dir.

Variables

View Source
var ErrBinaryUnusable = errors.New("install: the staged binary failed verification")

ErrBinaryUnusable means the staged file is not a cloop binary this machine can run: it failed to execute, exited non-zero, timed out, or produced output that does not identify it.

Deliberately *not* overridable by --force. --force means "replace even though the bytes are identical"; it has never meant "install something I have been shown is broken", and widening it to mean that would delete the only check standing between a truncated download and an offline device. There is no flag for this, because there is no case for it: a binary that will not run on this machine will not run after being renamed into place either.

View Source
var ErrDowngrade = errors.New("install: refusing to install an older build")

ErrDowngrade means the staged binary is older than what is installed, speaks an older executor protocol than the installed binary does, or speaks a protocol the control plane no longer accepts.

This one *is* overridable with --force, because a deliberate rollback is a real operation — an operator backing out a bad release needs it, and refusing outright would send them to `cp` and `systemctl`, bypassing every other check in this file.

View Source
var ErrNoRequest = errors.New("install: no upgrade request is waiting")

ErrNoRequest reports that no request was filed.

View Source
var ErrNotInstalled = errors.New("install: no existing agent install found")

ErrNotInstalled is returned when there is nothing to upgrade.

A sentinel because the CLI distinguishes it: it is the one failure with a different next step (install, not retry), and a caller should not have to match on message text to find out.

View Source
var ErrRollback = errors.New("install: refusing to roll this device back to a build earlier on main")

ErrRollback means the staged build is earlier on main than the installed one: its sequence is lower, or it carries none while the installed binary does (Task 20380). Every rollback is also an ErrDowngrade — errors.Is matches both — but it is the one downgrade Force does not override.

The sequence is the only ordering two builds of main have, and before it the guard below could not order them at all: both report "dev+g<sha>", so a compromised agent, or a compromised hub, could have the root helper install any older signed edge build at the same protocol — one from before a fix to the agent or to the helper itself. So only UpgradeOptions.AllowRollback overrides this, and only a local root invocation's --force sets that (`cloop executor agent install --upgrade --force`). The upgrade request the agent files, and the upgrade frame the hub sends an agent running as root, carry a force flag that cannot reach it.

Functions

func BootstrapScript

func BootstrapScript(p BootstrapParams) string

BootstrapScript renders the /install.sh body.

func ChannelDropIn added in v0.0.5

func ChannelDropIn(s Spec, c provenance.Channel) string

ChannelDropIn renders the drop-in that puts the agent on channel c. Only the edge channel has one: the stable channel is the absence of the file.

func ComposeFragment

func ComposeFragment(s Spec) string

ComposeFragment renders the same deployment as a compose service.

func ContainerFragment

func ContainerFragment(s Spec) string

ContainerFragment renders both the `podman run` command and the compose service, with the run command first because it is the thing an operator on a single device actually pastes.

func FileUpgradeRequest added in v0.0.5

func FileUpgradeRequest(s Spec, req UpgradeRequest) error

FileUpgradeRequest writes req where the helper's path unit is watching. It writes beside and renames, so the helper never reads half a request.

func InitScript

func InitScript(s Spec) string

InitScript renders the POSIX supervisor script for spec s.

func PacketFilterDropIn added in v0.0.5

func PacketFilterDropIn(s Spec) string

PacketFilterDropIn renders the drop-in that grants the agent CAP_NET_ADMIN and netlink sockets (Task 20352), so it can install the IP firewall a virtual executor's sandboxes carry.

func PodmanRun

func PodmanRun(s Spec) string

PodmanRun renders the run command. It works unchanged under `docker run`; podman is named first because a rootless engine is the better fit for an edge device.

func SystemdUnit

func SystemdUnit(s Spec) string

SystemdUnit renders the service unit for spec s.

s is expected to have been through Normalize; BuildPlan is the supported entry point and does that for you.

func UpgradeHelperPathUnit added in v0.0.5

func UpgradeHelperPathUnit(s Spec) string

UpgradeHelperPathUnit renders the path unit that starts the helper when a request appears.

func UpgradeHelperService added in v0.0.5

func UpgradeHelperService(s Spec) string

UpgradeHelperService renders the root oneshot that carries out a request.

Types

type Artifact

type Artifact struct {
	// Path is absolute on the device (before any Installer.Root prefix).
	Path string
	// Mode is the file's permission bits.
	Mode os.FileMode
	// Content is the file body.
	Content string
	// Secret marks content that must never be logged, printed by --dry-run,
	// or written world-readable.
	Secret bool
	// Owned marks a file that should be chowned to the service user.
	Owned bool
}

Artifact is one file the installer writes.

type BinaryIdentity added in v0.0.2

type BinaryIdentity struct {
	// Version is the build version it reported, e.g. "v0.1.0" or
	// "dev+g4f7b5bc". Never empty for a successful probe.
	Version string
	// Protocol and MinProtocol are the executor-agent protocol versions it
	// speaks. Zero means it did not report them — a build older than the
	// machine-readable version report. Zero is "unknown", never "none": a
	// caller must not read it as "speaks no protocol".
	Protocol, MinProtocol int
	// OS and Arch are the platform it was built for, empty when unreported.
	OS, Arch string
	// Commit is the full commit it says it was built from, and Sequence that
	// commit's first-parent position on main (Task 20380). Empty and zero for
	// a build that does not report them — a developer's unstamped build, and
	// every build made before builds were stamped with a sequence. Zero is
	// "carries none", never "position zero".
	Commit   string
	Sequence int
	// Structured records that the machine-readable form was available, which
	// is what makes the protocol fields meaningful.
	Structured bool
}

BinaryIdentity is what a candidate binary said about itself.

func (BinaryIdentity) String added in v0.0.2

func (id BinaryIdentity) String() string

String renders an identity for an operator-facing message.

type BootstrapParams

type BootstrapParams struct {
	// Server is the WebSocket URL agents dial, e.g.
	// wss://cloop.example.com/api/executors/connect.
	Server string
	// Pin is the hub's SPKI fingerprint, or "" when the hub has no
	// configured certificate. An empty pin makes the script say so rather
	// than quietly install an unpinned agent.
	Pin string
	// ServiceName overrides the installed unit's name.
	ServiceName string

	// HubVersion and HubProtocol are the build the hub runs and the executor
	// protocol it speaks (Task 20421). The script refuses to install an agent
	// that speaks an older protocol — one it downloads, one already on the
	// device, or CLOOP_BIN — because such an agent connects and is then
	// refused everything the hub needs the newer protocol for. A HubProtocol
	// of zero leaves the check out.
	HubVersion  string
	HubProtocol int
	// EdgeCommit is the full commit of the hub's own build when CI signed and
	// published it on the edge channel: what `--channel edge` installs. Empty
	// when there is none, and EdgeUnavailable then says why, for the script's
	// refusal of --channel edge.
	EdgeCommit      string
	EdgeUnavailable string
}

BootstrapParams are the deployment-specific values baked into the script.

type Dir

type Dir struct {
	Path string
	Mode os.FileMode
}

Dir is a directory the install must exist, with the mode it must carry.

The mode is per-directory rather than one constant because these fall into two classes with opposite requirements: a state directory holding a credential must be 0700, while /etc/systemd/system must stay traversable by everything on the box. Creating the latter at 0700 — which a single MkdirAll(…, 0700) over a missing path would do — breaks unrelated services.

type Installer

type Installer struct {
	// Root prefixes every path. Empty means the real filesystem root.
	// A non-empty Root also disables command execution entirely: a staged
	// install describes a device that is not this machine, so running
	// systemctl or useradd against this one would be wrong even if it
	// happened to work.
	Root string

	// Run executes a command. Nil uses exec.Command. Ignored when Root is
	// set.
	Run Runner

	// Exec runs a candidate cloop binary during upgrade verification. Nil
	// uses exec.CommandContext; a staged installer with no Exec skips
	// verification rather than running a binary built for another machine.
	Exec Prober

	// Logf receives progress messages. Nil discards them.
	Logf func(format string, args ...any)
}

Installer applies and reverses plans.

The zero value writes to the real filesystem and runs real commands, which is what the CLI wants; tests set Root and Run.

func (*Installer) Apply

func (in *Installer) Apply(p Plan) error

Apply materialises the plan.

func (*Installer) DeviceChannel added in v0.0.5

func (in *Installer) DeviceChannel(s Spec) (provenance.Channel, error)

DeviceChannel reports the channel the agent's unit puts it on, as root sees it: systemd's merged Environment= for the unit, so an operator's own override counts as much as the drop-in this package writes. A staged installer, which runs no commands, reads the drop-in directory instead.

It never reads anything the agent's user can write. That is what makes it the helper's answer to "may this device install an edge build", rather than the request's.

func (*Installer) HelperInstalled added in v0.0.5

func (in *Installer) HelperInstalled(s Spec) bool

HelperInstalled reports whether the device has the remote-upgrade helper.

func (*Installer) Uninstall

func (in *Installer) Uninstall(spec Spec, out Output, purgeState bool) error

Uninstall reverses an install. It is idempotent: running it twice, or running it on a device that was never installed, succeeds quietly.

The credential is removed first and the state directory last, so an interrupted uninstall never leaves a live token behind in a directory it then failed to delete.

func (*Installer) Upgrade

func (in *Installer) Upgrade(spec Spec, out Output, opts UpgradeOptions) (UpgradeResult, error)

Upgrade replaces the service binary and restarts the service.

It reports what it did rather than only whether it succeeded — see UpgradeResult — because "nothing needed doing" and "the binary was replaced but the service was stopped" are both successes that an operator must be able to tell apart from "the device is now running the new build".

type Output

type Output string

Output selects which supervision system the plan targets.

const (
	// OutputSystemd emits a hardened .service unit. The default, because it
	// is what an edge device running a mainstream Linux already has.
	OutputSystemd Output = "systemd"
	// OutputDocker emits a `podman run` command and an equivalent compose
	// fragment, for a device whose only supervision is a container engine.
	OutputDocker Output = "docker"
	// OutputShell emits a POSIX init script, for BusyBox/OpenRC devices and
	// anything else without systemd.
	OutputShell Output = "shell"
)

func ParseOutput

func ParseOutput(s string) (Output, error)

ParseOutput validates an --output value.

type PacketFilterChange added in v0.0.5

type PacketFilterChange int

PacketFilterChange is what an upgrade does to the packet-filter grant.

const (
	// PacketFilterKeep leaves the grant as the device has it.
	PacketFilterKeep PacketFilterChange = iota
	// PacketFilterGrant writes the drop-in, so the agent can install sandbox
	// firewalls.
	PacketFilterGrant
	// PacketFilterWithdraw removes it.
	PacketFilterWithdraw
)

type Plan

type Plan struct {
	Output Output
	Spec   Spec

	// Dirs are created before Artifacts are written.
	Dirs []Dir

	// Artifacts are the files to write.
	Artifacts []Artifact

	// Display is the operator-facing rendering for --dry-run. It never
	// contains credential material.
	Display string

	// Next lists the follow-up steps Apply performs (or that the operator
	// must perform when the plan targets a system this machine is not).
	Next []string

	// Remove lists files an earlier install may have left that this plan must
	// not keep — today, the packet-filter drop-in of a device now installed
	// without it. Absent files are fine: removing is about the state after.
	Remove []string
}

Plan is everything an install would do, computed without touching anything.

func BuildPlan

func BuildPlan(spec Spec, out Output) (Plan, error)

BuildPlan renders everything an install needs, without writing anything.

type Prober added in v0.0.2

type Prober func(ctx context.Context, path string, args ...string) ([]byte, error)

Prober executes a candidate cloop binary and returns its stdout.

Separate from Runner, and not merely because it returns output. Runner shells out to the *system* — systemctl, useradd — and is disabled entirely for a staged installer, since a staged install describes a machine that is not this one. A Prober runs a file the caller explicitly named and asks it what it is, which is a different act with a different safety argument: it is how Upgrade refuses to replace a working agent with a truncated download. See verify.go.

type RemoteUpgradeChange added in v0.0.5

type RemoteUpgradeChange int

RemoteUpgradeChange is what an upgrade does to the remote-upgrade helper.

const (
	// RemoteUpgradeKeep leaves the helper as the device has it.
	RemoteUpgradeKeep RemoteUpgradeChange = iota
	// RemoteUpgradeGrant installs and arms it.
	RemoteUpgradeGrant
	// RemoteUpgradeWithdraw disarms and removes it.
	RemoteUpgradeWithdraw
)

type Runner

type Runner func(name string, args ...string) error

Runner executes a system command. Tests substitute a recorder.

type Spec

type Spec struct {
	// ServiceName names the unit, the container, the init script, the
	// system user's home and the state directory. One name so an operator
	// greps for one string.
	ServiceName string

	// User and Group own the credential file and run the process. Empty
	// defaults to ServiceName.
	User  string
	Group string

	// BinaryPath is the cloop binary on the device.
	BinaryPath string

	// StateDir holds the credential file, the agent's long-lived identity,
	// and (by default) the workload root. Empty defaults to
	// /var/lib/<ServiceName>.
	StateDir string

	// UnitDir is where the systemd unit is written. Empty defaults to
	// /etc/systemd/system.
	UnitDir string

	// InitDir is where the OutputShell init script is written. Empty defaults
	// to /etc/init.d.
	//
	// It exists for the same reason UnitDir does. Without it the shell output
	// was the one artifact in this package with no redirectable destination,
	// which made every OutputShell path untestable except against the real
	// /etc/init.d: as root a test would overwrite the host's own service
	// script, and as anyone else it failed outright.
	InitDir string

	// CredentialsFile is the 0600 file the enrollment material is written
	// to. Empty defaults to <StateDir>/enrollment.
	//
	// There is deliberately no way to put the token on the command line
	// instead: a flag that inlines a credential into ExecStart is a flag
	// that will be used, and the resulting unit is world-readable.
	CredentialsFile string

	// AgentCredential is where the agent persists the long-lived identity
	// it receives in exchange for the enrollment token. Empty defaults to
	// <StateDir>/agent.json.
	AgentCredential string

	// WorkDirRoot confines every workload on the device. Empty defaults to
	// <StateDir>/work.
	WorkDirRoot string

	// Server is the control-plane WebSocket URL the agent dials.
	Server string

	// Pin is the hub's SPKI fingerprint ("sha256:<base64>", comma-separated
	// to stage a key rotation). Empty means ordinary PKI verification, which
	// is a real downgrade — see remote.Bundle.
	Pin string

	// Bundle is the encoded enrollment bundle (server + token + pin in one
	// blob). Either this or Token must be set for a first install; both are
	// empty when re-installing over an agent that already holds a
	// credential.
	Bundle string

	// Token is a bare enrollment token, for an operator who has one without
	// a bundle.
	Token string

	// RootCAFile is a PEM bundle to trust in addition to the system store,
	// for a hub behind a private CA.
	RootCAFile string

	// MaxConcurrent bounds simultaneous workloads. Zero lets the agent
	// choose (number of CPUs).
	MaxConcurrent int

	// Labels are scheduler selectors baked into the unit.
	Labels map[string]string

	// Image is the container image for OutputDocker.
	Image string

	// NoStart installs the unit without enabling or starting it, for
	// golden-image builds where the device must not enroll until first boot.
	NoStart bool

	// PacketFilter grants the agent what it needs to install a virtual
	// executor's IP firewall on the host (Task 20345): CAP_NET_ADMIN, and the
	// netlink socket family nft(8) speaks. It is written as a drop-in beside
	// the unit (PacketFilterDropIn) rather than into it, and a plan without it
	// removes a drop-in an earlier install or upgrade left, so the flag always
	// describes the device afterwards. systemd output only.
	//
	// `cloop executor agent install` grants it unless told
	// --packet-filter=false (Task 20352): a device that cannot install a
	// firewall refuses every sandbox that has one, and the grant stays with
	// the agent — pkg/caps keeps it from every program the agent starts except
	// nft(8), so a workload never holds it. The zero value withholds it, for
	// callers that build a Spec themselves.
	PacketFilter bool

	// RemoteUpgrade installs the root helper that carries out an upgrade the
	// hub asked the agent for (Task 20376): <service>-upgrade.service and the
	// .path unit that starts it when the agent files a request. Without it a
	// device installed with the hardened defaults cannot be upgraded from the
	// Executors panel or by auto-update — the agent cannot replace a
	// root-owned binary or restart itself — and it says so when asked.
	// systemd output only. `cloop executor agent install` installs it unless
	// told --remote-upgrade=false; the zero value withholds it.
	RemoteUpgrade bool

	// Channel is the update channel the device follows (Task 20376). Empty or
	// stable: releases only, and no drop-in. Edge: the channel drop-in, which
	// lets the device be upgraded to signed builds of main as well. systemd
	// output only, and decided here, on the device: the hub has no way to
	// set it.
	Channel provenance.Channel
}

Spec describes the installation to materialise.

Every field has a usable default; Normalize fills them and rejects the combinations that cannot produce a working service.

func (Spec) ChannelDropInPath added in v0.0.5

func (s Spec) ChannelDropInPath() string

ChannelDropInPath is where the channel drop-in is written.

func (Spec) DropInDir added in v0.0.5

func (s Spec) DropInDir() string

DropInDir is the unit's drop-in directory.

func (Spec) InitScriptPath

func (s Spec) InitScriptPath() string

InitScriptPath is where OutputShell writes its script.

func (Spec) Normalize

func (s Spec) Normalize() (Spec, error)

Normalize fills defaults and rejects a spec that cannot yield a working service. It returns a copy: callers keep their input intact so an error message can quote what was actually asked for.

func (Spec) NormalizeForRemoval

func (s Spec) NormalizeForRemoval() (Spec, error)

NormalizeForRemoval fills the same defaults but does not insist on a control-plane URL.

Uninstalling is not the inverse of installing in its inputs: it needs only the paths, and an operator removing a device does not necessarily still have the bundle it was enrolled with — often the reason they are removing it is that the hub is gone. Demanding --server to delete a file would be exactly the friction this command exists to remove.

func (Spec) PacketFilterDropInPath added in v0.0.5

func (s Spec) PacketFilterDropInPath() string

PacketFilterDropInPath is where the packet-filter grant is written.

func (Spec) UnitFileName

func (s Spec) UnitFileName() string

UnitFileName is the systemd unit's basename.

func (Spec) UnitPath

func (s Spec) UnitPath() string

UnitPath is the absolute path the unit is written to.

func (Spec) UpgradeHelperName added in v0.0.5

func (s Spec) UpgradeHelperName() string

UpgradeHelperName is the base name of the helper's two units.

func (Spec) UpgradeHelperPathUnitName added in v0.0.5

func (s Spec) UpgradeHelperPathUnitName() string

UpgradeHelperPathUnitName is the path unit's name, for systemctl.

func (Spec) UpgradeHelperPathUnitPath added in v0.0.5

func (s Spec) UpgradeHelperPathUnitPath() string

UpgradeHelperPathUnitPath is the path unit that starts it.

func (Spec) UpgradeHelperServicePath added in v0.0.5

func (s Spec) UpgradeHelperServicePath() string

UpgradeHelperServicePath is the helper's service unit.

func (Spec) UpgradeHelperStateDir added in v0.0.5

func (s Spec) UpgradeHelperStateDir() string

UpgradeHelperStateDir is the helper's own state directory — systemd's StateDirectory for it, where cosign keeps the Sigstore trust root it fetched.

func (Spec) UpgradeRequestPath added in v0.0.5

func (s Spec) UpgradeRequestPath() string

UpgradeRequestPath is where the agent files a request for the helper.

type UpgradeOptions

type UpgradeOptions struct {
	// Source is the new cloop binary to install. Empty means the running
	// executable, which is the common case: an operator copies a new cloop to
	// the device and runs it with --upgrade.
	Source string
	// Force replaces the binary and restarts the service even when the
	// installed binary is already byte-identical. For recovering from a
	// truncated or mis-permissioned install, where the bytes match but the
	// service is not actually running the binary on disk.
	//
	// It also permits a deliberate rollback: installing a build older than
	// the one in place, or one speaking a protocol this control plane no
	// longer accepts. It does *not* permit installing a binary that failed
	// verification — see ErrBinaryUnusable for why that line is where it is —
	// nor one earlier on main than the installed binary, which takes
	// AllowRollback.
	Force bool

	// AllowRollback permits installing a build whose sequence is lower than
	// the installed binary's, or one carrying none when the installed binary
	// does (ErrRollback, Task 20380).
	//
	// It is not Force, because Force arrives from places that must not be able
	// to do this: the upgrade request the agent files for the root helper, and
	// the upgrade frame the hub sends an agent running as root, both carry a
	// force flag, and a compromised agent or hub could set either. Only the
	// CLI sets this, from --force on an `install --upgrade` run by root on the
	// device — never from --apply-request, whose target the request names.
	AllowRollback bool
	// DryRun performs every check and reports what would happen without
	// writing a byte or restarting anything.
	//
	// It is a real mode rather than a courtesy, because this command bounces a
	// service on a device an operator may not be able to reach again quickly.
	// The checks it runs are the expensive ones — is anything installed, does
	// the source exist, is it even a different build, does the new binary run
	// at all — so a dry run answers "will this do what I think" rather than
	// merely echoing the flags back.
	DryRun bool

	// Bundle is the Sigstore bundle proving the source binary's provenance.
	// Empty looks for one beside the source, at <source>.sigstore.json, which
	// is where a release download leaves it.
	Bundle string

	// SkipVerify installs a binary whose provenance has not been proven.
	//
	// The escape hatch for an air-gapped site that cannot reach Sigstore, and
	// for a developer installing a locally built binary — which has no
	// signature and cannot have one. It is a separate flag from Force on
	// purpose: Force means "I know this is a downgrade", which is a statement
	// about versions, and conflating it with "I do not know where this binary
	// came from" would let a routine rollback silently drop a security check.
	SkipVerify bool

	// ProvenanceEstablished states that the caller has already proven this
	// binary's provenance against a signed artifact, and that there is
	// therefore no bundle to find beside it (Task 20331).
	//
	// It exists for exactly one caller: a remote agent upgrading itself. The
	// release signs the *archive*, not the executable inside it, so by the time
	// pkg/upgrade has verified the signature and extracted the binary there is
	// no bundle that would match the extracted file — the check has already
	// happened, one layer up, and cannot be repeated here.
	//
	// It is deliberately not spelled as SkipVerify. Reusing that flag would
	// work and would be a lie in two directions: it would print a warning
	// saying provenance was not checked when it was, and it would record
	// ProvenanceVerified=false in the result an operator reads afterwards.
	// Distinguishing "verified elsewhere" from "not verified" is the difference
	// between an audit trail and a plausible one.
	//
	// Like SkipVerify, this is settable only in-process. Nothing an agent
	// receives from the control plane reaches it; see the security argument in
	// pkg/executor/remote/upgradeproto.go.
	ProvenanceEstablished bool

	// PacketFilter grants or withdraws the packet-filter drop-in (Task 20352)
	// as part of the upgrade. The zero value leaves it as the device has it.
	//
	// It is how a device installed before the installer granted CAP_NET_ADMIN
	// gets the grant without the enrollment bundle a full install needs, and it
	// applies even when the binary is already current: the drop-in is written,
	// systemd reloaded, and the agent restarted, with the same rollback as a
	// binary swap if it does not come back.
	PacketFilter PacketFilterChange

	// Channel moves the device to another update channel (Task 20376): edge
	// writes the channel drop-in, stable removes it, empty keeps it. Like the
	// packet filter it applies even when the binary is current, and it is
	// decided here, on the device — the hub has no way to set it.
	Channel provenance.Channel

	// RemoteUpgrade installs or removes the root helper that carries out an
	// upgrade the hub asks for (remoteupgrade.go). The zero value keeps it.
	RemoteUpgrade RemoteUpgradeChange

	// ExpectVersion, when set, is the version the staged binary must report
	// when it is run. An edge build's signed manifest names it (Task 20376):
	// the signature proves edge.yml built *a* commit, the manifest says which,
	// and this is what proves the bytes being installed are that build rather
	// than another genuine one renamed. Unlike a downgrade, a mismatch is not
	// overridable by Force.
	ExpectVersion string

	// ExpectCommit and ExpectSequence are the rest of what the signed manifest
	// says about the build (Task 20380): the full commit it was made from and
	// that commit's place on main. Checked whenever ExpectVersion is set,
	// against what the binary reports when it is run, and not overridable: a
	// validly signed manifest for one commit paired with a binary of another
	// is refused even when the versions happen to agree. ExpectSequence zero
	// means the manifest names none (schema 1), and then the binary must carry
	// none either.
	ExpectCommit   string
	ExpectSequence int

	// SettleTimeout bounds the wait for a restarted service to report itself
	// active before the upgrade concludes the new build is bad and rolls back.
	// Zero uses serviceSettleTimeout.
	//
	// Exposed because the right value is a property of the device, not of
	// cloop. The default is generous for a laptop and can still be short for a
	// heavily loaded single-core gateway paging in a 40 MB executable off slow
	// flash — and there, a timeout that fires early does not merely report a
	// problem, it reverts a good upgrade.
	SettleTimeout time.Duration
}

UpgradeOptions parameterises an in-place upgrade.

type UpgradeRequest added in v0.0.5

type UpgradeRequest struct {
	TargetVersion string `json:"target_version"`
	Force         bool   `json:"force,omitempty"`
	SettleSeconds int    `json:"settle_seconds,omitempty"`
	Reason        string `json:"reason,omitempty"`
	RequestedAt   string `json:"requested_at"`
}

UpgradeRequest is what an agent files for the helper. It mirrors the upgrade frame's payload field for field (remote.UpgradePayload), which is the point: it carries no more authority than the frame does.

func TakeUpgradeRequest added in v0.0.5

func TakeUpgradeRequest(s Spec, now time.Time) (UpgradeRequest, error)

TakeUpgradeRequest reads the waiting request and deletes it, as root, from a directory owned by the agent's user.

That directory is the caller's to control, so the read is defensive: the file is opened without following a symlink, must be a regular file of a few KiB, and is deleted before it is acted on — a request that cannot be parsed or is stale is deleted too, so the path unit does not start the helper again on the same file. now is the clock, for tests.

type UpgradeResult

type UpgradeResult struct {
	// Spec is the normalised spec the upgrade ran against.
	Spec Spec
	// Output is the supervision system involved.
	Output Output
	// Source is the binary that was installed from.
	Source string
	// AlreadyCurrent reports that the installed binary was byte-identical and
	// nothing was changed.
	AlreadyCurrent bool
	// BinaryCurrent reports that the installed binary was already this build,
	// so none was copied — whether or not something else, the packet-filter
	// drop-in, changed.
	BinaryCurrent bool
	// BinaryReplaced reports that the service binary was rewritten.
	BinaryReplaced bool
	// Restarted reports that the service was asked to restart. False with
	// BinaryReplaced true means the service was not running, so the new binary
	// is in place and will be used whenever it is next started.
	Restarted bool
	// DryRun echoes that nothing was actually changed, so a caller cannot
	// render a result as done when it was only planned.
	DryRun bool
	// StagedBuild is what the new binary said about itself when it was run,
	// and InstalledBuild is what the one being replaced said. A zero
	// StagedBuild means verification was skipped — see Verified.
	StagedBuild, InstalledBuild BinaryIdentity
	// Verified reports that the staged binary was executed and identified
	// itself before anything was replaced. False means the check was skipped
	// because the installer is staged (its target is another machine, whose
	// binary may legitimately not run here), and the result must not be
	// rendered as though the binary had been proven good.
	Verified bool
	// ProvenanceVerified reports that the source binary's signature was
	// checked against cloop's release workflow before anything was executed or
	// written. False means --insecure-skip-verify was passed, and the result
	// must not be rendered as though the binary's origin were known.
	ProvenanceVerified bool
	// BackupPath is where the replaced binary was kept, so an operator can
	// roll back by hand. Empty when nothing was replaced.
	BackupPath string
	// RolledBack reports that the service did not come back on the new binary
	// and the previous one was restored. It travels with a non-nil error:
	// the upgrade failed, and this says the device was left working.
	RolledBack bool

	// PacketFilterChanged reports that the upgrade wrote or removed the
	// packet-filter drop-in, and PacketFilterChange says how, for the log.
	PacketFilterChanged bool
	PacketFilterChange  string
	// PacketFilterGranted reports whether the device's unit grants the agent
	// what installing a sandbox firewall takes, once the upgrade is done. Only
	// meaningful for OutputSystemd.
	PacketFilterGranted bool

	// ChannelChanged reports that the upgrade moved the device to another
	// update channel, and Channel is the channel it follows afterwards.
	ChannelChanged bool
	Channel        provenance.Channel
	// RemoteUpgradeChanged reports that the upgrade installed or removed the
	// remote-upgrade helper, and RemoteUpgradeInstalled whether the device has
	// it afterwards.
	RemoteUpgradeChanged   bool
	RemoteUpgradeInstalled bool
	// UnitChanges says, one line each, what the upgrade changed beside the
	// binary — or, for a dry run, would change.
	UnitChanges []string

	// PreviousChecksum and NewChecksum are SHA-256 sums of the old and new
	// binaries, for an operator correlating a rollout across a fleet.
	//
	// "Checksum" rather than "digest" deliberately. These are content sums of a
	// public binary used to answer "are these the same bytes", not credential
	// material, so comparing them with == is correct — and the security suite's
	// constant-time scanner reads any identifier containing "digest" as a
	// secret. Naming them for what they are keeps that check meaningful instead
	// of adding an exception to it.
	PreviousChecksum string
	NewChecksum      string
}

UpgradeResult describes what an upgrade did, so the CLI can report the truth rather than a fixed success message.

Jump to

Keyboard shortcuts

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