carlos / platform Public

feat: DNS and cert health stamps for route aliases (#151)

Extends the reconcile pass's DNS-health (section 4) and TLS-health
(section 5) loops to stamp route_aliases rows the same way route rows
are stamped: reg.ListAliases() is read once, guarded by the same
cfg.Resolve != nil || cfg.AcmeDir != "" condition each section already
gates on, so a box with neither seam configured pays no extra query.

DNS stamping reuses the route loop's verdict logic (extracted into a
shared dnsStatus helper so the two can't drift apart): resolver
failure stamps "" (unknown, never a guess), an edge-IP match stamps
"pointing", anything else "elsewhere" — landed via
SetAliasDNSStatus(a.Alias, status, now). Cert stamping calls the
existing hostCertExpiry(cfg.AcmeDir, a.Alias, cfg.AppsDomain) and
lands it via SetAliasCertExpiresAt. Both loops log-and-continue per
item, matching the route stamping style.

Neither stamp touches Fingerprint — Task 2 already excludes these
columns from it, so a stamping pass alone still never forces an edge
reload.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell cd233518716d5cb77bde9a8fb09da1d146bf2e59 parent f3eb2ea
2 files changed, +120 −10
  • internal/domains/reconcile.go +43 −10
  • internal/domains/reconcile_test.go +77 −0
diff --git a/internal/domains/reconcile.go b/internal/domains/reconcile.go
index f879b461e..c5d4f72f5 100644
--- a/internal/domains/reconcile.go
+++ b/internal/domains/reconcile.go
@@ -406,6 +406,19 @@ func Reconcile(ctx context.Context, s release.Store, reg *registry.Registry, cfg
}
routes = kept
+ // aliases are the extra hostnames route_aliases holds (Task 2/#151),
+ // health-stamped by sections 4 and 5 below exactly like the routes they
+ // point at. Listed ONCE, above both sections, and guarded the same way
+ // each section already guards itself — a box with neither Resolve nor
+ // AcmeDir configured pays no aliases query at all.
+ var aliases []registry.RouteAlias
+ if cfg.Resolve != nil || cfg.AcmeDir != "" {
+ aliases, err = reg.ListAliases()
+ if err != nil {
+ return fmt.Errorf("domains: list aliases: %w", err)
+ }
+ }
+
// --- 4. DNS health --------------------------------------------------------
if cfg.Resolve != nil {
edge := map[string]bool{}
@@ -414,20 +427,17 @@ func Reconcile(ctx context.Context, s release.Store, reg *registry.Registry, cfg
}
now := cfg.Now().Unix()
for _, rt := range routes {
- status := "" // resolver failure is UNKNOWN, never a guess
- if ips, err := cfg.Resolve(rt.Host); err == nil {
- status = "elsewhere"
- for _, ip := range ips {
- if edge[ip] {
- status = "pointing"
- break
- }
- }
- }
+ status := dnsStatus(cfg.Resolve, rt.Host, edge)
if err := reg.SetDNSStatus(rt.Host, status, now); err != nil {
log.Printf("domains: stamp dns on %s: %v", rt.Host, err)
}
}
+ for _, a := range aliases {
+ status := dnsStatus(cfg.Resolve, a.Alias, edge)
+ if err := reg.SetAliasDNSStatus(a.Alias, status, now); err != nil {
+ log.Printf("domains: stamp dns on alias %s: %v", a.Alias, err)
+ }
+ }
}
// --- 5. TLS health --------------------------------------------------------
@@ -438,10 +448,33 @@ func Reconcile(ctx context.Context, s release.Store, reg *registry.Registry, cfg
log.Printf("domains: stamp cert expiry on %s: %v", rt.Host, err)
}
}
+ for _, a := range aliases {
+ at := hostCertExpiry(cfg.AcmeDir, a.Alias, cfg.AppsDomain)
+ if err := reg.SetAliasCertExpiresAt(a.Alias, at); err != nil {
+ log.Printf("domains: stamp cert expiry on alias %s: %v", a.Alias, err)
+ }
+ }
}
return nil
}
+// dnsStatus resolves host through resolve and classifies the answer against
+// edge: a resolver failure is UNKNOWN ("") — never a guess — an edge-IP match
+// is "pointing", anything else is "elsewhere". Shared by the route and alias
+// loops in section 4 so the two verdicts can never drift apart.
+func dnsStatus(resolve func(host string) ([]string, error), host string, edge map[string]bool) string {
+ ips, err := resolve(host)
+ if err != nil {
+ return ""
+ }
+ for _, ip := range ips {
+ if edge[ip] {
+ return "pointing"
+ }
+ }
+ return "elsewhere"
+}
+
// OpsSqid is the built-in ops account's frozen sqid (registry id 1). Every
// deployment has it, console or not.
var OpsSqid = Sqid(1)
diff --git a/internal/domains/reconcile_test.go b/internal/domains/reconcile_test.go
index 4c25b21b0..ab3b8b652 100644
--- a/internal/domains/reconcile_test.go
+++ b/internal/domains/reconcile_test.go
@@ -624,6 +624,83 @@ func TestReconcileStampsCertExpiry(t *testing.T) {
}
}
+// aliasByName finds an alias row by hostname among reg.ListAliases(), the
+// same read-back-from-the-registry shape mustGet uses for routes.
+func aliasByName(t *testing.T, reg *registry.Registry, alias string) registry.RouteAlias {
+ t.Helper()
+ aliases, err := reg.ListAliases()
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, a := range aliases {
+ if a.Alias == alias {
+ return a
+ }
+ }
+ t.Fatalf("alias %s missing", alias)
+ return registry.RouteAlias{}
+}
+
+func TestReconcileStampsAliasDNS(t *testing.T) {
+ s, reg := dirStore(t), regWith(t,
+ registry.Route{Host: "app.test", Kind: "static", Status: "ready"},
+ )
+ for _, a := range []string{"points-alias.test", "elsewhere-alias.test", "broken-alias.test"} {
+ if err := reg.UpsertAlias(registry.RouteAlias{Alias: a, Host: "app.test"}); err != nil {
+ t.Fatal(err)
+ }
+ }
+ c := cfg(t)
+ c.Resolve = func(host string) ([]string, error) {
+ switch host {
+ case "points-alias.test":
+ return []string{"198.51.100.9", "203.0.113.7"}, nil // edge IP among others
+ case "elsewhere-alias.test":
+ return []string{"198.51.100.9"}, nil
+ default:
+ return nil, errors.New("NXDOMAIN or worse")
+ }
+ }
+ if err := domains.Reconcile(ctx, s, reg, c); err != nil {
+ t.Fatal(err)
+ }
+ now := c.Now().Unix()
+ for alias, want := range map[string]string{
+ "points-alias.test": "pointing",
+ "elsewhere-alias.test": "elsewhere",
+ "broken-alias.test": "", // resolver failure is UNKNOWN, never a guess
+ } {
+ got := aliasByName(t, reg, alias)
+ if got.DNSStatus != want {
+ t.Errorf("%s dns_status = %q, want %q", alias, got.DNSStatus, want)
+ }
+ if got.DNSCheckedAt != now {
+ t.Errorf("%s dns_checked_at = %d, want %d", alias, got.DNSCheckedAt, now)
+ }
+ }
+}
+
+func TestReconcileStampsAliasCert(t *testing.T) {
+ s, reg := dirStore(t), regWith(t,
+ registry.Route{Host: "kass.training", Kind: "instance", App: "kass",
+ Status: "ready", Addr: "127.0.0.1:9000"},
+ )
+ if err := reg.UpsertAlias(registry.RouteAlias{Alias: "kass-alias.training", Host: "kass.training"}); err != nil {
+ t.Fatal(err)
+ }
+ acmeDir := t.TempDir()
+ notAfter := time.Date(2026, 10, 12, 0, 0, 0, 0, time.UTC)
+ writeCertBundle(t, acmeDir, "kass-alias.training", notAfter)
+ c := cfg(t)
+ c.AcmeDir = acmeDir
+ if err := domains.Reconcile(ctx, s, reg, c); err != nil {
+ t.Fatal(err)
+ }
+ if got := aliasByName(t, reg, "kass-alias.training"); got.CertExpiresAt != notAfter.Unix() {
+ t.Errorf("kass-alias.training cert_expires_at = %d, want %d", got.CertExpiresAt, notAfter.Unix())
+ }
+}
+
func TestReconcileInertWithoutAppsDomain(t *testing.T) {
s, reg := dirStore(t), regWith(t, registry.Route{Host: "kass.training", Kind: "instance",
App: "kass", Status: "ready", Addr: "127.0.0.1:9000"})