This document describes the internal architecture of go-microvm: how it turns an OCI container image into a running microVM, the two-process model, networking, state management, security measures, and extension points.
- The Two-Process Model
- Hypervisor Backend Abstraction
- OCI Image Pipeline
- Image Caching
- .krun_config.json
- Networking
- Extension Points
- Preflight Check System
- Guest-Side Packages
- State Management
- Security Model
- SSH Utilities
libkrun's krun_start_enter() function takes over the calling process. On
success it never returns -- the process becomes the VM supervisor and
eventually calls exit() when the guest shuts down. This means we cannot call
it from a normal Go application without losing the Go runtime entirely.
go-microvm solves this with two processes:
+----------------------------------+ +----------------------------------+
| Your application | | go-microvm-runner |
| (links go-microvm library) | | (CGO binary, links libkrun) |
| | | |
| 1. Preflight checks | spawn | 1. Parse Config from argv[1] |
| 2. Pull & cache OCI image |------>| 2. Validate (rootfs, vCPUs>0) |
| 3. Extract layers to rootfs | JSON | 3. krun.CreateContext() |
| 4. Run rootfs hooks | config| 4. SetVMConfig(vCPUs, RAM) |
| 5. Write .krun_config.json | | 5. SetRoot(rootfsPath) |
| 6. Start networking (if custom)| | 6. Setup networking |
| 7. Spawn go-microvm-runner | | 7. AddVirtioFS (for each mount) |
| 8. Run post-boot hooks | | 8. SetConsoleOutput(logPath) |
| 9. Return *VM handle | | 9. krun_start_enter() |
| | | (NEVER RETURNS ON SUCCESS) |
| Pure Go, no CGO | | |
| Monitors runner PID | | Process becomes VM supervisor |
+----------------------------------+ +----------------------------------+
| |
| SIGTERM (graceful) / SIGKILL (30s timeout) |
+--------------------------------------------->|
The library is pure Go with no CGO dependency. It performs the following steps
in microvm.Run():
-
Preflight checks -- Runs all registered
preflight.Checkervalidations. Built-in checks verify KVM/HVF access, disk space, and system resources. Custom checks can be added viaWithPreflightChecks(). Required check failures abort the pipeline; non-required failures log warnings. -
Image pull and cache -- Uses an
ImageFetcherinterface to retrieve the OCI image. The default fetcher tries the local Docker/Podman daemon first, then falls back to pulling from a remote registry. Callers can provide a custom fetcher viaWithImageFetcher(). After fetching, computes the manifest digest for cache lookup. On cache miss, flattens layers viamutate.Extract()and extracts the tar stream to a temporary directory with full security checks. -
Rootfs hooks -- Runs caller-provided
RootFSHookfunctions in registration order. Each receives the rootfs path and parsedOCIConfig. -
Write .krun_config.json -- Constructs
KrunConfigfrom the OCI image config (Entrypoint, Cmd, Env, WorkingDir), applyingWithInitOverrideif set. Writes the JSON file to/.krun_config.jsonin the rootfs. -
Start networking -- Networking follows one of two paths:
- Default (no
WithNetProvider): Port forwards are passed to the runner viarunner.Config. The runner creates an in-process VirtualNetwork (gvisor-tap-vsock) connected via a socketpair. Networking lives in the runner process alongside the VM. - Custom provider (
WithNetProvider): Callsnet.Provider.Start()which creates the network stack in the caller's process, starts a Unix socket listener, and returns once ready. The socket path is passed to the runner forkrun_add_net_unixstream.
The
net/hostedpackage provides a ready-made hosted provider that runs the VirtualNetwork in the caller's process and supports HTTP services on the gateway IP. - Default (no
-
Start VM via backend -- The
hypervisor.Backendhandles rootfs preparation and VM launch. The default libkrun backend serializesrunner.Configas JSON and spawnsgo-microvm-runneras a detached subprocess (setsidfor new session). The runner is located by searching: explicit path, system PATH, then next to the calling executable. Custom backends can be provided viaWithBackend(). -
Post-boot hooks -- Runs caller-provided
PostBootHookfunctions. If any hook fails, the VM is stopped and the error is returned.
The runner binary (runner/cmd/go-microvm-runner/main.go) is intentionally
minimal. It is a thin translation layer between JSON config and the libkrun
C API:
- Parse the JSON config from
os.Args[1] - Validate: rootfs path exists and is a directory, vCPUs > 0, RAM > 0
- Create a libkrun context via
krun.CreateContext() - Configure vCPUs and RAM via
SetVMConfig() - Set the root filesystem via
SetRoot() - Set up networking:
- If
NetSockPathis set (custom provider), connect viaAddNetUnixStream() - If
PortForwardsis set (default path), create an in-process VirtualNetwork with a socketpair and pass the VM-side fd to libkrun
- If
- Add virtio-fs mounts via
AddVirtioFS()(for each mount) - Set console output via
SetConsoleOutput()(if log path provided) - Call
krun_start_enter()which takes over the process
The runner does NOT call SetExec(). Instead, libkrun's built-in init process
(PID 1 in the guest) reads /.krun_config.json from the rootfs to determine
what program to execute. This is the same mechanism used by krunvm.
The runner package provides two interfaces for testability and extensibility:
// ProcessHandle abstracts a running process.
type ProcessHandle interface {
Stop(ctx context.Context) error
IsAlive() bool
PID() int
}
// Spawner abstracts subprocess creation.
type Spawner interface {
Spawn(ctx context.Context, cfg Config) (ProcessHandle, error)
}DefaultSpawner is the production implementation that calls SpawnProcess().
The libkrun backend accepts a custom spawner via libkrun.WithSpawner() for
testing or to customize how the runner subprocess is launched.
The hypervisor package defines the Backend interface that decouples go-microvm
from any specific hypervisor implementation:
type Backend interface {
Name() string
PrepareRootFS(ctx context.Context, rootfsPath string, initCfg InitConfig) (string, error)
Start(ctx context.Context, cfg VMConfig) (VMHandle, error)
}
type VMHandle interface {
Stop(ctx context.Context) error
IsAlive() bool
ID() string
}The default backend (hypervisor/libkrun) implements Backend by:
- PrepareRootFS: Writes
/.krun_config.jsonto the rootfs fromInitConfig - Start: Converts
VMConfigtorunner.Config, spawnsgo-microvm-runnervia therunner.Spawnerinterface, and returns aprocessHandlewrapping the subprocess
The libkrun backend accepts its own options via libkrun.NewBackend(opts...):
| Option | Description |
|---|---|
libkrun.WithRunnerPath(p) |
Path to go-microvm-runner binary |
libkrun.WithLibDir(d) |
Directory for libkrun/libkrunfw shared libraries |
libkrun.WithSpawner(s) |
Custom runner subprocess spawner (for testing) |
To support a different hypervisor, implement hypervisor.Backend and pass it
via microvm.WithBackend(). The backend handles all hypervisor-specific logic
while go-microvm handles OCI image management, networking, hooks, and lifecycle.
For migration details from the older top-level WithRunnerPath/WithLibDir/
WithSpawner options, see MIGRATION-BACKEND-ABSTRACTION.md.
The pipeline converts an OCI container image into a booted microVM. Each step includes security measures where applicable.
Image retrieval is abstracted behind the ImageFetcher interface:
type ImageFetcher interface {
Pull(ctx context.Context, ref string) (v1.Image, error)
}Three implementations are provided:
| Fetcher | Description |
|---|---|
RemoteFetcher |
Pulls from OCI registries via go-containerregistry/remote. Uses authn.DefaultKeychain for Docker/Podman credential stores. |
DaemonFetcher |
Pulls from the local Docker/Podman daemon via its Unix socket. Useful for locally-built images. |
FallbackFetcher |
Tries multiple fetchers in order; returns the first success. Errors are aggregated with errors.Join. |
The default (when no WithImageFetcher() is set) is
NewLocalThenRemoteFetcher(), which tries the daemon first, then falls
back to remote registry pull.
Step 1: PULL
ImageFetcher.Pull(imageRef) with context support
Default: tries local Docker/Podman daemon, then remote registry
Supports Docker Hub, GHCR, quay.io, private registries
Uses ~/.docker/config.json for authentication
|
Step 2: DIGEST
img.Digest() --> manifest digest (sha256:...)
Used as content-addressable cache key
|
Step 3: CACHE CHECK
cache.Get(digest) --> check if directory exists
Hit? --> return cached rootfs path + OCI config
Miss? --> continue to extraction
|
Step 4: FLATTEN
mutate.Extract(img) --> single tar stream
Merges all image layers into one unified filesystem
|
Step 5: EXTRACT
extractTar(reader, tmpDir) --> rootfs directory
Security checks at this step:
a. io.LimitedReader caps total extraction at 30 GiB
b. sanitizeTarPath() rejects absolute paths and ".." traversal
c. mkdirAllNoSymlink() refuses to follow symlinks when creating dirs
d. validateNoSymlinkLeaf() prevents writing through symlinks
e. Hardlink sources validated to stay within rootfs boundary
f. Unsupported entry types (char/block devices, fifos) are skipped
|
Step 6: CACHE STORE
cache.Put(digest, tmpDir) --> atomic os.Rename to cache dir
If another process already cached this digest, discard our extraction
|
Step 7: ROOTFS HOOKS
hook(rootfsPath, ociConfig) for each registered hook
Runs in registration order; any error aborts the pipeline
|
Step 8: KRUN CONFIG
Write /.krun_config.json to rootfs:
{
"Cmd": [...], // from OCI or WithInitOverride
"Env": [...], // PATH default + OCI env vars
"WorkingDir": "/" // from OCI or default "/"
}
|
Step 9: NETWORKING
Default: port forwards passed to runner config (runner creates VirtualNetwork)
Custom provider: net.Provider.Start() in caller's process, socket path to runner
|
Step 10: SPAWN
runner.Spawner.Spawn() --> go-microvm-runner subprocess
Detached (setsid), stdout/stderr redirected to vm.log
|
Step 11: POST-BOOT
hook(ctx, vm) for each registered hook
Common: SSH wait, config push, health check
Images are cached by manifest digest under the data directory:
~/.config/go-microvm/cache/
sha256-abc123def456.../ <-- extracted rootfs for this digest
sha256-789012345678.../ <-- another cached image
The cache key is the manifest digest (e.g., sha256:abc123...). The colon is
replaced with a hyphen for filesystem safety: sha256-abc123....
cache.Get(digest) checks if the directory exists via os.Stat(). If it
exists and is a directory, the cache hit returns the path.
cache.Put(digest, tmpDir) uses os.Rename() for atomicity. If two
concurrent pulls for the same image race:
- Both extract to separate temporary directories
- The first to call
Renamesucceeds - The second detects the destination already exists and discards its extraction
via
os.RemoveAll()
This ensures no partial writes and no corruption from concurrent access.
The cache directory defaults to ~/.config/go-microvm/cache/. It can be
customized via WithDataDir() (which sets the cache under $dataDir/cache/)
or directly via WithImageCache(image.NewCache("/custom/path")).
libkrun's built-in init process reads /.krun_config.json from the root of
the guest filesystem to determine what program to execute. go-microvm constructs
this file from the OCI image config with optional overrides.
{
"Cmd": ["/bin/sh", "-c", "echo hello"],
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"HOME=/root"
],
"WorkingDir": "/"
}| Field | Source | Override |
|---|---|---|
Cmd |
OCI Entrypoint + Cmd concatenated | WithInitOverride(cmd...) replaces entirely |
Env |
Default PATH + OCI image Env |
None (extend via rootfs hook) |
WorkingDir |
OCI WorkingDir, or / if unset |
None |
- libkrun's init process starts as PID 1 in the guest
- It reads
/.krun_config.json - It sets the environment variables from
Env - It changes to
WorkingDir - It executes
Cmd[0]withCmd[1:]as arguments
This is the same mechanism used by krunvm and podman machine.
go-microvm uses a userspace network stack powered by gvisor-tap-vsock. All VM traffic flows as Ethernet frames with no kernel networking between host and guest.
There are two networking modes:
-
Default (runner-side): When no
WithNetProvider()is set, the runner process creates an in-process VirtualNetwork connected to libkrun via a socketpair. Port forwards are configured directly in the runner. This is the simplest path -- no external process or socket needed. -
Hosted (caller-side): When
WithNetProvider()is set, the network stack runs in the caller's process and exposes a Unix socket that the runner connects to. Thenet/hostedpackage provides a ready-made implementation that also supports HTTP services on the gateway IP.
The networking layer is abstracted behind the net.Provider interface,
allowing alternative implementations. An optional frame-level firewall can
be enabled via WithFirewallRules().
Network topology constants (subnet, gateway IP, guest IP, MAC, MTU) are
centralized in the net/topology package and shared by both the runner
and the hosted provider.
For a thorough deep dive on the networking and firewall subsystem, see docs/NETWORKING.md.
+---------------------------------------------------+
| Host Machine |
| |
| +---------------+ Unix socket +-----------+ |
| | VirtualNetwork|---(SOCK_STREAM)->| libkrun | |
| | (in-process) | 4-byte BE len | virtio- | |
| | | prefix frames | net | |
| | Gateway: | | | |
| | 192.168.127.1 | +-----------+ |
| | | | |
| | DHCP server | +----v-----+ |
| | DNS server | | Guest VM | |
| | Port forwards | | | |
| +---------------+ | eth0: | |
| | | 192.168. | |
| | Port forwards: | 127.2 | |
| | localhost:8080 | | |
| +-----> guest:80 +----------+ |
| | localhost:2222 |
| +-----> guest:22 |
+---------------------------------------------------+
| Property | Value |
|---|---|
| Gateway | 192.168.127.1 (VirtualNetwork, in-process) |
| Guest IP | 192.168.127.2 (DHCP assigned) |
| Subnet | 192.168.127.0/24 |
| Socket type | Unix domain, SOCK_STREAM |
| Wire format | 4-byte big-endian length prefix + Ethernet frame |
| DHCP | Built into VirtualNetwork |
| DNS | Built into VirtualNetwork |
| Port forwarding | TCP, host-to-guest only |
type Provider interface {
// Start launches the network provider. Must block until ready.
Start(ctx context.Context, cfg Config) error
// SocketPath returns the Unix socket for virtio-net.
SocketPath() string
// Stop terminates the provider and cleans up.
Stop()
}Config contains:
LogDir-- directory for log filesForwards-- slice ofPortForward{Host, Guest}for TCP forwardingFirewallRules-- optional packet filtering rules for frame-level filteringFirewallDefaultAction-- default action when no rule matches (Allow or Deny)
To replace the default runner-side networking with a different backend
(e.g., passt, slirp4netns, a custom bridge, or the built-in hosted
provider), implement this interface and pass it via
microvm.WithNetProvider(). The SocketPath() return value is passed to
the runner as the Unix socket path for krun_add_net_unixstream.
The net/topology package centralizes all network layout constants:
| Constant | Value | Description |
|---|---|---|
Subnet |
192.168.127.0/24 |
Virtual network CIDR |
GatewayIP |
192.168.127.1 |
Host-side gateway (VirtualNetwork) |
GuestIP |
192.168.127.2 |
DHCP-assigned guest IP |
GatewayMAC |
5a:94:ef:e4:0c:ee |
Gateway MAC address |
MTU |
1500 |
Maximum transmission unit |
Both the runner's in-process networking and the hosted provider import these values to ensure a consistent topology.
The net/hosted package implements a net.Provider that runs the
VirtualNetwork in the caller's process rather than inside go-microvm-runner.
This enables callers to access the VirtualNetwork directly -- for example,
to create in-process TCP listeners via gonet that are reachable from the
guest VM without opening real host sockets.
p := hosted.NewProvider()
vm, err := microvm.Run(ctx, image,
microvm.WithNetProvider(p),
microvm.WithPorts(microvm.PortForward{Host: sshPort, Guest: 22}),
)
// p.VirtualNetwork() is now available for gonet listeners.Provider.AddService() registers HTTP handlers that listen inside the
virtual network on the gateway IP (192.168.127.1). Services are started
before the guest boots and are reachable from inside the VM:
p := hosted.NewProvider()
p.AddService(hosted.Service{Port: 4483, Handler: myHandler})
vm, err := microvm.Run(ctx, image, microvm.WithNetProvider(p))
// Guest can reach http://192.168.127.1:4483/The hosted provider exposes a Unix socket (hosted-net.sock in the data
directory) that go-microvm-runner connects to. When firewall rules are
configured, a firewall.Relay is inserted between the runner connection
and the VirtualNetwork to filter traffic. The relay is accessible via
Provider.Relay() for metrics.
microvm.Run()
|
+------------+------------+
| |
preflight.Checker net.Provider (optional)
(KVM, ports, resources, (hosted vnet,
disk space, custom) + optional firewall)
| |
v v
Pull & Extract Start networking
(via ImageFetcher) (only if custom provider)
| |
v |
RootFSHook(s) |
(inject files, SSH keys, |
TLS certs, config) |
| |
v v
Write .krun_config.json Socket ready
(WithInitOverride to |
replace OCI CMD) |
| |
+------------+------------+
|
Spawn runner
(via Spawner interface;
default: creates in-process
VirtualNetwork if no
custom provider)
|
v
PostBootHook(s)
(SSH wait, config push,
health checks, service mesh)
Inject validation before any work begins:
microvm.WithPreflightChecks(check1, check2)Modify the extracted filesystem before boot:
microvm.WithRootFSHook(func(rootfs string, cfg *image.OCIConfig) error {
// Write files, install keys, modify configs
return nil
})Replace the OCI ENTRYPOINT/CMD:
microvm.WithInitOverride("/sbin/my-init", "--flag")Replace the default runner-side networking with a custom provider:
microvm.WithNetProvider(myProvider)The net/hosted package provides a built-in hosted provider that runs the
VirtualNetwork in the caller's process:
p := hosted.NewProvider()
p.AddService(hosted.Service{Port: 4483, Handler: myHandler})
microvm.WithNetProvider(p)
// After Run(), p.VirtualNetwork() is available for gonet listeners.Run logic after the VM process is confirmed alive:
microvm.WithPostBoot(func(ctx context.Context, vm *microvm.VM) error {
// Wait for SSH, push config, verify health
return nil
})The preflight package provides an extensible system for running pre-boot
verification checks.
// Check represents a single preflight verification.
type Check struct {
Name string // Short identifier
Description string // Human-readable description
Run func(ctx context.Context) error // Nil on success, error on failure
Required bool // true = fatal, false = warning
}
// Checker runs preflight checks before VM creation.
type Checker interface {
RunAll(ctx context.Context) error // Run all checks, return error if any required fail
Register(check Check) // Add a check
}| Check | Platform | Required | Description |
|---|---|---|---|
kvm |
Linux | Yes | Verifies /dev/kvm exists, is a character device, and is read/write accessible. Error messages include remediation hints (modprobe commands, usermod). |
cap-chown |
Linux | No | Verifies the process has CAP_CHOWN (or runs as root) so extracted rootfs files get correct ownership. Without it, guest processes may see permission errors. |
disk-space |
Linux | No | Verifies at least 2.0 GB free disk space on the data directory filesystem. Walks up the directory tree to find an existing ancestor if the dir does not yet exist. |
resources |
Linux | No | Verifies the host has at least 1 CPU core and 1.0 GiB RAM. |
ports |
All | Yes | Verifies requested host ports are available for binding. Uses ss on Linux to identify the process holding a port. |
On macOS, the kvm, disk-space, and resources checks are either no-ops
or not registered. Hypervisor.framework is assumed available on Apple Silicon.
RunAll() executes checks in registration order. Required check failures are
collected into a combined error. Non-required failures are logged as warnings
via slog.Warn but do not prevent the pipeline from proceeding.
Pass checks via WithPreflightChecks():
microvm.Run(ctx, ref,
microvm.WithPreflightChecks(
preflight.PortCheck(8080, 2222),
preflight.Check{
Name: "my-check",
Description: "Check something",
Run: func(ctx context.Context) error { return nil },
Required: true,
},
),
)Custom checks are appended to (not replacing) the built-in platform defaults.
To replace the entire preflight checker (e.g., when the caller manages its own):
microvm.WithPreflightChecker(preflight.NewEmpty())The guest/ directory contains Linux-only packages (//go:build linux) that
run inside the guest VM, not on the host. These are used by custom init
processes to orchestrate guest boot.
| Package | Purpose |
|---|---|
guest/boot |
Orchestrates the guest boot sequence: mounts, networking, workspace, hardening, environment, SSH, capabilities. Functional options pattern for configuration. |
guest/mount |
Handles essential filesystem mounts (devtmpfs, sysfs, procfs) and workspace mounts |
guest/env |
Loads environment variables from /.go-microvm/env.sh |
guest/netcfg |
Guest-side network configuration (DHCP, routes) |
guest/harden |
VM kernel hardening: capability dropping (CAP_NET_ADMIN, etc.), CIS benchmark sysctls, PR_SET_NO_NEW_PRIVS |
guest/reaper |
Zombie process reaping (init process duties) |
guest/sshd |
Embedded SSH server implementation with session handling |
| Package | Purpose |
|---|---|
hooks |
RootFS hook factories: InjectAuthorizedKeys, InjectFile, InjectBinary, InjectEnvFile. Uses internal/pathutil for path traversal validation. |
extract |
Binary bundle caching with SHA-256 versioning, atomic extraction, and cross-process file locking. Used to cache and extract the go-microvm-runner binary and libraries. |
image/disk |
Streaming disk image download with retry support and decompression (gzip, bzip2, xz). |
The state package provides persistent VM state with file-based locking.
~/.config/go-microvm/ (or $GO_MICROVM_DATA_DIR, or WithDataDir path)
go-microvm-state.json <-- VM state (atomic JSON)
go-microvm-state.lock <-- flock for exclusive access
cache/
sha256-abc123.../ <-- cached rootfs by digest
sha256-def456.../
console.log <-- guest console output (kernel, init)
vm.log <-- go-microvm-runner stdout/stderr
hosted-net.sock <-- networking Unix socket (only with hosted provider)
{
"version": 1,
"active": true,
"name": "my-vm",
"image": "alpine:latest",
"cpus": 2,
"memory_mb": 1024,
"pid": 12345,
"created_at": "2025-01-15T10:00:00Z"
}| Field | Type | Description |
|---|---|---|
version |
int | State file format version (currently 1) |
active |
bool | Whether the VM is currently running |
name |
string | VM name |
image |
string | OCI image reference |
cpus |
uint32 | Number of vCPUs |
memory_mb |
uint32 | RAM in MiB |
pid |
int | Runner process ID (0 if not running) |
created_at |
RFC 3339 | When the state was first created |
The state.Manager provides atomic load-and-lock semantics:
LoadAndLock(ctx)acquires an exclusiveflockongo-microvm-state.lock- Reads and parses
go-microvm-state.json(or returns a default State if the file does not exist) - Returns a
LockedStatethat holds the lock
The lock is held until LockedState.Release() is called. Callers should
use defer to ensure the lock is always released:
mgr := state.NewManager(dataDir)
ls, err := mgr.LoadAndLock(ctx)
if err != nil {
return err
}
defer ls.Release()
ls.State.Active = true
ls.State.PID = proc.PID
return ls.Save()LoadAndLockWithRetry(ctx, timeout) wraps LoadAndLock with a retry loop
for cases where another process may hold the lock temporarily.
Load() reads the state without locking (read-only access).
LockedState.Save() ensures crash safety:
- Marshals the state to JSON with indentation
- Writes to a temporary file in the same directory (
state-*.json.tmp) - Calls
os.Rename()to atomically replacego-microvm-state.json
If a crash occurs during write, either the old state remains intact or the
new state is fully written -- never a partial file. The flock ensures only
one process writes at a time.
For a comprehensive security analysis including trust boundaries, guest escape blast radius, and hardening recommendations, see docs/SECURITY.md.
The ssh package provides two capabilities for guest communication:
GenerateKeyPair(keyDir) creates an ECDSA P-256 SSH key pair:
- Private key:
keyDir/id_ecdsawith 0600 permissions, PEM-encoded EC key - Public key:
keyDir/id_ecdsa.pubwith 0644 permissions, OpenSSH authorized_keys format
If the key files already exist, they are overwritten.
GetPublicKeyContent(publicKeyPath) reads the public key file and returns
its content as a string for inclusion in authorized_keys.
ssh.Client wraps golang.org/x/crypto/ssh with convenience methods:
| Method | Description |
|---|---|
Run(ctx, cmd) |
Execute a command, return combined stdout+stderr |
RunSudo(ctx, cmd) |
Execute via doas (used in Alpine-based VMs) |
RunStream(ctx, cmd, stdout, stderr) |
Execute with streaming I/O |
CopyTo(ctx, local, remote, mode) |
Upload a file via cat over SSH |
CopyFrom(ctx, remote, local) |
Download a file via cat over SSH |
WaitForReady(ctx) |
Poll SSH at 2s intervals until connection succeeds |
ShellEscape(s) wraps a string in single quotes with proper escaping for
safe use in shell commands.
The standard post-boot hook pattern uses WaitForReady:
microvm.WithPostBoot(func(ctx context.Context, vm *microvm.VM) error {
client := ssh.NewClient("127.0.0.1", 2222, "root", keyPath)
return client.WaitForReady(ctx)
})WaitForReady polls every 2 seconds, attempting a full SSH connection and
running true as a trivial command. It returns when the connection succeeds
or the context is cancelled. Connection timeout per attempt is 10 seconds.
Security note: The SSH client uses InsecureIgnoreHostKey() for host key
verification. This is acceptable because we trust the VM we just created -- it
was booted from an image we pulled and configured.