Documentation
¶
Overview ¶
Package usb generates the artifacts needed to boot a bare-metal laptop off a USB stick and have it self-provision a K3s node that joins an existing devx-managed cluster.
The package is organized around a single source of truth — the canonical join script produced by RenderJoinScript from a JoinSpec. Every OS renderer (FCOS/Ignition, prebaked image) only re-packages that same script for its boot medium, so the join *logic* lives in exactly one place.
Index ¶
- Constants
- func Flash(ctx context.Context, imagePath, device string, confirm func() bool) error
- func ParseSizeMB(s string) (int, error)
- func RenderAssemblyScript(p AssemblyParams) (string, error)
- func RenderBuilderProvision() string
- func RenderJoinScript(s JoinSpec) (string, error)
- func RendererNames() []string
- func ValidateFlashTarget(d DiskInfo) error
- type Artifact
- type ArtifactSink
- type AssemblyParams
- type BakedRenderer
- type BootEntry
- type BuildOptions
- type BuildResult
- type Builder
- type Coordinates
- type DirSink
- type DiskInfo
- type FCOSRenderer
- type Injection
- type InjectionKind
- type JoinSpec
- type Mode
- type PlanEntry
- type PruneOptions
- type PruneResult
- type Renderer
- type Role
- type ScratchConfig
- type ScratchStrategy
Constants ¶
const ( // CoreosInstallerImage runs the install. coreos-installer ships no Linux // binary on GitHub, so we run its official container in the builder VM. It // fetches the Fedora CoreOS metal image from the stream itself, so there is // no ISO download and nothing for devx to vendor. CoreosInstallerImage = "quay.io/coreos/coreos-installer:release" // DefaultImageSizeMB sizes the sparse image just for the Fedora CoreOS metal // layout (BIOS-BOOT, EFI-SYSTEM, boot, root). FCOS grows root to fill the real // USB on first boot, so a few GB is plenty here. DefaultImageSizeMB = 4096 )
const BakedImage = "devx-node.x86_64.img"
BakedImage is the prebuilt, self-contained node image the baked entries boot. It already contains k3s + Tailscale + a generic devx-join unit; only the per-cluster secrets (the join script) live on the separate config partition, so the image itself carries no credentials. Built by CI / a future `devx cluster usb bake`.
const ButaneVersion = "0.23.0"
ButaneVersion is fetched into the builder VM as a static binary (not an apt package). butane compiles the join Butane into the Ignition that coreos-installer embeds.
const DefaultTailscaleVersion = "1.98.4"
DefaultTailscaleVersion is the pinned Tailscale static-binary version used when the spec does not override it. Pinned (not "latest") so first boot is offline-deterministic and golden-testable.
const EphemeralLabel = "devx.io/ephemeral"
EphemeralLabel marks nodes that joined via an ephemeral live-boot. The prune reaper uses it to distinguish disposable nodes from permanent ones.
const FCOSStickName = "fedora-coreos.x86_64.gpt"
FCOSStickName labels the FCOS boot entry's medium. coreos-installer writes a native GPT Fedora CoreOS disk (fetching the metal image from the stream itself), so this is a provenance label for the build plan/manifest, not a file devx places on the stick.
Variables ¶
This section is empty.
Functions ¶
func Flash ¶ added in v0.61.0
Flash validates the target device and writes imagePath to it. confirm is called (unless the caller passes a nil/always-true confirm) to gate the destructive write.
func ParseSizeMB ¶ added in v0.61.0
ParseSizeMB parses a size like "16G", "16GiB", "512M" into mebibytes (G = 1024 MiB). It requires an explicit M or G unit.
func RenderAssemblyScript ¶ added in v0.61.0
func RenderAssemblyScript(p AssemblyParams) (string, error)
RenderAssemblyScript returns the deterministic bash run inside the Lima VM to produce a bootable GPT Fedora CoreOS image. It compiles the join Butane to Ignition, then runs coreos-installer (in its official container) to install the FCOS metal image + Ignition onto a loop-mounted sparse image. The resulting image flashes to a USB on macOS where coreos-installer cannot run. Pure text (golden-tested); execution is validated by the gated integration test.
func RenderBuilderProvision ¶ added in v0.61.0
func RenderBuilderProvision() string
RenderBuilderProvision returns the Lima provision script that installs the Linux tooling needed to assemble the GPT Fedora CoreOS image. Deterministic (golden). coreos-installer is NOT installed here — it ships no Linux binary on GitHub, so the assembly script runs its official container via podman.
func RenderJoinScript ¶
RenderJoinScript produces the canonical first-boot join script. The script is deterministic for a given spec (golden-tested) and OS-agnostic — renderers drop it onto their boot medium and arrange to run it once at first boot.
func RendererNames ¶
func RendererNames() []string
RendererNames returns the built-in renderer names, sorted.
func ValidateFlashTarget ¶ added in v0.61.0
ValidateFlashTarget refuses any target that is not positively identified as a removable, non-system, whole USB/SD stick. Unknown/unparseable attributes are treated as unsafe (fail closed).
Types ¶
type Artifact ¶
type Artifact struct {
Path string // relative path under the output root (forward slashes)
Contents []byte // file contents
Mode fs.FileMode // 0 means 0644
}
Artifact is a single file a renderer wants written under the staging root.
type ArtifactSink ¶
ArtifactSink receives the files a renderer produces.
type AssemblyParams ¶ added in v0.61.0
type AssemblyParams struct {
// ImageSizeMB sizes the sparse image. Zero falls back to DefaultImageSizeMB.
ImageSizeMB int
ButaneVM string // path inside the VM to the FCOS Butane (.bu) to compile
ImageVM string // path inside the VM for the output GPT .img
}
AssemblyParams is the fully-resolved input to RenderAssemblyScript.
type BakedRenderer ¶
type BakedRenderer struct{}
BakedRenderer separates a prebuilt OS image (with k3s+Tailscale baked in) from the per-cluster secrets, which it emits onto a small FAT config partition the image reads at boot. Fastest/most offline-robust boot at the cost of a build pipeline for the image.
func (BakedRenderer) Render ¶
func (BakedRenderer) Render(spec JoinSpec, m Mode, sink ArtifactSink) ([]BootEntry, error)
Render implements Renderer. It emits the config-partition payload: the join script and a mode flag the baked image's generic unit reads at boot. No credentials are in the image — only here.
type BootEntry ¶
type BootEntry struct {
MenuTitle string // human label for the boot entry
BaseImage string // boot medium this entry produces/boots
Renderer string // provenance: renderer Name()
Mode Mode // provenance: lifecycle mode
InstallToDisk bool // true if this entry persists the OS to the internal disk
Injection Injection // how the join payload attaches
}
BootEntry is one boot option plus the metadata the assembler needs to wire its provisioning payload.
type BuildOptions ¶
type BuildOptions struct {
OutputDir string // staging directory for artifacts (required unless DryRun)
Renderers []string // subset of fcos|baked; empty uses config/all
Modes []Mode // subset of ephemeral|install; empty uses both
Role Role // role for install entries (ephemeral always joins as agent)
DryRun bool // plan only; do not resolve coordinates or write files
}
BuildOptions configures a `devx cluster usb build`.
type BuildResult ¶
type BuildResult struct {
OutputDir string `json:"output_dir,omitempty"`
DryRun bool `json:"dry_run"`
Entries []PlanEntry `json:"entries"`
RequiredImages []string `json:"required_images"`
}
BuildResult summarizes a build for human and --json output.
func Build ¶
func Build(ctx context.Context, cfg *config.Config, opts BuildOptions) (*BuildResult, error)
Build resolves the join coordinates, runs the selected renderers across the selected modes into OutputDir, and writes an operator manifest. With DryRun it produces only the plan (no cluster access, no files).
type Builder ¶ added in v0.61.0
type Builder struct {
VMName string
// contains filtered or unexported fields
}
Builder orchestrates the GPT Fedora CoreOS image assembly inside a Lima VM.
func NewBuilder ¶ added in v0.61.0
NewBuilder returns a Builder that shells out via the system command runner.
func (*Builder) BuildImage ¶ added in v0.61.0
func (b *Builder) BuildImage(ctx context.Context, p AssemblyParams, stagingDir string) (string, error)
BuildImage tars the staging payloads, copies them + a generated assembly script into the builder VM, runs it, and copies the resulting image back to a unique host temp file (the caller owns its cleanup). Tarring avoids limactl-copy directory-nesting ambiguity and the need for a recursive copy flag.
func (*Builder) EnsureBuilderVM ¶ added in v0.61.0
EnsureBuilderVM ensures the builder VM exists and is provisioned. It creates a bare VM when absent, resumes a stopped one, then runs the (idempotent) provision script explicitly so failures surface as errors. Idempotent.
type Coordinates ¶
type Coordinates struct {
LANServerURL string
TailnetServerURL string
Token string
K3sVersion string
}
Coordinates are the cluster-derived facts a USB node needs to join: where the API server is (on the LAN and/or the tailnet), the join token, and the k3s version to match.
func ResolveCoordinates ¶
ResolveCoordinates finds the first healthy server node, reads its join token and reachable API endpoints. It mirrors the healthy-server discovery in cluster.Join (kept separate to avoid changing Join's behavior; deduping the two is a noted follow-up).
If cfg.Cluster.USB.AgentToken is set it is preferred over the server node-token, so operators can bake a least-privilege agent token that cannot add control-plane servers.
type DirSink ¶
type DirSink struct {
Root string
}
DirSink is an ArtifactSink that writes artifacts as files under Root.
type DiskInfo ¶ added in v0.61.0
type DiskInfo struct {
Identifier string // "disk4"
MediaName string // "SanDisk USB"
Internal bool
Removable bool
Protocol string // "USB"
SizeBytes int64
}
DiskInfo is the subset of `diskutil info <device>` output relevant to deciding whether a target is a safe, removable flash destination.
func ParseDiskutilInfo ¶ added in v0.61.0
ParseDiskutilInfo parses the text output of `diskutil info <device>`.
type FCOSRenderer ¶
type FCOSRenderer struct{}
FCOSRenderer packages the canonical join script as a Fedora CoreOS Ignition config (authored as Butane). This is the highest-reuse renderer — it rides the same FCOS/Ignition/butane stack devx already uses in internal/ignition.
func (FCOSRenderer) Render ¶
func (FCOSRenderer) Render(spec JoinSpec, m Mode, sink ArtifactSink) ([]BootEntry, error)
Render implements Renderer. It emits a Butane file (.bu) that writes the join script and a oneshot systemd unit to run it. The build layer compiles the .bu to Ignition with butane (gated) and embeds it into the live ISO.
type Injection ¶
type Injection struct {
Kind InjectionKind
PayloadPath string // relative path of the payload artifact in the sink
Notes string // operator-facing notes
}
Injection describes the provisioning payload attached to one boot entry.
type InjectionKind ¶
type InjectionKind string
InjectionKind identifies how a boot entry's provisioning payload attaches to its boot medium.
const ( // InjectIgnition delivers a Fedora CoreOS Ignition (compiled from Butane). InjectIgnition InjectionKind = "ignition" // InjectCloudInit delivers a cloud-init NoCloud/autoinstall seed. InjectCloudInit InjectionKind = "cloud-init" // InjectConfigPartition delivers a small FAT partition image a prebaked OS // reads at boot. InjectConfigPartition InjectionKind = "config-partition" )
type JoinSpec ¶
type JoinSpec struct {
Role Role
Mode Mode
K3sVersion string // e.g. "v1.30.4+k3s1"; empty installs the k3s default
Pool string // node pool label value
// NodeNamePrefix is combined at boot with the MAC address and hostname to
// produce a name that is unique per laptop and per boot.
NodeNamePrefix string
Token string // K3s join token (prefer a dedicated agent token)
LANServerURL string // e.g. "https://10.0.0.5:6443"; tried first if reachable
TailnetServerURL string // e.g. "https://100.64.0.5:6443"; Tailscale fallback
TailscaleAuthKey string // ephemeral, ACL-tagged; empty disables Tailscale
TailscaleVersion string // pinned static-binary version for FCOS provisioning (e.g. "1.84.3")
Ephemeral bool // add the EphemeralLabel and run as a disposable node
Scratch ScratchConfig
ExtraLabels map[string]string
ExtraArgs []string // appended verbatim to the k3s install args
WiFiSSID string // optional; bring up Wi-Fi before joining (for laptops w/o Ethernet)
WiFiPSK string
}
JoinSpec is the fully-resolved description of how one boot entry should join the cluster. It is the input to RenderJoinScript and to every Renderer.
type PlanEntry ¶
type PlanEntry struct {
Renderer string `json:"renderer"`
Mode Mode `json:"mode"`
MenuTitle string `json:"menu_title"`
BaseImage string `json:"base_image"`
Injection string `json:"injection"`
}
PlanEntry is one boot-menu entry in a build result (JSON-friendly).
type PruneOptions ¶
type PruneOptions struct {
TTL time.Duration // remove NotReady ephemeral nodes whose Ready transition is older than this
DryRun bool
}
PruneOptions configures the ephemeral-node reaper.
type PruneResult ¶
type PruneResult struct {
DryRun bool `json:"dry_run"`
Stale []string `json:"stale"` // nodes selected for removal
Removed []string `json:"removed"` // nodes actually drained+deleted
}
PruneResult reports what the reaper found and did (JSON-friendly).
func Prune ¶
func Prune(ctx context.Context, cfg *config.Config, opts PruneOptions) (*PruneResult, error)
Prune removes ephemeral USB nodes that have gone NotReady (e.g. the laptop was unplugged) and stayed that way past the TTL — the disposable-node cleanup that keeps `kubectl get nodes` free of ghosts. It only ever touches nodes carrying the EphemeralLabel, so permanent nodes are never reaped.
type Renderer ¶
type Renderer interface {
Name() string
Modes() []Mode
Render(spec JoinSpec, m Mode, sink ArtifactSink) ([]BootEntry, error)
}
Renderer turns a JoinSpec into boot artifacts for one OS/packaging. Renderers are pure: they only produce in-memory artifacts and never shell out, so they are fully unit-testable. External tooling (butane compile, ISO embedding, USB writes) lives in the build/assemble layer and is gated on tool presence.
func AllRenderers ¶
func AllRenderers() []Renderer
AllRenderers returns the built-in renderers keyed by Name(), in a stable order.
func RendererByName ¶
RendererByName looks up a built-in renderer.
type Role ¶
type Role string
Role is the K3s role a USB-booted node joins as.
const ( // RoleAgent joins as a worker. This is the default and the only safe choice // for ephemeral nodes: they must never become etcd voters. RoleAgent Role = "agent" // RoleServer joins as an additional control-plane (etcd) member. Only valid // for persistent installs. RoleServer Role = "server" )
type ScratchConfig ¶
type ScratchConfig struct {
Strategy ScratchStrategy
Source string // for ScratchExisting: "LABEL=...", "UUID=..." or "/dev/..."
ForceFormat bool // allow formatting an existing partition (destructive)
}
ScratchConfig describes ephemeral on-disk scratch storage.
type ScratchStrategy ¶
type ScratchStrategy string
ScratchStrategy selects how an ephemeral node obtains on-disk scratch space for containerd state and local-path volumes without destroying the operator's existing OS/data.
const ( // ScratchNone keeps everything in RAM (smallest footprint). ScratchNone ScratchStrategy = "none" // ScratchExisting mounts a pre-existing partition the operator designates by // LABEL=, UUID= or /dev/ path. Never formatted unless ForceFormat is set. ScratchExisting ScratchStrategy = "existing" // ScratchFreeSpace carves a new partition from unallocated space only. It // refuses to shrink or touch any existing partition. ScratchFreeSpace ScratchStrategy = "free-space" )