5.5 KiB
| title | description |
|---|---|
| Development | How to build, test, and contribute to go-infra. |
Development
Prerequisites
- Go 1.26+
- Go workspace -- this module is part of the workspace at
~/Code/go.work. After cloning, rungo work syncif module resolution fails. ghCLI (optional) -- required only forcore monitorcommands.
Building
The library package (infra) has no binary output. The CLI commands in cmd/prod/ and cmd/monitor/ are compiled into the core binary via the forge.lthn.ai/core/cli module -- they are not standalone binaries.
To verify the package compiles:
cd /Users/snider/Code/core/go-infra
go build ./...
Running Tests
# All tests
go test ./...
# With race detector
go test -race ./...
# A specific test
go test -run TestAPIClient_Do_Good_Success
# Verbose output
go test -v ./...
If the core CLI is available:
core go test
core go test --run TestAPIClient_Do_Good_Success
Test Organisation
Tests follow the _Good, _Bad, _Ugly suffix convention:
| Suffix | Purpose | Example |
|---|---|---|
_Good |
Happy path -- expected successful behaviour | TestAPIClient_Do_Good_Success |
_Bad |
Expected error conditions -- invalid input, auth failures, exhausted retries | TestAPIClient_Do_Bad_ClientError |
_Ugly |
Edge cases -- context cancellation, malformed data, panics | TestAPIClient_Do_Ugly_ContextCancelled |
Test Approach
All API client tests use net/http/httptest.Server to mock HTTP responses. No real API calls are made during tests. The test servers simulate:
- Successful JSON responses
- HTTP error codes (400, 401, 403, 404, 500, 502, 503)
- Rate limiting (429 with
Retry-Afterheader) - Transient failures that succeed after retries
- Authentication verification (bearer tokens, basic auth, query parameters)
The config tests use Discover() to find a real infra.yaml in parent directories (skipped if not present) and also test error paths with nonexistent and malformed files.
Test Coverage by File
| File | Tests | Coverage Focus |
|---|---|---|
client_test.go |
20 tests | Constructor defaults/options, Do JSON decoding, DoRaw raw responses, retry on 5xx, no retry on 4xx, rate-limit handling, context cancellation, parseRetryAfter, integration with HCloud/CloudNS clients |
hetzner_test.go |
10 tests | HCloud/HRobot constructors, ListServers, JSON deserialisation of servers/load balancers/Robot servers, auth header verification, error responses |
cloudns_test.go |
16 tests | Constructor, auth params, raw HTTP calls, zone/record JSON parsing, CRUD round-trips, ACME challenge helpers, EnsureRecord logic (already correct / needs update / needs create), edge cases (empty body, empty map) |
config_test.go |
4 tests | Load with real config, missing file, invalid YAML, expandPath with tilde/absolute/relative paths |
Code Style
- UK English in all documentation, comments, and user-facing strings (colour, organisation, centre, serialisation).
- Strict typing -- all function parameters and return values have explicit types.
- Error wrapping -- use
fmt.Errorf("context: %w", err)to preserve error chains. - Formatting -- standard
gofmt. Rungo fmt ./...orcore go fmtbefore committing.
Adding a New Provider Client
To add support for a new infrastructure provider:
- Create a new file (e.g.
vultr.go) in the package root. - Define a client struct that embeds or holds an
*APIClient:
type VultrClient struct {
apiKey string
baseURL string
api *APIClient
}
func NewVultrClient(apiKey string) *VultrClient {
c := &VultrClient{
apiKey: apiKey,
baseURL: "https://api.vultr.com/v2",
}
c.api = NewAPIClient(
WithAuth(func(req *http.Request) {
req.Header.Set("Authorization", "Bearer "+c.apiKey)
}),
WithPrefix("vultr API"),
)
return c
}
- Add internal helper methods (
get,post,delete) that delegate toc.api.Do(req, result). - Write tests using
httptest.NewServer-- never call real APIs in tests. - Follow the
_Good/_Bad/_Uglytest naming convention.
Adding CLI Commands
CLI commands live in subdirectories of cmd/. Each command package:
- Calls
cli.RegisterCommands(AddXyzCommands)in aninit()function (seecmd/prod/cmd_commands.go). - Defines a root
*cli.Commandwith subcommands. - Uses
loadConfig()to auto-discoverinfra.yamlwhen needed.
The core binary picks up these commands via blank imports in its main package.
Project Structure
go-infra/
client.go Shared APIClient
client_test.go APIClient tests (20 tests)
config.go YAML config types + parser
config_test.go Config tests (4 tests)
hetzner.go HCloudClient + HRobotClient
hetzner_test.go Hetzner tests (10 tests)
cloudns.go CloudNSClient
cloudns_test.go CloudNS tests (16 tests)
cmd/
prod/
cmd_commands.go Command registration
cmd_prod.go Root 'prod' command + flags
cmd_status.go Parallel host health checks
cmd_setup.go Foundation setup pipeline (discover, lb, dns)
cmd_dns.go DNS record management
cmd_lb.go Load balancer management
cmd_ssh.go SSH into production hosts
monitor/
cmd_commands.go Command registration
cmd_monitor.go Security finding aggregation
go.mod
go.sum
CLAUDE.md
Licence
EUPL-1.2