[agent/claude] Migrate module path to dappco.re/go/core/infra. Update go.mo... #3

Closed
Virgil wants to merge 3 commits from agent/migrate-module-path-to-dappco-re-go-core into main
19 changed files with 172 additions and 53 deletions

4
.gitignore vendored
View file

@ -1,2 +1,4 @@
.core/
.idea/
.vscode/
*.log
.core/

View file

@ -11,7 +11,7 @@ import (
"sync"
"time"
coreerr "forge.lthn.ai/core/go-log"
coreerr "dappco.re/go/core/log"
)
// RetryConfig controls exponential backoff retry behaviour.

View file

@ -137,7 +137,8 @@ func TestAPIClient_Do_Bad_ClientError(t *testing.T) {
err = c.Do(req, nil)
assert.Error(t, err)
assert.Contains(t, err.Error(), "test-api 404")
assert.Contains(t, err.Error(), "test-api")
assert.Contains(t, err.Error(), "HTTP 404")
assert.Contains(t, err.Error(), "not found")
}
@ -226,7 +227,8 @@ func TestAPIClient_Do_Bad_ExhaustsRetries(t *testing.T) {
err = c.Do(req, nil)
assert.Error(t, err)
assert.Contains(t, err.Error(), "exhaust-test 500")
assert.Contains(t, err.Error(), "exhaust-test")
assert.Contains(t, err.Error(), "HTTP 500")
// 1 initial + 2 retries = 3 attempts
assert.Equal(t, int32(3), attempts.Load())
}
@ -476,7 +478,8 @@ func TestAPIClient_DoRaw_Bad_ClientError(t *testing.T) {
_, err = c.DoRaw(req)
assert.Error(t, err)
assert.Contains(t, err.Error(), "raw-test 403")
assert.Contains(t, err.Error(), "raw-test")
assert.Contains(t, err.Error(), "HTTP 403")
}
func TestAPIClient_DoRaw_Good_RetriesServerError(t *testing.T) {

View file

@ -7,7 +7,7 @@ import (
"net/url"
"strconv"
coreerr "forge.lthn.ai/core/go-log"
coreerr "dappco.re/go/core/log"
)
const cloudnsBaseURL = "https://api.cloudns.net"

View file

@ -78,7 +78,8 @@ func TestCloudNSClient_DoRaw_Bad_HTTPError(t *testing.T) {
_, err = client.doRaw(req)
assert.Error(t, err)
assert.Contains(t, err.Error(), "cloudns API 403")
assert.Contains(t, err.Error(), "cloudns API")
assert.Contains(t, err.Error(), "HTTP 403")
}
func TestCloudNSClient_DoRaw_Bad_ServerError(t *testing.T) {
@ -102,7 +103,8 @@ func TestCloudNSClient_DoRaw_Bad_ServerError(t *testing.T) {
_, err = client.doRaw(req)
assert.Error(t, err)
assert.Contains(t, err.Error(), "cloudns API 500")
assert.Contains(t, err.Error(), "cloudns API")
assert.Contains(t, err.Error(), "HTTP 500")
}
// --- Zone JSON parsing ---

View file

@ -18,10 +18,10 @@ import (
"slices"
"strings"
"dappco.re/go/core/io"
"dappco.re/go/core/log"
"forge.lthn.ai/core/cli/pkg/cli"
"forge.lthn.ai/core/go-i18n"
"forge.lthn.ai/core/go-io"
"forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-scm/repos"
)

View file

@ -5,9 +5,9 @@ import (
"os"
"time"
"dappco.re/go/core/infra"
coreerr "dappco.re/go/core/log"
"forge.lthn.ai/core/cli/pkg/cli"
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-infra"
)
var dnsCmd = &cli.Command{

View file

@ -6,9 +6,9 @@ import (
"os"
"time"
"dappco.re/go/core/infra"
coreerr "dappco.re/go/core/log"
"forge.lthn.ai/core/cli/pkg/cli"
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-infra"
)
var lbCmd = &cli.Command{

View file

@ -5,9 +5,9 @@ import (
"os"
"time"
"dappco.re/go/core/infra"
coreerr "dappco.re/go/core/log"
"forge.lthn.ai/core/cli/pkg/cli"
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-infra"
)
var setupCmd = &cli.Command{

View file

@ -6,8 +6,8 @@ import (
"os/exec"
"syscall"
coreerr "dappco.re/go/core/log"
"forge.lthn.ai/core/cli/pkg/cli"
coreerr "forge.lthn.ai/core/go-log"
)
var sshCmd = &cli.Command{

View file

@ -8,10 +8,10 @@ import (
"sync"
"time"
"forge.lthn.ai/core/go-ansible"
"dappco.re/go/core/infra"
coreerr "dappco.re/go/core/log"
"forge.lthn.ai/core/cli/pkg/cli"
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-infra"
"forge.lthn.ai/core/go-ansible"
)
var statusCmd = &cli.Command{

View file

@ -6,8 +6,8 @@ import (
"os"
"path/filepath"
coreerr "forge.lthn.ai/core/go-log"
coreio "forge.lthn.ai/core/go-io"
coreio "dappco.re/go/core/io"
coreerr "dappco.re/go/core/log"
"gopkg.in/yaml.v3"
)

View file

@ -5,14 +5,14 @@ description: Infrastructure provider API clients and YAML-based configuration fo
# go-infra
`forge.lthn.ai/core/go-infra` provides typed Go clients for infrastructure provider APIs (Hetzner Cloud, Hetzner Robot, CloudNS) and a declarative YAML configuration layer for describing production topology. It also ships CLI commands for production management (`core prod`) and security monitoring (`core monitor`).
`dappco.re/go/core/infra` provides typed Go clients for infrastructure provider APIs (Hetzner Cloud, Hetzner Robot, CloudNS) and a declarative YAML configuration layer for describing production topology. It also ships CLI commands for production management (`core prod`) and security monitoring (`core monitor`).
The library has no framework dependencies beyond the Go standard library, YAML parsing, and testify for tests. All HTTP communication goes through a shared `APIClient` that handles retries, exponential backoff, and rate-limit compliance automatically.
## Module Path
```
forge.lthn.ai/core/go-infra
dappco.re/go/core/infra
```
Requires **Go 1.26+**.
@ -22,7 +22,7 @@ Requires **Go 1.26+**.
### Using the API Clients Directly
```go
import "forge.lthn.ai/core/go-infra"
import "dappco.re/go/core/infra"
// Hetzner Cloud -- list all servers
hc := infra.NewHCloudClient(os.Getenv("HCLOUD_TOKEN"))
@ -40,7 +40,7 @@ changed, err := dns.EnsureRecord(ctx, "example.com", "www", "A", "1.2.3.4", 300)
### Loading Infrastructure Configuration
```go
import "forge.lthn.ai/core/go-infra"
import "dappco.re/go/core/infra"
// Auto-discover infra.yaml by walking up from the current directory
cfg, path, err := infra.Discover(".")

20
go.mod
View file

@ -1,21 +1,23 @@
module forge.lthn.ai/core/go-infra
module dappco.re/go/core/infra
go 1.26.0
require (
forge.lthn.ai/core/cli v0.3.5
forge.lthn.ai/core/go-ansible v0.1.4
forge.lthn.ai/core/go-i18n v0.1.5
forge.lthn.ai/core/go-io v0.1.5
forge.lthn.ai/core/go-log v0.0.4
forge.lthn.ai/core/go-scm v0.3.4
dappco.re/go/core/io v0.2.0
dappco.re/go/core/log v0.1.0
forge.lthn.ai/core/cli v0.3.7
forge.lthn.ai/core/go-ansible v0.1.6
forge.lthn.ai/core/go-i18n v0.1.7
forge.lthn.ai/core/go-scm v0.3.6
github.com/stretchr/testify v1.11.1
gopkg.in/yaml.v3 v3.0.1
)
require (
forge.lthn.ai/core/go v0.3.1 // indirect
forge.lthn.ai/core/go-inference v0.1.4 // indirect
forge.lthn.ai/core/go v0.3.3 // indirect
forge.lthn.ai/core/go-inference v0.1.6 // indirect
forge.lthn.ai/core/go-io v0.1.7 // indirect
forge.lthn.ai/core/go-log v0.0.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/bubbletea v1.3.10 // indirect
github.com/charmbracelet/colorprofile v0.4.3 // indirect

32
go.sum
View file

@ -1,19 +1,23 @@
forge.lthn.ai/core/cli v0.3.5 h1:P7yK0DmSA1QnUMFuCjJZf/fk/akKPIxopQ6OwD8Sar8=
forge.lthn.ai/core/cli v0.3.5/go.mod h1:SeArHx+hbpX5iZqgASCD7Q1EDoc6uaaGiGBotmNzIx4=
forge.lthn.ai/core/go v0.3.1 h1:5FMTsUhLcxSr07F9q3uG0Goy4zq4eLivoqi8shSY4UM=
forge.lthn.ai/core/go v0.3.1/go.mod h1:gE6c8h+PJ2287qNhVUJ5SOe1kopEwHEquvinstpuyJc=
forge.lthn.ai/core/go-ansible v0.1.4 h1:kja8xFADQ97c0X1RqCTHrYG8SZwfCtP33Ms48bJDmmI=
forge.lthn.ai/core/go-ansible v0.1.4/go.mod h1:TV7eGhm8z86iihVPpn5I21jzBZ3UoR/TRva6tNOCRoU=
forge.lthn.ai/core/go-i18n v0.1.5 h1:B4hV4eTl63akZiplM8lswuttctrcSOCWyFSGBZmu6Nc=
forge.lthn.ai/core/go-i18n v0.1.5/go.mod h1:hJsUxmqdPly73i3VkTDxvmbrpjxSd65hQVQqWA3+fnM=
forge.lthn.ai/core/go-inference v0.1.4 h1:fuAgWbqsEDajHniqAKyvHYbRcBrkGEiGSqR2pfTMRY0=
forge.lthn.ai/core/go-inference v0.1.4/go.mod h1:jfWz+IJX55wAH98+ic6FEqqGB6/P31CHlg7VY7pxREw=
forge.lthn.ai/core/go-io v0.1.5 h1:+XJ1YhaGGFLGtcNbPtVlndTjk+pO0Ydi2hRDj5/cHOM=
forge.lthn.ai/core/go-io v0.1.5/go.mod h1:FRtXSsi8W+U9vewCU+LBAqqbIj3wjXA4dBdSv3SAtWI=
dappco.re/go/core/io v0.2.0 h1:zuudgIiTsQQ5ipVt97saWdGLROovbEB/zdVyy9/l+I4=
dappco.re/go/core/io v0.2.0/go.mod h1:1QnQV6X9LNgFKfm8SkOtR9LLaj3bDcsOIeJOOyjbL5E=
dappco.re/go/core/log v0.1.0 h1:pa71Vq2TD2aoEUQWFKwNcaJ3GBY8HbaNGqtE688Unyc=
dappco.re/go/core/log v0.1.0/go.mod h1:Nkqb8gsXhZAO8VLpx7B8i1iAmohhzqA20b9Zr8VUcJs=
forge.lthn.ai/core/cli v0.3.7 h1:1GrbaGg0wDGHr6+klSbbGyN/9sSbHvFbdySJznymhwg=
forge.lthn.ai/core/cli v0.3.7/go.mod h1:DBUppJkA9P45ZFGgI2B8VXw1rAZxamHoI/KG7fRvTNs=
forge.lthn.ai/core/go v0.3.3 h1:kYYZ2nRYy0/Be3cyuLJspRjLqTMxpckVyhb/7Sw2gd0=
forge.lthn.ai/core/go v0.3.3/go.mod h1:Cp4ac25pghvO2iqOu59t1GyngTKVOzKB5/VPdhRi9CQ=
forge.lthn.ai/core/go-ansible v0.1.6 h1:jTeW26Gqa4mhv+4newyrfwfg6TY/17nG9yWymZdBF1Y=
forge.lthn.ai/core/go-ansible v0.1.6/go.mod h1:RcthHm/ouMCZLYW3R4LgVWASXvg2Ndc408GX5TxqDZE=
forge.lthn.ai/core/go-i18n v0.1.7 h1:aHkAoc3W8fw3RPNvw/UszQbjyFWXHszzbZgty3SwyAA=
forge.lthn.ai/core/go-i18n v0.1.7/go.mod h1:0VDjwtY99NSj2iqwrI09h5GUsJeM9s48MLkr+/Dn4G8=
forge.lthn.ai/core/go-inference v0.1.6 h1:ce42zC0zO8PuISUyAukAN1NACEdWp5wF1mRgnh5+58E=
forge.lthn.ai/core/go-inference v0.1.6/go.mod h1:jfWz+IJX55wAH98+ic6FEqqGB6/P31CHlg7VY7pxREw=
forge.lthn.ai/core/go-io v0.1.7 h1:Tdb6sqh+zz1lsGJaNX9RFWM6MJ/RhSAyxfulLXrJsbk=
forge.lthn.ai/core/go-io v0.1.7/go.mod h1:8lRLFk4Dnp5cR/Cyzh9WclD5566TbpdRgwcH7UZLWn4=
forge.lthn.ai/core/go-log v0.0.4 h1:KTuCEPgFmuM8KJfnyQ8vPOU1Jg654W74h8IJvfQMfv0=
forge.lthn.ai/core/go-log v0.0.4/go.mod h1:r14MXKOD3LF/sI8XUJQhRk/SZHBE7jAFVuCfgkXoZPw=
forge.lthn.ai/core/go-scm v0.3.4 h1:McZvp2gI3wEPCF/jim8O4F1+Vp477N81TUiiklTq5hw=
forge.lthn.ai/core/go-scm v0.3.4/go.mod h1:AOrx4CEmV8/Q73Cvd2LkbFniYGpk46mticpYmK5MnJA=
forge.lthn.ai/core/go-scm v0.3.6 h1:LFNx8Fs82mrpxro/MPUM6tMiD4DqPmdu83UknXztQjc=
forge.lthn.ai/core/go-scm v0.3.6/go.mod h1:IWFIYDfRH0mtRdqY5zV06l/RkmkPpBM6FcbKWhg1Qa8=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=

View file

@ -7,7 +7,7 @@ import (
"net/http"
"strings"
coreerr "forge.lthn.ai/core/go-log"
coreerr "dappco.re/go/core/log"
)
const (

View file

@ -116,7 +116,8 @@ func TestHCloudClient_Do_Bad_APIError(t *testing.T) {
var result struct{}
err := client.get(context.Background(), "/servers", &result)
assert.Error(t, err)
assert.Contains(t, err.Error(), "hcloud API 403")
assert.Contains(t, err.Error(), "hcloud API")
assert.Contains(t, err.Error(), "HTTP 403")
}
func TestHCloudClient_Do_Bad_APIErrorNoJSON(t *testing.T) {
@ -136,7 +137,8 @@ func TestHCloudClient_Do_Bad_APIErrorNoJSON(t *testing.T) {
err := client.get(context.Background(), "/servers", nil)
assert.Error(t, err)
assert.Contains(t, err.Error(), "hcloud API 500")
assert.Contains(t, err.Error(), "hcloud API")
assert.Contains(t, err.Error(), "HTTP 500")
}
func TestHCloudClient_Do_Good_NilResult(t *testing.T) {
@ -218,7 +220,8 @@ func TestHRobotClient_Get_Bad_HTTPError(t *testing.T) {
err := client.get(context.Background(), "/server", nil)
assert.Error(t, err)
assert.Contains(t, err.Error(), "hrobot API 401")
assert.Contains(t, err.Error(), "hrobot API")
assert.Contains(t, err.Error(), "HTTP 401")
}
// --- Type serialisation ---

45
kb/API-Clients.md Normal file
View file

@ -0,0 +1,45 @@
# API Clients
Module: `dappco.re/go/core/infra`
## CloudNS Client
`NewCloudNSClient(authID, password)` creates a client for the CloudNS DNS API (`api.cloudns.net`).
### DNS Record Operations
| Method | Description |
|--------|-------------|
| `ListZones(ctx)` | Returns all DNS zones |
| `ListRecords(ctx, domain)` | Returns all records for a zone |
| `CreateRecord(ctx, domain, host, type, value, ttl)` | Creates a record, returns ID |
| `UpdateRecord(ctx, domain, id, host, type, value, ttl)` | Updates an existing record |
| `DeleteRecord(ctx, domain, id)` | Deletes a record by ID |
| `EnsureRecord(ctx, domain, host, type, value, ttl)` | Idempotent create-or-update, returns `(changed bool, err)` |
### ACME Helpers
- `SetACMEChallenge(ctx, domain, value)` — Creates `_acme-challenge` TXT record (TTL 60s)
- `ClearACMEChallenge(ctx, domain)` — Removes all `_acme-challenge` TXT records
## Hetzner Cloud Client
`NewHCloudClient(token)` creates a client for the Hetzner Cloud API (`api.hetzner.cloud/v1`). Uses Bearer token auth.
### Types
- `HCloudServer` — ID, Name, Status, PublicNet (IPv4), PrivateNet, ServerType, Datacenter, Labels
- `HCloudLoadBalancer` — ID, Name, PublicNet, Algorithm, Services, Targets, Location, Labels
- `HCloudLBCreateRequest` — Creation parameters for load balancers
## Hetzner Robot Client
`NewHRobotClient(user, password)` creates a client for dedicated server management (`robot-ws.your-server.de`). Uses Basic auth.
- `HRobotServer` — ServerIP, ServerName, Product, Datacenter, Status, PaidUntil
## Infrastructure Config
`Load(path)` reads `infra.yaml`. `Discover(startDir)` walks up directories looking for `infra.yaml`.
Helper: `cfg.HostsByRole(role)` filters hosts, `cfg.AppServers()` returns hosts with role "app".

58
kb/Home.md Normal file
View file

@ -0,0 +1,58 @@
# go-infra
Module: `dappco.re/go/core/infra`
Infrastructure management for the Host UK production environment. Provides API clients for CloudNS DNS, Hetzner Cloud, and Hetzner Robot, plus a declarative infrastructure configuration model loaded from `infra.yaml`.
## Architecture
| File | Purpose |
|------|---------|
| `config.go` | `Config` struct and `Load()`/`Discover()` for `infra.yaml` |
| `client.go` | `APIClient` — shared HTTP client with auth, retries, JSON handling |
| `cloudns.go` | `CloudNSClient` — DNS record management via CloudNS API |
| `hetzner.go` | `HCloudClient` (Cloud API) + `HRobotClient` (Robot API) |
CLI commands in `cmd/prod/` (dns, lb, setup, ssh, status) and `cmd/monitor/`.
## Key Types
### Infrastructure Config
- **`Config`** — Top-level config: `Hosts`, `LoadBalancer`, `Network`, `DNS`, `SSL`, `Database`, `Cache`, `Containers`, `S3`, `CDN`, `CICD`, `Monitoring`, `Backups`
- **`Host`** — Server: `FQDN`, `IP`, `PrivateIP`, `Type` (hcloud/hrobot), `Role` (bastion/app/builder), `SSH`, `Services`
- **`LoadBalancer`** — LB config with backends, health checks, listeners, SSL
- **`DNS`** — Provider config with zones and records
- **`Database`** — Cluster config (engine, version, nodes, backup)
### API Clients
- **`CloudNSClient`** — DNS operations: `ListZones()`, `ListRecords()`, `CreateRecord()`, `UpdateRecord()`, `DeleteRecord()`, `EnsureRecord()`, `SetACMEChallenge()`, `ClearACMEChallenge()`
- **`HCloudClient`** — Hetzner Cloud: `ListServers()`, `ListLoadBalancers()`, `GetLoadBalancer()`, `CreateLoadBalancer()`, `DeleteLoadBalancer()`, `CreateSnapshot()`
- **`HRobotClient`** — Hetzner Robot (dedicated): `ListServers()`, `GetServer()`
- **`APIClient`** — Shared HTTP client with configurable auth and error handling
## Usage
```go
import "dappco.re/go/core/infra"
// Load infrastructure config
cfg, path, _ := infra.Discover(".")
// Get app servers
appServers := cfg.AppServers()
// DNS management
dns := infra.NewCloudNSClient(authID, password)
changed, _ := dns.EnsureRecord(ctx, "example.com", "www", "A", "1.2.3.4", 300)
// Hetzner Cloud
hcloud := infra.NewHCloudClient(token)
servers, _ := hcloud.ListServers(ctx)
```
## Dependencies
- `gopkg.in/yaml.v3` — YAML config parsing
- No core ecosystem dependencies (standalone)