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

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, run go work sync if module resolution fails.
  • gh CLI (optional) -- required only for core monitor commands.

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-After header)
  • 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. Run go fmt ./... or core go fmt before committing.

Adding a New Provider Client

To add support for a new infrastructure provider:

  1. Create a new file (e.g. vultr.go) in the package root.
  2. 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
}
  1. Add internal helper methods (get, post, delete) that delegate to c.api.Do(req, result).
  2. Write tests using httptest.NewServer -- never call real APIs in tests.
  3. Follow the _Good/_Bad/_Ugly test naming convention.

Adding CLI Commands

CLI commands live in subdirectories of cmd/. Each command package:

  1. Calls cli.RegisterCommands(AddXyzCommands) in an init() function (see cmd/prod/cmd_commands.go).
  2. Defines a root *cli.Command with subcommands.
  3. Uses loadConfig() to auto-discover infra.yaml when 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