carlos / platform Public

feat: alias readings in box status snapshot (#151)

Probes gains an additive Aliases []registry.RouteAlias field and
BoxMetrics gains Aliases map[string]AliasReading (JSON aliases,omitempty),
carrying route_aliases' DNS/cert stamps (Task 2/5) up to the same private
home as RouteVersions: status/boxes/<label>.json only, never BuildFleet's
public fleet.json. CollectBox maps one to the other, keyed by alias
hostname. The agent's statusTick call site populates Probes.Aliases from
reg.ListAliases(), logging and continuing on error so a failed alias read
never sinks the whole snapshot.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 08b2b6611b0d9f7d0709690a06484dfe8f1b3632 parent ab0ae24
3 files changed, +109 −6
  • internal/host/agent.go +12 −0
  • internal/status/snapshot.go +59 −6
  • internal/status/status_test.go +38 −0
diff --git a/internal/host/agent.go b/internal/host/agent.go
index 23fb23f62..9f89b2555 100644
--- a/internal/host/agent.go
+++ b/internal/host/agent.go
@@ -1828,6 +1828,18 @@ func statusTick(ctx context.Context, s release.Store, reg *registry.Registry, p
}
now := time.Now()
+ // Aliases carries route_aliases' DNS/cert stamps (Task 2/5) up to the box
+ // snapshot (issue #151) — read fresh alongside routes above rather than
+ // folded into the caller's Probes in statusLoop, since reg is already in
+ // scope here and a failed read must not sink the whole snapshot: logged
+ // and left nil (CollectBox already treats nil the same as "no alias rows
+ // exist"), same best-effort shape as every other probe in this loop.
+ if aliases, err := reg.ListAliases(); err != nil {
+ log.Printf("agent: status tick: list aliases: %v", err)
+ } else {
+ p.Aliases = aliases
+ }
+
box := status.CollectBox(routes, p, now)
if labelOverride != "" {
box.Label = labelOverride
diff --git a/internal/status/snapshot.go b/internal/status/snapshot.go
index 39b18fc08..f5a4ad64b 100644
--- a/internal/status/snapshot.go
+++ b/internal/status/snapshot.go
@@ -93,6 +93,15 @@ type Probes struct {
// unknown/not-applicable for both, never a guess.
Release string
Tunnel string
+
+ // Aliases is this box's route_aliases rows (registry.RouteAlias), the
+ // agent's reg.ListAliases() read at statusTick — carried through raw so
+ // CollectBox can fold each row's DNS/cert stamp into Box.Metrics.Aliases
+ // alongside RouteVersions above. A failed read is the caller's problem
+ // (log-and-continue, see agent.go's statusTick), never CollectBox's: nil
+ // here means "no aliases known this tick," identical on the wire to "no
+ // alias rows exist."
+ Aliases []registry.RouteAlias
}
// BoxMetrics is the raw, PRIVATE block: real numbers, never banded. It lives
@@ -118,6 +127,33 @@ type BoxMetrics struct {
// no versioned route, same "absent, not empty" rule as the rest of this
// block.
RouteVersions map[string]string `json:"route_versions,omitempty"`
+ // Aliases is issue #151's per-hostname DNS/cert status, keyed by the
+ // alias hostname — the box's own route_aliases rows only (CollectBox is
+ // called per-box, same scope as RouteVersions above), never a fleet-wide
+ // join. It lives here for the identical reason RouteVersions does:
+ // status/boxes/<label>.json is bucket-private (this package's own doc
+ // comment, and dryrun.go's leak-scan self-check, which guards the PUBLIC
+ // feed only — fleet.json, index.html, status.css — and deliberately
+ // EXPECTS raw values inside status/boxes/*.json). BuildFleet copies Box's
+ // fields into FleetBox one at a time by name and never names Metrics, so
+ // these hostnames can never reach the public aggregation. Task 9's
+ // console list door is the intended reader. Omitted entirely (not a
+ // present-but-empty map) when this box has no alias rows, same
+ // "absent, not empty" rule as RouteVersions.
+ Aliases map[string]AliasReading `json:"aliases,omitempty"`
+}
+
+// AliasReading is one alias hostname's status as CollectBox saw it this
+// tick: Route is the route it points at (registry.RouteAlias.Host), DNSStatus
+// and CertExpiresAt are the box-stamped health columns (Task 2/5), carried
+// through unbanded like the rest of BoxMetrics. Route has no omitempty — an
+// alias with no route would be a data bug worth seeing, not a field worth
+// hiding; DNSStatus/CertExpiresAt omit when unstamped ("" / 0), same
+// unknown-stays-absent convention as every other Metrics field.
+type AliasReading struct {
+ Route string `json:"route"`
+ DNSStatus string `json:"dns_status,omitempty"`
+ CertExpiresAt int64 `json:"cert_expires_at,omitempty"`
}
// RouteCounts is aggregate-only: no host, no socket path, just how many.
@@ -340,6 +376,21 @@ func CollectBox(routes []registry.Route, p Probes, now time.Time) Box {
counts.Resident++
}
}
+ // aliasReadings is issue #151's Metrics.Aliases source, built once here
+ // (not a second pass) from p.Aliases — nil (not an empty map) until the
+ // first entry, same "nothing to report" convention as routeVersions
+ // above, and for the identical reason: an all-alias-less box's Metrics
+ // attachment check below must see exactly "nothing to report".
+ var aliasReadings map[string]AliasReading
+ for _, a := range p.Aliases {
+ if aliasReadings == nil {
+ aliasReadings = map[string]AliasReading{}
+ }
+ aliasReadings[a.Alias] = AliasReading{
+ Route: a.Host, DNSStatus: a.DNSStatus, CertExpiresAt: a.CertExpiresAt,
+ }
+ }
+
if awakeKnown && p.Listening != nil {
counts.Awake, counts.Asleep = &awake, &asleep
}
@@ -382,18 +433,20 @@ func CollectBox(routes []registry.Route, p Probes, now time.Time) Box {
}
// Metrics attaches only when something is known — an all-unknown Probes
- // AND no versioned route publishes no block at all, the same "unknown
- // stays absent, never a guess" rule every other field on Box follows,
- // and it keeps a box that can't answer any of the seven fields
- // indistinguishable on the wire from a box running code that predates
- // Metrics entirely.
+ // AND no versioned route AND no alias reading publishes no block at all,
+ // the same "unknown stays absent, never a guess" rule every other field
+ // on Box follows, and it keeps a box that can't answer any of the eight
+ // fields indistinguishable on the wire from a box running code that
+ // predates Metrics entirely.
if p.MemTotalBytes != nil || p.MemAvailBytes != nil || p.Load1 != nil ||
- p.DiskFreeBytes != nil || p.Release != "" || p.Tunnel != "" || len(routeVersions) > 0 {
+ p.DiskFreeBytes != nil || p.Release != "" || p.Tunnel != "" ||
+ len(routeVersions) > 0 || len(aliasReadings) > 0 {
box.Metrics = &BoxMetrics{
MemTotalBytes: p.MemTotalBytes, MemAvailBytes: p.MemAvailBytes,
Load1: p.Load1, DiskFreeBytes: p.DiskFreeBytes,
Release: p.Release, Tunnel: p.Tunnel,
RouteVersions: routeVersions,
+ Aliases: aliasReadings,
}
}
diff --git a/internal/status/status_test.go b/internal/status/status_test.go
index 3d0a93e94..1a2daa907 100644
--- a/internal/status/status_test.go
+++ b/internal/status/status_test.go
@@ -340,6 +340,44 @@ func TestCollectBoxRouteVersionsEmptyOmitsField(t *testing.T) {
}
}
+// ---- Task 6: alias readings (route_aliases DNS/cert stamps, private) -----
+
+// TestCollectBoxAliases proves CollectBox maps Probes.Aliases (registry rows
+// carried in by the agent's own reg.ListAliases() call, see agent.go's
+// statusTick) onto Box.Metrics.Aliases, keyed by the alias hostname — the
+// same private home as RouteVersions (status/boxes/<label>.json only, never
+// BuildFleet's public fleet.json) — and that an empty Probes.Aliases omits
+// the "aliases" key entirely rather than publishing a present-but-empty map.
+func TestCollectBoxAliases(t *testing.T) {
+ p := status.Probes{
+ MachineID: "m",
+ Aliases: []registry.RouteAlias{
+ {Alias: "extra.test", Host: "h.test", DNSStatus: "ok", CertExpiresAt: 1234567890},
+ },
+ }
+ b := status.CollectBox(nil, p, time.Now())
+ if b.Metrics == nil {
+ t.Fatal("metrics block missing — Aliases should have attached it")
+ }
+ got, ok := b.Metrics.Aliases["extra.test"]
+ if !ok || got.Route != "h.test" || got.DNSStatus != "ok" || got.CertExpiresAt != 1234567890 {
+ t.Fatalf("aliases[extra.test] = %+v (ok=%v), want {Route:h.test DNSStatus:ok CertExpiresAt:1234567890}", got, ok)
+ }
+
+ // Empty Probes.Aliases, but something else known so Metrics still
+ // attaches — the omission must be specific to the aliases key, not a
+ // side effect of the whole Metrics block being absent.
+ mt := int64(1)
+ b2 := status.CollectBox(nil, status.Probes{MachineID: "m", MemTotalBytes: &mt}, time.Now())
+ j, err := json.Marshal(b2)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if strings.Contains(string(j), "aliases") {
+ t.Fatalf("empty Probes.Aliases must omit the aliases key entirely, got: %s", j)
+ }
+}
+
// ---- Task 3: fleet merge + publisher --------------------------------------
// teststore is a fresh DirStore over a scratch directory — every fleet test