161 lines
5.5 KiB
Markdown
161 lines
5.5 KiB
Markdown
|
|
---
|
||
|
|
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
|