6.7 KiB
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, config, go-i18n, cli) requires the workspace file.
Optional (for actually running VMs):
- QEMU --
qemu-system-x86_64for 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.Mediumabstraction -- File system operations go throughio.Medium(fromgo-io) rather than directly callingosfunctions. This enables testing with mock file systems. Theio.Localsingleton is used for real file system access.- Compile-time interface checks -- Use
var _ Interface = (*Impl)(nil)to verify implementations at compile time (seesources/cdn.goandsources/github.go). - Context propagation -- All operations that might block accept a
context.Contextas their first parameter.
Adding a new hypervisor
- Create a new struct implementing the
Hypervisorinterface inhypervisor.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
}
-
Register it in
DetectHypervisor()andGetHypervisor()inhypervisor.go. -
Add tests following the
_Good/_Badnaming convention.
Adding a new image source
- Create a new struct implementing
ImageSourcein thesources/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
}
- Wire it into
NewImageManager()indevenv/images.gounder the appropriate source selector.
Adding a new LinuxKit template
Built-in template
- Create a
.ymlfile in thetemplates/directory. - Add an entry to
builtinTemplatesintemplates.go. - The file will be embedded via the
//go:embed templates/*.ymldirective.
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