go-container/docs/development.md
Snider 6c4050486b refactor(container): migrate module path to dappco.re/go/core/container
Update module path from forge.lthn.ai/core/go-container to
dappco.re/go/core/container. Migrate import paths for go-io (v0.2.0),
go-log (v0.1.0), and go-i18n (v0.2.0) to their new dappco.re
equivalents. cli and config remain at forge.lthn.ai (not yet migrated).

Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-22 01:55:36 +00:00

197 lines
6.7 KiB
Markdown

---
title: Development
description: 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 (core/io, config, core/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
```bash
# 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:
```go
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 `core/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`:
```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
}
```
2. Register it in `DetectHypervisor()` and `GetHypervisor()` in `hypervisor.go`.
3. Add tests following the `_Good`/`_Bad` naming convention.
## Adding a new image source
1. Create a new struct implementing `ImageSource` in the `sources/` package:
```go
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
}
```
2. 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