control

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Jun 10, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package control is the client half of the holder protocol: it spawns a holder process, waits for its members to come up, and drives them (createnetwork/ reserve/boot-member/status/stop) over the per-member unix sockets. It is backend-neutral — it imports only internal/store, never a hypervisor or the orchestrator — so the client and CLI that drive a holder over this protocol stay pure Go and need no codesign (ADR-0017, R1).

Two spawn modes share one wire protocol. The CLI spawns DETACHED (the holder is a session leader via Setsid and its VMs outlive the command — cattle-with-persistence). The library spawns BOUND/attached: the holder is a child of the test process, is handed the client PID, and holds one long-lived control connection open; when the test process dies the holder reaps itself and its VMs (ADR-0017, R4). The server half lives in internal/holder.

Index

Constants

View Source
const (
	// RunnerFlag marks a process as a holder and is followed by the
	// comma-separated member names it owns. The holder server parses it; Spawn
	// passes it. The helper no longer receives the VM options — it is a
	// backend-server that the client drives over RPCs (ADR-0020), so the only thing
	// it needs at launch is the member names it will serve.
	RunnerFlag = "--fleetbox-runner"

	// ReconcileFlag marks a one-shot reconcile launch of the helper: it reclaims
	// orphaned host network state (Linux bridges/taps/iptables/ip_forward) and
	// exits, without serving any member. It backs Prune (ADR-0013/0020).
	ReconcileFlag = "--fleetbox-reconcile"

	// EnvParentPID is set only in bound mode: it is the client PID the holder
	// watches so it can reap itself when the test process is gone (R4).
	EnvParentPID = "FLEETBOX_PARENT_PID"

	// ProtocolVersion is exchanged on the bind handshake. The client and the
	// helper bake in this constant from the same source, so a download (whose
	// filename is version-stamped) always matches; a mismatch means a stale
	// FLEETBOX_HELPER override pointing at a different build, which Spawn rejects
	// loudly rather than driving with an incompatible protocol (ADR-0017, R5).
	//
	// "2" is the NDJSON command protocol with the resolved-member-spec payload and
	// the create-network/reserve/boot-member exchange; "1" was the fixed-256-byte
	// text protocol carrying an image alias. The bump forces a stale "1" helper to
	// be rejected at handshake rather than driven with an incompatible wire format.
	ProtocolVersion = "2"

	// Wire commands, shared with the holder server half (internal/holder) so the
	// two ends never drift. The command-socket commands (status/stop and the
	// createnetwork/reserve/boot-member set in wire.go) travel as NDJSON Request
	// objects; only bind/ack stay a raw-text handshake on the .ctl socket. Adding a
	// member to a live cluster is reserve + boot-member (no dedicated command).
	CmdStatus = "status"
	CmdStop   = "stop"
	CmdBind   = "bind"
	// CmdBindAck confirms the bind handshake. The client sends it only after it
	// has accepted the helper's version and is committing to hold the connection;
	// the helper arms its EOF death-watch only after receiving it. This keeps a
	// connection that closes mid-handshake (a dial retry, a stray probe) from
	// being mistaken for the parent dying and tearing the cluster down (R4).
	CmdBindAck = "ack"

	// Member states reported over the status socket. Since ADR-0020 the image pull
	// is client-side (before the helper is spawned), so StateDownloading now covers
	// only the helper's one-time VMM-binary fetch; it is reported separately so the
	// readiness wait does not charge that download against the per-boot budget.
	StateDownloading = "downloading"
	StateStarting    = "starting"
	StateRunning     = "running"
	StateStopped     = "stopped"
	StateError       = "error"
)
View Source
const (
	// CmdCreateNetwork asks the helper to create the cluster's shared network and
	// report its subnet (empty on a DHCP backend such as vz).
	CmdCreateNetwork = "createnetwork"

	// CmdReserve asks the helper to allocate one member's address on the live
	// network (Linux static IP honoring the client's hint, or just a MAC on the
	// DHCP/vz path) and return the reservation. It replaces the orchestrator's
	// old client-side allocateIP (Decision 5).
	CmdReserve = "reserve"

	// CmdBootMember asks the helper to create and start one member's VM from a
	// resolved MemberSpec on the shared network, using the address it reserved.
	CmdBootMember = "boot-member"
)

Variables

This section is empty.

Functions

func IsRunning

func IsRunning(st *store.Store, name string) bool

IsRunning reports whether the holder serving a member is alive, by reading its pidfile and signalling the process.

func ReadMessage added in v0.2.0

func ReadMessage(r io.Reader, v any) error

ReadMessage decodes one NDJSON message from r into v. Each connection carries a single request and a single reply, so a fresh decoder per call never strands buffered bytes. Any read deadline set on the underlying conn still bounds it.

func Stop

func Stop(st *store.Store, name string) error

Stop gracefully shuts down a single member (the holder keeps running for its siblings until the last one leaves).

func WaitMembers

func WaitMembers(st *store.Store, names []string, timeout time.Duration) (map[string]*Status, error)

WaitMembers polls per-member status until every name is running with an IP, returning early with an error if any member enters the error state. While any member is downloading the image/VMM binaries it announces the pull once and keeps the boot deadline ahead of the (bounded) download.

func WriteMessage added in v0.2.0

func WriteMessage(w io.Writer, v any) error

