carlos / platform Public

steering: desired state — steered hosts × armed edges

Task 4 of the multi-region-steering plan: the pure desiredState fold
(internal/steering/steering.go). Consumes registry.Route rows and this
package's own edgesFileView reader (internal/host/edgesfile.go is the
writer contract), producing []desiredRecord — one latency A record per
steered host per armed edge, plus the always-present
edge.<apps-domain> steering hostname whenever >=1 edge is armed.

Armed gate validates the AWS-region shape (^[a-z]{2}(-[a-z]+)+-\d$),
never bare non-empty (Task 1's RULED block) — a CARLOS placement code
leaking through is refused and logged by value, not silently accepted.
Steering values outside ""/"latency", wildcard registry rows, and
pool-carrying steered routes are all excluded defensively with a loud
log naming the offending row. Names are normalized to a trailing-dot
FQDN exactly once here, so Task 5's converger never has to.

Pure functions only, no AWS/store I/O; single-box inert (zero callers
until Task 6 wires this into the housekeeper duty).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev 86236875333ca511bc447ebefd9747a6a1aa0505 parent 35ce275
2 files changed, +471 −0
  • internal/steering/steering.go +200 −0
  • internal/steering/steering_test.go +271 −0
diff --git a/internal/steering/steering.go b/internal/steering/steering.go
new file mode 100644
index 000000000..04dcffc39
--- /dev/null
+++ b/internal/steering/steering.go
@@ -0,0 +1,200 @@
+// Package steering builds the desired DNS-steering state for the
+// multi-region ingress design's Part 4 (docs/superpowers/specs/
+// 2026-08-20-multi-region-ingress-design.md): a fold of steered routes
+// (Route.Steering == "latency") across armed edges (boxes publishing a
+// usable control/edges/<box>.json — EIP present, Region a real AWS
+// region) into the Route53 latency A records the converger should want.
+//
+// Everything in this file is a PURE function: no AWS calls, no
+// release.Store I/O, no registry writes. desiredState is table-testable
+// with plain literals, by design — Task 5 (internal/steering/route53.go)
+// is the only place an AWS SDK call is allowed to appear, and Task 6 wires
+// this package into the host housekeeper. As of this task, nothing calls
+// desiredState: it is SINGLE-BOX INERT, exactly like every other stage of
+// this plan until its own wiring task lands.
+package steering
+
+import (
+ "encoding/json"
+ "fmt"
+ "log"
+ "regexp"
+ "sort"
+ "strings"
+
+ "github.com/carlosframework/platform/internal/registry"
+)
+
+// A desiredRecord is one latency A record: name -> eip, keyed by the
+// edge that serves it. SetIdentifier is the box label; Region is the
+// edge's AWS region; HealthCheckRef ties it to the edge's check.
+//
+// Name is ALWAYS a trailing-dot FQDN (Route53's own wire shape,
+// "app.example.com."). desiredState normalizes it exactly once, here —
+// Task 5's converger must never re-normalize a Name, and must never treat
+// a bare (dot-less) Name as anything but a bug upstream of this fold.
+type desiredRecord struct {
+ Name string // fqdn, trailing-dot normalized
+ EIP string
+ Box string // SetIdentifier
+ Region string // AWS region for latency routing
+}
+
+// edgesFileView is this package's own reader of internal/host's edgesFile
+// wire shape (internal/host/edgesfile.go is the writer contract — schema
+// v1, one flat object per box at control/edges/<box>.json). A separate
+// declaration from internal/edge/homemap.go's own edgesFileView is
+// deliberate, not an oversight: internal/steering depends on registry and
+// release only (never internal/edge — the import direction the plan
+// fixes for this task), and homemap.go's reader does not yet carry the
+// Region field this fold needs (it is a Task 1 addition homemap.go has no
+// consumer for). Hosts is deliberately absent: steering is any-edge-
+// any-app (the spec's home-boxed-kinds-steer-via-backhaul rule), so this
+// fold never needs to know which hosts an edge homes.
+type edgesFileView struct {
+ V int `json:"v"`
+ Box string `json:"box"`
+ EIP string `json:"eip"`
+ Region string `json:"region,omitempty"`
+}
+
+// parseEdgesFileView decodes one box's edges file body into the fields
+// this package needs. Pure — no store I/O — mirroring
+// internal/edge/homemap.go's parseEdgesFile contract: V < 1 (no version
+// field at all) is refused as not a file this reader understands; a
+// HIGHER version is accepted, because the schema grows additively and a
+// newer box's file still carries box/eip/region verbatim.
+func parseEdgesFileView(body []byte) (edgesFileView, error) {
+ var f edgesFileView
+ if err := json.Unmarshal(body, &f); err != nil {
+ return edgesFileView{}, fmt.Errorf("steering: parse edges file: %w", err)
+ }
+ if f.V < 1 {
+ return edgesFileView{}, fmt.Errorf("steering: unversioned edges file")
+ }
+ return f, nil
+}
+
+// awsRegionPattern matches an AWS region name shape (e.g. "eu-west-1",
+// "us-gov-west-1") — never a CARLOS placement code ("ie", "fleet/<name>").
+// Task 1's RULED block (plan review catch, 2026-08-23): a bare non-empty
+// check would let a placement code leak straight into a
+// ChangeResourceRecordSets call, which Route53 would reject batch-wide.
+// The gate matches SHAPE, never mere presence.
+var awsRegionPattern = regexp.MustCompile(`^[a-z]{2}(-[a-z]+)+-\d$`)
+
+// armedEdges filters edges to the set that can receive latency records:
+// EIP present (there is an address to answer with) and Region matching
+// the AWS-region shape. An edge failing either check is UNARMED for
+// steering — logged once per edge, naming the bad value, so a leaking
+// placement code (or a probe that never resolved) is loud rather than
+// silently folded into a record nobody can trust. The result is sorted by
+// Box so desiredState's output is deterministic regardless of input
+// order (harmless for Route53, load-bearing for tests).
+func armedEdges(edges []edgesFileView) []edgesFileView {
+ out := make([]edgesFileView, 0, len(edges))
+ for _, e := range edges {
+ if e.EIP == "" {
+ log.Printf("steering: edge %s unarmed: no EIP", e.Box)
+ continue
+ }
+ if !awsRegionPattern.MatchString(e.Region) {
+ log.Printf("steering: edge %s unarmed: region %q is not an AWS region (a CARLOS placement code cannot receive latency records)", e.Box, e.Region)
+ continue
+ }
+ out = append(out, e)
+ }
+ sort.Slice(out, func(i, j int) bool { return out[i].Box < out[j].Box })
+ return out
+}
+
+// steeredHosts filters routes to the concrete hosts eligible for a
+// latency record:
+//
+// - Steering must be exactly "latency" (provision.ValidSteering's own
+// vocabulary, not re-imported here — see the package doc for the
+// import-direction rule). "" is the common, unopted-in case and is
+// skipped silently; anything else is hand-written bucket state (a
+// value no door would ever write) and is skipped LOUDLY.
+// - The host must not be a wildcard row ("*.parent") — steering is
+// per-concrete-host (the spec's per-host DNS rule: a wildcard cannot
+// resolve to more than one A record set without per-request
+// knowledge Route53 does not have). Registry wildcard entries live in
+// route_aliases today, never in Route.Host, so this is a defensive
+// check, not a reachable one — kept anyway, loudly, per the plan.
+// - Pool must be "" (the shared pool). The console door and CLI already
+// refuse steering:"latency" on a pool-scoped route (Task 3); this
+// fold refuses it AGAIN, because a hand-edited bucket record can
+// still carry both — the door guards the write path, not the bucket.
+//
+// The result is sorted by Host for the same determinism armedEdges keeps.
+func steeredHosts(routes []registry.Route) []registry.Route {
+ out := make([]registry.Route, 0, len(routes))
+ for _, r := range routes {
+ switch r.Steering {
+ case "":
+ continue
+ case "latency":
+ // candidate — remaining checks below
+ default:
+ log.Printf("steering: route %s excluded: invalid steering value %q", r.Host, r.Steering)
+ continue
+ }
+ if strings.HasPrefix(r.Host, "*.") {
+ log.Printf("steering: route %s excluded: wildcard host cannot receive a per-host latency record", r.Host)
+ continue
+ }
+ if r.Pool != "" {
+ log.Printf("steering: route %s excluded: pool %q set (steering requires the shared pool)", r.Host, r.Pool)
+ continue
+ }
+ out = append(out, r)
+ }
+ sort.Slice(out, func(i, j int) bool { return out[i].Host < out[j].Host })
+ return out
+}
+
+// fqdn normalizes host to a trailing-dot FQDN, Route53's own wire shape —
+// exactly once, here, so every desiredRecord.Name downstream consumer
+// (Task 5) never has to reason about whether a Name is already
+// normalized.
+func fqdn(host string) string {
+ return strings.TrimSuffix(host, ".") + "."
+}
+
+// desiredState folds steered routes × armed edges. The steering
+// hostname edge.<apps-domain> is ALWAYS present when any edge is
+// armed (custom-domain CNAME target). Non-shared-pool routes are
+// excluded defensively (the door refuses them; the bucket may not
+// have) with a loud log.
+//
+// appsDomain empty with at least one armed edge is treated the same as
+// any other missing input: no synthetic edge.<apps-domain> records are
+// minted (there is no domain to build the hostname from), logged once —
+// steered per-host records are unaffected either way.
+func desiredState(routes []registry.Route, edges []edgesFileView, appsDomain string) []desiredRecord {
+ armed := armedEdges(edges)
+ if len(armed) == 0 {
+ return nil
+ }
+
+ var out []desiredRecord
+
+ if domain := strings.TrimSpace(strings.TrimSuffix(appsDomain, ".")); domain == "" {
+ log.Printf("steering: no apps domain configured — the edge.<apps-domain> steering hostname is not minted this pass")
+ } else {
+ edgeHost := fqdn("edge." + domain)
+ for _, e := range armed {
+ out = append(out, desiredRecord{Name: edgeHost, EIP: e.EIP, Box: e.Box, Region: e.Region})
+ }
+ }
+
+ for _, r := range steeredHosts(routes) {
+ name := fqdn(r.Host)
+ for _, e := range armed {
+ out = append(out, desiredRecord{Name: name, EIP: e.EIP, Box: e.Box, Region: e.Region})
+ }
+ }
+
+ return out
+}
diff --git a/internal/steering/steering_test.go b/internal/steering/steering_test.go
new file mode 100644
index 000000000..0ad59e7f4
--- /dev/null
+++ b/internal/steering/steering_test.go
@@ -0,0 +1,271 @@
+package steering
+
+import (
+ "bytes"
+ "log"
+ "reflect"
+ "strings"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/registry"
+)
+
+func captureLog(t *testing.T) *bytes.Buffer {
+ t.Helper()
+ var buf bytes.Buffer
+ prev := log.Writer()
+ flags := log.Flags()
+ log.SetOutput(&buf)
+ log.SetFlags(0)
+ t.Cleanup(func() { log.SetOutput(prev); log.SetFlags(flags) })
+ return &buf
+}
+
+func steeredRoute(host string) registry.Route {
+ return registry.Route{Host: host, Steering: "latency"}
+}
+
+func edge(box, eip, region string) edgesFileView {
+ return edgesFileView{V: 1, Box: box, EIP: eip, Region: region}
+}
+
+// TestDesiredState is the brief's table: one steered host x two armed
+// edges = two records; unsteered host absent; edge.<apps-domain> present
+// whenever >=1 edge is armed, absent at zero.
+func TestDesiredState(t *testing.T) {
+ dublin := edge("dublin", "1.2.3.4", "eu-west-1")
+ oregon := edge("oregon", "5.6.7.8", "us-west-2")
+
+ tests := []struct {
+ name string
+ routes []registry.Route
+ edges []edgesFileView
+ appsDomain string
+ want []desiredRecord
+ }{
+ {
+ name: "one steered host x two armed edges",
+ routes: []registry.Route{steeredRoute("app.example.com")},
+ edges: []edgesFileView{dublin, oregon},
+ appsDomain: "example.com",
+ want: []desiredRecord{
+ {Name: "edge.example.com.", EIP: "1.2.3.4", Box: "dublin", Region: "eu-west-1"},
+ {Name: "edge.example.com.", EIP: "5.6.7.8", Box: "oregon", Region: "us-west-2"},
+ {Name: "app.example.com.", EIP: "1.2.3.4", Box: "dublin", Region: "eu-west-1"},
+ {Name: "app.example.com.", EIP: "5.6.7.8", Box: "oregon", Region: "us-west-2"},
+ },
+ },
+ {
+ name: "unsteered host absent",
+ routes: []registry.Route{
+ {Host: "app.example.com", Steering: ""},
+ steeredRoute("steered.example.com"),
+ },
+ edges: []edgesFileView{dublin},
+ appsDomain: "example.com",
+ want: []desiredRecord{
+ {Name: "edge.example.com.", EIP: "1.2.3.4", Box: "dublin", Region: "eu-west-1"},
+ {Name: "steered.example.com.", EIP: "1.2.3.4", Box: "dublin", Region: "eu-west-1"},
+ },
+ },
+ {
+ name: "zero armed edges yields nothing, no edge.<apps-domain>",
+ routes: []registry.Route{steeredRoute("app.example.com")},
+ edges: nil,
+ appsDomain: "example.com",
+ want: nil,
+ },
+ {
+ name: "edge with empty EIP skipped",
+ routes: []registry.Route{steeredRoute("app.example.com")},
+ edges: []edgesFileView{dublin, edge("noeip", "", "us-west-2")},
+ appsDomain: "example.com",
+ want: []desiredRecord{
+ {Name: "edge.example.com.", EIP: "1.2.3.4", Box: "dublin", Region: "eu-west-1"},
+ {Name: "app.example.com.", EIP: "1.2.3.4", Box: "dublin", Region: "eu-west-1"},
+ },
+ },
+ {
+ name: "edge with empty region skipped",
+ routes: []registry.Route{steeredRoute("app.example.com")},
+ edges: []edgesFileView{dublin, edge("noregion", "9.9.9.9", "")},
+ appsDomain: "example.com",
+ want: []desiredRecord{
+ {Name: "edge.example.com.", EIP: "1.2.3.4", Box: "dublin", Region: "eu-west-1"},
+ {Name: "app.example.com.", EIP: "1.2.3.4", Box: "dublin", Region: "eu-west-1"},
+ },
+ },
+ {
+ name: "edge region carrying a placement code (not an AWS region) skipped",
+ routes: []registry.Route{steeredRoute("app.example.com")},
+ edges: []edgesFileView{dublin, edge("tito", "9.9.9.9", "ie")},
+ appsDomain: "example.com",
+ want: []desiredRecord{
+ {Name: "edge.example.com.", EIP: "1.2.3.4", Box: "dublin", Region: "eu-west-1"},
+ {Name: "app.example.com.", EIP: "1.2.3.4", Box: "dublin", Region: "eu-west-1"},
+ },
+ },
+ {
+ name: "pool-carrying steered route excluded",
+ routes: []registry.Route{
+ {Host: "pooled.example.com", Steering: "latency", Pool: "reserved"},
+ },
+ edges: []edgesFileView{dublin},
+ appsDomain: "example.com",
+ want: []desiredRecord{
+ {Name: "edge.example.com.", EIP: "1.2.3.4", Box: "dublin", Region: "eu-west-1"},
+ },
+ },
+ {
+ name: "wildcard registry row with steering set never produces a record",
+ routes: []registry.Route{
+ {Host: "*.example.com", Steering: "latency"},
+ },
+ edges: []edgesFileView{dublin},
+ appsDomain: "example.com",
+ want: []desiredRecord{
+ {Name: "edge.example.com.", EIP: "1.2.3.4", Box: "dublin", Region: "eu-west-1"},
+ },
+ },
+ {
+ name: "invalid steering value skipped, never guessed",
+ routes: []registry.Route{
+ {Host: "hand-written.example.com", Steering: "geo"},
+ },
+ edges: []edgesFileView{dublin},
+ appsDomain: "example.com",
+ want: []desiredRecord{
+ {Name: "edge.example.com.", EIP: "1.2.3.4", Box: "dublin", Region: "eu-west-1"},
+ },
+ },
+ {
+ name: "trailing-dot input normalizes to a single trailing dot",
+ routes: []registry.Route{steeredRoute("app.example.com.")},
+ edges: []edgesFileView{dublin},
+ appsDomain: "example.com.",
+ want: []desiredRecord{
+ {Name: "edge.example.com.", EIP: "1.2.3.4", Box: "dublin", Region: "eu-west-1"},
+ {Name: "app.example.com.", EIP: "1.2.3.4", Box: "dublin", Region: "eu-west-1"},
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ captureLog(t) // silence + isolate log output from test noise
+ got := desiredState(tt.routes, tt.edges, tt.appsDomain)
+ if !reflect.DeepEqual(got, tt.want) {
+ t.Fatalf("desiredState() =\n%#v\nwant\n%#v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestDesiredStateLogsEachSkipReason(t *testing.T) {
+ dublin := edge("dublin", "1.2.3.4", "eu-west-1")
+
+ tests := []struct {
+ name string
+ routes []registry.Route
+ edges []edgesFileView
+ wantLog string
+ }{
+ {
+ name: "edge with no EIP is named in the log",
+ edges: []edgesFileView{edge("noeip", "", "eu-west-1")},
+ wantLog: "steering: edge noeip unarmed: no EIP",
+ },
+ {
+ name: "edge with a placement code names the bad value",
+ edges: []edgesFileView{edge("tito", "9.9.9.9", "ie")},
+ wantLog: `steering: edge tito unarmed: region "ie" is not an AWS region`,
+ },
+ {
+ name: "pool-carrying route is named",
+ edges: []edgesFileView{dublin},
+ routes: []registry.Route{{Host: "pooled.example.com", Steering: "latency", Pool: "reserved"}},
+ wantLog: `steering: route pooled.example.com excluded: pool "reserved" set`,
+ },
+ {
+ name: "wildcard route is named",
+ edges: []edgesFileView{dublin},
+ routes: []registry.Route{{Host: "*.example.com", Steering: "latency"}},
+ wantLog: "steering: route *.example.com excluded: wildcard host",
+ },
+ {
+ name: "invalid steering value is named verbatim",
+ edges: []edgesFileView{dublin},
+ routes: []registry.Route{{Host: "hand.example.com", Steering: "geo"}},
+ wantLog: `steering: route hand.example.com excluded: invalid steering value "geo"`,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ buf := captureLog(t)
+ desiredState(tt.routes, tt.edges, "example.com")
+ if !strings.Contains(buf.String(), tt.wantLog) {
+ t.Fatalf("log output =\n%s\nwant substring %q", buf.String(), tt.wantLog)
+ }
+ })
+ }
+}
+
+func TestParseEdgesFileView(t *testing.T) {
+ tests := []struct {
+ name string
+ body string
+ want edgesFileView
+ wantErr bool
+ }{
+ {
+ name: "v1 body with region",
+ body: `{"v":1,"box":"dublin","eip":"1.2.3.4","region":"eu-west-1","generation":"2026-08-22T09:00:00Z","hosts":{"app.example.com":{"role":"home"}}}`,
+ want: edgesFileView{V: 1, Box: "dublin", EIP: "1.2.3.4", Region: "eu-west-1"},
+ },
+ {
+ name: "v1 body without region (pre-region writer)",
+ body: `{"v":1,"box":"dublin","eip":"1.2.3.4","generation":"2026-08-22T09:00:00Z","hosts":{}}`,
+ want: edgesFileView{V: 1, Box: "dublin", EIP: "1.2.3.4", Region: ""},
+ },
+ {
+ name: "unversioned body refused",
+ body: `{"box":"dublin","eip":"1.2.3.4"}`,
+ wantErr: true,
+ },
+ {
+ name: "garbage body refused",
+ body: `not json`,
+ wantErr: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := parseEdgesFileView([]byte(tt.body))
+ if tt.wantErr {
+ if err == nil {
+ t.Fatalf("parseEdgesFileView(%q) = %+v, nil; want error", tt.body, got)
+ }
+ return
+ }
+ if err != nil {
+ t.Fatalf("parseEdgesFileView(%q) unexpected error: %v", tt.body, err)
+ }
+ if got != tt.want {
+ t.Fatalf("parseEdgesFileView(%q) = %+v, want %+v", tt.body, got, tt.want)
+ }
+ })
+ }
+}
+
+func TestArmedEdgesSortedByBox(t *testing.T) {
+ edges := []edgesFileView{
+ edge("oregon", "5.6.7.8", "us-west-2"),
+ edge("dublin", "1.2.3.4", "eu-west-1"),
+ }
+ got := armedEdges(edges)
+ if len(got) != 2 || got[0].Box != "dublin" || got[1].Box != "oregon" {
+ t.Fatalf("armedEdges() = %+v, want dublin then oregon", got)
+ }
+}