usb

package
v0.61.0 Latest Latest
Warning

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

Go to latest
Published: Jun 7, 2026 License: MIT Imports: 18 Imported by: 0

Documentation

Overview

Package usb generates the artifacts needed to boot a bare-metal laptop off a Ventoy 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, Ubuntu/cloud-init, prebaked image) only re-packages that same script for its boot medium, so the join *logic* lives in exactly one place.

Index

Constants

View Source
const (
	FCOSStreamURL     = "https://builds.coreos.fedoraproject.org/streams/stable.json"
	UbuntuReleasesDir = "https://releases.ubuntu.com/24.04/"
	// CoreosInstallerImage embeds the Ignition; coreos-installer ships no Linux
	// binary on GitHub, so we run its official container in the builder VM.
	CoreosInstallerImage = "quay.io/coreos/coreos-installer:release"
)

Runtime ISO resolution sources (x86_64; the feature targets x86 laptops). URLs are resolved at build time inside the VM so they never go stale: FCOS from its stream metadata, Ubuntu from the releases directory listing (point releases are pruned, so a pinned URL would 404).

View Source
const (
	VentoyVersion = "1.0.99"
	ButaneVersion = "0.23.0"
)

Tool versions fetched into the builder VM as binaries (not apt packages).

View Source
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`.

View Source
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.

View Source
const FCOSImage = "fedora-coreos-live.x86_64.iso"

FCOSImage is the Fedora CoreOS live ISO filename the FCOS boot entries expect on the Ventoy stick. The operator supplies the actual ISO (it is hundreds of MB and not vendored).

View Source
const UbuntuImage = "ubuntu-24.04-live-server-amd64.iso"

UbuntuImage is the Ubuntu live-server ISO the Ubuntu boot entries expect. The server ISO has first-class cloud-init/autoinstall support and the broadest laptop Wi-Fi/hardware coverage, making it the Wi-Fi-friendly renderer.

Variables

This section is empty.

Functions

func Flash added in v0.61.0

func Flash(ctx context.Context, imagePath, device string, confirm func() bool) error

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

func ParseSizeMB(s string) (int, error)

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 the full Ventoy disk image. Pure text (golden-tested); its execution against real Ventoy 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 Ventoy 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

func RenderJoinScript(s JoinSpec) (string, error)

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

func ValidateFlashTarget(d DiskInfo) error

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).

func VentoyJSON

func VentoyJSON(entries []BootEntry) ([]byte, error)

VentoyJSON renders a deterministic ventoy.json from the boot entries. It aliases each base image and, for cloud-init/autoinstall entries, registers their templates so Ventoy offers the ephemeral/install choice at boot.

Note: ventoy.json is a field-tunable starting point. FCOS Ignition is delivered by embedding it into the live ISO (coreos-installer iso ignition embed) at stick-prep time, so it needs no Ventoy plugin entry.

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

type ArtifactSink interface {
	Add(Artifact) error
}

ArtifactSink receives the files a renderer produces.

type AssemblyParams added in v0.61.0

type AssemblyParams struct {
	BootSizeMB  int
	TotalSizeMB int
	ISOs        []ISOSpec
	ButaneVM    string // path inside the VM to the FCOS Butane (.bu) to compile + embed
	PayloadVM   string // path inside the VM to staged ventoy.json + seeds
	ImageVM     string // path inside the VM for the output .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) Modes

func (BakedRenderer) Modes() []Mode

Modes implements Renderer.

func (BakedRenderer) Name

func (BakedRenderer) Name() string

Name implements Renderer.

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    // shown in the Ventoy boot menu
	BaseImage     string    // ISO/IMG filename this entry boots (operator supplies)
	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 line in the Ventoy menu 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|ubuntu|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 ventoy.json plus 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 Ventoy image assembly inside a Lima VM.

func NewBuilder added in v0.61.0

func NewBuilder(vmName string) *Builder

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

func (b *Builder) EnsureBuilderVM(ctx context.Context) error

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

func ResolveCoordinates(ctx context.Context, cfg *config.Config) (Coordinates, error)

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.

func (DirSink) Add

func (d DirSink) Add(a Artifact) error

Add implements ArtifactSink. Paths use forward slashes and are written relative to Root; parent directories are created as needed.

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

func ParseDiskutilInfo(out string) DiskInfo

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) Modes

func (FCOSRenderer) Modes() []Mode

Modes implements Renderer.

func (FCOSRenderer) Name

func (FCOSRenderer) Name() string

Name implements Renderer.

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 ISOSpec added in v0.61.0

type ISOSpec struct {
	Name          string // "fcos" | "ubuntu"
	URL           string
	Filename      string
	Resolver      string
	EmbedIgnition bool
}

ISOSpec describes one OS image placed on the stick. Resolver selects how the download URL is obtained: "fcos" (stream metadata), "ubuntu" (releases dir), or "" (use URL verbatim).

func DefaultISOSpecs added in v0.61.0

func DefaultISOSpecs(names []string) []ISOSpec

DefaultISOSpecs returns the built-in ephemeral ISO specs for the requested names (subset of "fcos", "ubuntu"). Filenames match the renderer BaseImage constants so the on-stick names line up with ventoy.json.

type Injection

type Injection struct {
	Kind        InjectionKind
	PayloadPath string // relative path of the payload artifact in the sink
	Notes       string // operator-facing notes (also surfaced in ventoy.json)
}

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

	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.

func (JoinSpec) Validate

func (s JoinSpec) Validate() error

Validate checks that the spec is internally consistent and safe.

type Mode

type Mode string

Mode is the node lifecycle a boot entry provisions.

const (
	// ModeEphemeral runs the node from RAM; the internal disk is not installed
	// to. The node disappears on reboot/unplug.
	ModeEphemeral Mode = "ephemeral"
	// ModeInstall installs the OS to the internal disk for a permanent node.
	ModeInstall Mode = "install"
)

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

func RendererByName(name string) (Renderer, error)

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"
)

type UbuntuRenderer

type UbuntuRenderer struct{}

UbuntuRenderer packages the canonical join script as a cloud-init NoCloud seed (ephemeral) or a subiquity autoinstall (install). Chosen when broad hardware support matters more than FCOS's immutability.

func (UbuntuRenderer) Modes

func (UbuntuRenderer) Modes() []Mode

Modes implements Renderer.

func (UbuntuRenderer) Name

func (UbuntuRenderer) Name() string

Name implements Renderer.

func (UbuntuRenderer) Render

func (UbuntuRenderer) Render(spec JoinSpec, m Mode, sink ArtifactSink) ([]BootEntry, error)

Render implements Renderer. It emits a NoCloud seed (user-data + meta-data) and the raw join script. The build/assembler layer points the kernel's cloud-init datasource at the seed directory.

Jump to

Keyboard shortcuts

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