WriteMessage encodes v as one NDJSON message (json.Encoder.Encode appends the trailing newline). It is shared by the client (control) and server (holder) halves so the two ends frame messages identically.

Types

type MemberSpec added in v0.2.0

type MemberSpec struct {
	Name          string   `json:"name"`
	DiskPath      string   `json:"disk_path"`
	SeedPath      string   `json:"seed_path"`
	FixturePaths  []string `json:"fixture_paths,omitempty"`
	EFIPath       string   `json:"efi_path,omitempty"`
	CPUs          int      `json:"cpus"`
	MemoryBytes   uint64   `json:"memory_bytes"`
	SerialLogPath string   `json:"serial_log_path,omitempty"`
}

MemberSpec is the resolved per-member boot payload sent over the wire. Every field is a ready value the helper consumes verbatim — the client has already resolved the image, copied the disk, built the seed and fixtures, and chosen the store paths. EFIPath is the vz EFI variable-store path (store-derived by the client; the helper creates/loads the file there); cloud-hypervisor ignores it (PVH boot). MAC and IP are intentionally absent: the helper owns them from the reservation it made (Decisions 5 and 6).

type Request added in v0.2.0

type Request struct {
	Cmd    string      `json:"cmd"`
	Name   string      `json:"name,omitempty"`
	IPHint string      `json:"ip_hint,omitempty"`
	Spec   *MemberSpec `json:"spec,omitempty"`
}

Request is the NDJSON command envelope. Cmd selects the command; the remaining fields are populated only for the commands that use them (Name + IPHint for reserve, Spec for boot-member).

type Reservation added in v0.2.0

type Reservation struct {
	IP  string `json:"ip,omitempty"`
	MAC string `json:"mac"`
}

Reservation is the address the helper allocated for a member on the shared network. IP is the static IPv4 the client bakes into the seed's network-config (Linux); it is empty on the DHCP/vz path, where the runtime IP is discovered post-boot and reported via status. MAC is the NIC address the helper will set, returned so the client's seed and the helper's NIC agree without both sides recomputing (Decision 6).

type Response added in v0.2.0

type Response struct {
	Error       string       `json:"error,omitempty"`
	Status      *Status      `json:"status,omitempty"`
	Subnet      string       `json:"subnet,omitempty"`
	Reservation *Reservation `json:"reservation,omitempty"`
}

Response is the NDJSON reply envelope. A non-empty Error means the command failed; otherwise the command-specific field is set (Status for status, Subnet for createnetwork, Reservation for reserve) and stop/boot-member reply with an empty object on success.

func SendCommand added in v0.2.0

func SendCommand(st *store.Store, name string, req Request, timeout time.Duration) (*Response, error)

SendCommand dials the holder socket serving member `name`, sends one NDJSON request, and returns the reply. It is the client transport the remote-proxy backend uses for the cluster-level RPCs (createnetwork/reserve/boot-member, routed through the primary member's socket) and for stop. A non-empty Response.Error is turned into a Go error. `timeout` bounds the wait for the reply, which a slow boot-member needs to be generous.

The dial is retried for a short window: a DETACHED helper (CLI) is not bound, so Spawn returns before the helper has opened its member sockets, and the first RPC (createnetwork) would otherwise race the helper's startup. The BOUND (library) path is already synchronized by Spawn's bind handshake, so the retry is a fast no-op there.

type Session

type Session struct {
	// contains filtered or unexported fields
}

Session is a handle to a spawned holder. In bound mode it owns the control connection whose EOF triggers holder teardown and reaps the child process; in detached mode Close merely releases the persistent process.

func Spawn

func Spawn(st *store.Store, cfg SpawnConfig) (*Session, error)

Spawn launches a holder for the given member names. In detached mode the holder is a persistent session leader; in bound mode it is an attached child handed the client PID, and Spawn additionally opens the holder's control connection so its EOF reaps the holder when the client goes away (R4). It does not wait for the members to boot — call WaitMembers (or use the returned Session) for that.

func (*Session) Close

func (s *Session) Close() error

Close tears the session down. In bound mode it closes the control connection (the holder's EOF teardown trigger) and reaps the child once it exits; in detached mode it releases the persistent process so this client can exit without leaving a zombie. It is idempotent.

type SpawnConfig

type SpawnConfig struct {
	Exe   string
	Names []string
	Bound bool
}

SpawnConfig configures a holder launch. Exe is the binary to run (the downloaded fleetbox-helper on darwin, os.Executable() for the linux re-exec-self CLI). Names are the members the holder will serve sockets for at launch; the client drives their boot afterwards over RPCs. Bound selects the library lifetime mode (attached + parent PID + control connection); false is the CLI's detached/persistent mode.

type Status

type Status struct {
	Name    string `json:"name"`
	PID     int    `json:"pid"`
	Running bool   `json:"running"`
	IP      string `json:"ip"`
	State   string `json:"state"` // "starting", "running", "stopped", "error"
	Error   string `json:"error,omitempty"`
}

Status represents the state of a VM member.

func GetStatus

func GetStatus(st *store.Store, name string) (*Status, error)

GetStatus returns the status of a member. A live holder is authoritative — even for a member still downloading or booting, whose config.json does not exist yet (it is written during boot) — so the holder socket is consulted first. Only when no holder serves the name do we fall back to on-disk state: stopped if the VM exists, otherwise an error that it is absent.

Jump to

Keyboard shortcuts

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