9.8 KiB
| title | description |
|---|---|
| Architecture | Internal design of go-container -- types, data flow, hypervisor abstraction, state management, and template engine. |
Architecture
go-container is organised into three packages with clear responsibilities. The root container package owns the core abstractions. The devenv package composes those abstractions into a higher-level development environment. The sources package provides pluggable image download backends.
container (root)
|-- Manager interface + LinuxKitManager implementation
|-- Hypervisor interface (QEMU, Hyperkit)
|-- State (persistent container registry)
|-- Template engine (embedded + user templates)
|
+-- devenv/
| |-- DevOps orchestrator
| |-- ImageManager (download, manifest, update checks)
| |-- Shell, Serve, Test, Claude sessions
| +-- Config (from ~/.core/config.yaml)
|
+-- sources/
|-- ImageSource interface
|-- CDNSource
+-- GitHubSource
Key types
Container
The central data structure representing a running or stopped VM instance.
type Container struct {
ID string `json:"id"`
Name string `json:"name,omitempty"`
Image string `json:"image"`
Status Status `json:"status"`
PID int `json:"pid"`
StartedAt time.Time `json:"started_at"`
Ports map[int]int `json:"ports,omitempty"`
Memory int `json:"memory,omitempty"`
CPUs int `json:"cpus,omitempty"`
}
Each container gets a unique 8-character hex ID generated from crypto/rand. Status transitions follow running -> stopped | error.
Manager interface
The Manager interface defines the contract for container lifecycle management:
type Manager interface {
Run(ctx context.Context, image string, opts RunOptions) (*Container, error)
Stop(ctx context.Context, id string) error
List(ctx context.Context) ([]*Container, error)
Logs(ctx context.Context, id string, follow bool) (io.ReadCloser, error)
Exec(ctx context.Context, id string, cmd []string) error
}
The only implementation is LinuxKitManager, which delegates VM execution to a Hypervisor and tracks state in a State store.
Hypervisor interface
Abstracts the underlying virtualisation technology:
type Hypervisor interface {
Name() string
Available() bool
BuildCommand(ctx context.Context, image string, opts *HypervisorOptions) (*exec.Cmd, error)
}
Two implementations exist:
| Implementation | Platform | Acceleration | Binary |
|---|---|---|---|
QemuHypervisor |
All | KVM (Linux), HVF (macOS) | qemu-system-x86_64 |
HyperkitHypervisor |
macOS only | Native macOS hypervisor | hyperkit |
DetectHypervisor() auto-selects the best available hypervisor. On macOS it prefers Hyperkit, falling back to QEMU. On Linux it uses QEMU with KVM if /dev/kvm is present.
Data flow: running a container
When LinuxKitManager.Run() is called, the following sequence occurs:
-
Validate -- Checks the image file exists via
io.Mediumand detects its format from the file extension (.iso,.qcow2,.vmdk,.raw,.img). -
Generate ID -- Creates an 8-character hex identifier using
crypto/rand. -
Apply defaults -- Memory defaults to 1024 MB, CPUs to 1, SSH port to 2222.
-
Build command -- Delegates to the
Hypervisor.BuildCommand()method, which constructs the full command line including:- Memory and CPU allocation
- Disk image attachment (format-specific flags)
- Network with port forwarding (SSH + user-defined ports)
- 9p volume shares (QEMU only)
- Hardware acceleration flags
-
Start process -- In detached mode, stdout/stderr are redirected to a log file under
~/.core/logs/<id>.log, and a background goroutine monitors the process for exit. In foreground mode, output is tee'd to both the log file and the terminal. -
Persist state -- The container record is written to
~/.core/containers.jsonvia theStatestore. -
Monitor -- For detached containers,
waitForExit()runs in a goroutine, updating the container status tostoppedorerrorwhen the process terminates.
State management
The State struct provides a thread-safe, JSON-persisted container registry:
type State struct {
Containers map[string]*Container `json:"containers"`
mu sync.RWMutex
filePath string
}
Key design decisions:
- Copy-on-read:
Get()andAll()return copies of container structs to prevent data races when callers modify the returned values. - Write-through: Every mutation (
Add,Update,Remove) immediately persists to disk viaSaveState(). - Auto-create:
LoadState()returns an empty state if the file does not exist, andSaveState()creates parent directories as needed.
Default paths:
| Path | Purpose |
|---|---|
~/.core/containers.json |
Container state file |
~/.core/logs/<id>.log |
Per-container log files |
Stopping a container
LinuxKitManager.Stop() performs a graceful shutdown:
- Sends
SIGTERMto the hypervisor process. - Waits up to 10 seconds for the process to exit.
- If the process does not exit in time, sends
SIGKILL. - Respects context cancellation throughout -- if the context is cancelled, the process is killed immediately.
Template engine
LinuxKit templates are YAML files that define a complete VM image configuration (kernel, init, services, files). The template engine adds variable substitution on top.
Variable syntax
Two forms are supported:
${VAR}-- Required variable. Produces an error if not provided.${VAR:-default}-- Optional variable with a default value.
Resolution order
GetTemplate(name) searches for templates in this order:
- Embedded templates -- Compiled into the binary via
//go:embed templates/*.yml. These always take precedence. - Workspace templates --
.core/linuxkit/relative to the current working directory. - User templates --
~/.core/linuxkit/in the user's home directory.
User-defined templates that share a name with a built-in template are ignored (built-ins win).
Variable extraction
ExtractVariables(content) parses a template and returns two collections:
- A sorted slice of required variable names (those using
${VAR}syntax with no default). - A map of optional variable names to their default values (those using
${VAR:-default}syntax).
This powers the core vm templates vars <name> command.
Development environment (devenv)
The DevOps struct in the devenv package composes the lower-level primitives into a complete development workflow.
Boot sequence
- Checks the dev image is installed (platform-specific qcow2 file:
core-devops-{os}-{arch}.qcow2). - Launches the image via
LinuxKitManager.Run()in detached mode with 4096 MB RAM, 2 CPUs, and SSH on port 2222. - Polls for up to 60 seconds until the VM's SSH host key can be scanned, then writes it to
~/.core/known_hosts.
Shell access
Two modes are available:
- SSH (default) -- Connects via
ssh -A -p 2222 root@localhostwith agent forwarding and strict host key checking against~/.core/known_hosts. - Serial console -- Attaches to the QEMU serial console socket via
socat.
Project mounting
Projects are mounted into the VM at /app using a reverse SSHFS tunnel. The VM opens an SSH reverse tunnel back to the host (port 10000) and mounts the host directory via SSHFS.
Auto-detection
Several operations auto-detect the project type by inspecting files on disk:
| File detected | Serve command | Test command |
|---|---|---|
artisan |
php artisan octane:start |
-- |
package.json with dev script |
npm run dev -- --host 0.0.0.0 |
npm test |
composer.json with test script |
frankenphp php-server |
composer test |
go.mod |
go run . |
go test ./... |
manage.py |
python manage.py runserver |
-- |
pytest.ini or pyproject.toml |
-- | pytest |
Auto-detection can be overridden with .core/test.yaml for tests or explicit --command flags.
Claude integration
DevOps.Claude() starts a sandboxed Claude session inside the VM:
- Auto-boots the dev environment if not running.
- Mounts the project directory at
/app. - Forwards authentication credentials (Anthropic API key, GitHub CLI config, SSH agent, git identity) based on configurable options.
- Launches
claudeinside the VM via SSH with agent forwarding.
Image sources (sources)
The ImageSource interface defines how dev environment images are downloaded:
type ImageSource interface {
Name() string
Available() bool
LatestVersion(ctx context.Context) (string, error)
Download(ctx context.Context, m io.Medium, dest string, progress func(downloaded, total int64)) error
}
| Source | Backend | Availability check |
|---|---|---|
CDNSource |
HTTP GET from a configured CDN URL | CDN URL is configured |
GitHubSource |
gh release download via the GitHub CLI |
gh is installed and authenticated |
The ImageManager in devenv maintains a manifest.json in ~/.core/images/ that tracks installed image versions, SHA256 checksums, download timestamps, and source names. When the source is set to "auto" (the default), it tries GitHub first, then CDN.
Configuration
The devenv package reads ~/.core/config.yaml via the config library:
version: 1
images:
source: auto # auto | github | cdn
github:
repo: host-uk/core-images
registry:
image: ghcr.io/host-uk/core-devops
cdn:
url: https://cdn.example.com/images
If the file does not exist, sensible defaults are used.
Licence
EUPL-1.2