go-container/docs/development.md
Snider 3ed0cf4907 docs: add human-friendly documentation
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 13:02:40 +00:00

6.7 KiB

title description
Development How to build, test, and contribute to go-container.

Development

Prerequisites

  • Go 1.26+ -- The module uses Go 1.26 features.
  • Go workspace -- This module is part of a Go workspace at ~/Code/go.work. Local development of sibling modules (go-io, go-config, go-i18n, cli) requires the workspace file.

Optional (for actually running VMs):

  • QEMU -- qemu-system-x86_64 for running LinuxKit images on any platform.
  • Hyperkit -- macOS-only alternative hypervisor.
  • LinuxKit -- For building images from templates (linuxkit build).
  • GitHub CLI (gh) -- For the GitHub image source.

Running tests

# All tests
go test ./...

# With race detector
go test -race ./...

# Single test by name
go test -run TestState_Add_Good ./...

# Single package
go test ./sources/
go test ./devenv/

Tests use testify for assertions. Most tests are self-contained and do not require a running hypervisor -- they test command construction, state management, template parsing, and configuration loading in isolation.

Test naming convention

Tests follow a _Good, _Bad, _Ugly suffix pattern:

Suffix Meaning
_Good Happy path -- valid inputs, expected success
_Bad Expected error conditions -- invalid inputs, missing resources
_Ugly Edge cases, panics, and boundary conditions

Examples from the codebase:

func TestNewState_Good(t *testing.T)           { /* creates state successfully */ }
func TestLoadState_Bad_InvalidJSON(t *testing.T) { /* handles corrupt state file */ }
func TestGetHypervisor_Bad_Unknown(t *testing.T) { /* rejects unknown hypervisor name */ }

Project structure

go-container/
  container.go          # Container struct, Manager interface, Status, RunOptions, ImageFormat
  hypervisor.go         # Hypervisor interface, QemuHypervisor, HyperkitHypervisor, DetectHypervisor
  hypervisor_test.go
  linuxkit.go           # LinuxKitManager (Manager implementation), followReader for log tailing
  linuxkit_test.go
  state.go              # State persistence (containers.json), log paths
  state_test.go
  templates.go          # Template listing, loading, variable substitution, user template scanning
  templates_test.go
  templates/            # Embedded LinuxKit YAML templates
    core-dev.yml
    server-php.yml
  sources/
    source.go           # ImageSource interface, SourceConfig
    source_test.go
    cdn.go              # CDNSource implementation
    cdn_test.go
    github.go           # GitHubSource implementation
    github_test.go
  devenv/
    devops.go           # DevOps orchestrator, Boot, Stop, Status, ImageName, ImagePath
    devops_test.go
    config.go           # Config, ImagesConfig, LoadConfig from ~/.core/config.yaml
    config_test.go
    images.go           # ImageManager, Manifest, Install, CheckUpdate
    images_test.go
    shell.go            # Shell (SSH and serial console)
    shell_test.go
    serve.go            # Serve (mount project, auto-detect serve command)
    serve_test.go
    test.go             # Test (auto-detect test command, .core/test.yaml)
    test_test.go
    claude.go           # Claude sandbox session with auth forwarding
    claude_test.go
    ssh_utils.go        # Host key scanning for ~/.core/known_hosts
  cmd/vm/
    cmd_vm.go           # CLI registration (init + AddVMCommands)
    cmd_commands.go     # Package doc
    cmd_container.go    # run, ps, stop, logs, exec commands
    cmd_templates.go    # templates, templates show, templates vars commands

Coding standards

  • UK English in all strings, comments, and documentation (colour, organisation, honour).
  • Strict typing -- All function parameters and return values are typed. No interface{} without justification.
  • Error wrapping -- Use fmt.Errorf("context: %w", err) for all error returns.
  • io.Medium abstraction -- File system operations go through io.Medium (from go-io) rather than directly calling os functions. This enables testing with mock file systems. The io.Local singleton is used for real file system access.
  • Compile-time interface checks -- Use var _ Interface = (*Impl)(nil) to verify implementations at compile time (see sources/cdn.go and sources/github.go).
  • Context propagation -- All operations that might block accept a context.Context as their first parameter.

Adding a new hypervisor

  1. Create a new struct implementing the Hypervisor interface in hypervisor.go:
type MyHypervisor struct {
    Binary string
}

func (h *MyHypervisor) Name() string { return "my-hypervisor" }
func (h *MyHypervisor) Available() bool { /* check if binary exists */ }
func (h *MyHypervisor) BuildCommand(ctx context.Context, image string, opts *HypervisorOptions) (*exec.Cmd, error) {
    // Build and return exec.Cmd
}
  1. Register it in DetectHypervisor() and GetHypervisor() in hypervisor.go.

  2. Add tests following the _Good/_Bad naming convention.

Adding a new image source

  1. Create a new struct implementing ImageSource in the sources/ package:
type MySource struct {
    config SourceConfig
}

var _ ImageSource = (*MySource)(nil)  // Compile-time check

func (s *MySource) Name() string { return "my-source" }
func (s *MySource) Available() bool { /* check prerequisites */ }
func (s *MySource) LatestVersion(ctx context.Context) (string, error) { /* fetch version */ }
func (s *MySource) Download(ctx context.Context, m io.Medium, dest string, progress func(downloaded, total int64)) error {
    // Download image to dest
}
  1. Wire it into NewImageManager() in devenv/images.go under the appropriate source selector.

Adding a new LinuxKit template

Built-in template

  1. Create a .yml file in the templates/ directory.
  2. Add an entry to builtinTemplates in templates.go.
  3. The file will be embedded via the //go:embed templates/*.yml directive.

User template

Place a .yml file in either:

  • .core/linuxkit/ relative to your project root (workspace-scoped)
  • ~/.core/linuxkit/ in your home directory (global)

The first comment line in the YAML file is extracted as the template description.

File system paths

All persistent data lives under ~/.core/:

Path Purpose
~/.core/containers.json Container state registry
~/.core/logs/ Per-container log files
~/.core/images/ Downloaded dev environment images
~/.core/images/manifest.json Image version manifest
~/.core/config.yaml Global configuration
~/.core/known_hosts SSH host keys for dev VMs
~/.core/linuxkit/ User-defined LinuxKit templates

The CORE_IMAGES_DIR environment variable overrides the default images directory.

Licence

EUPL-1.2