go-infra/docs/development.md

161 lines
5.5 KiB
Markdown
Raw Permalink Normal View History

---
title: Development
description: 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:
```bash
cd /Users/snider/Code/core/go-infra
go build ./...
```
## Running Tests
```bash
# 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:
```bash
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`:
```go
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
}
```
3. Add internal helper methods (`get`, `post`, `delete`) that delegate to `c.api.Do(req, result)`.
4. Write tests using `httptest.NewServer` -- never call real APIs in tests.
5. 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