carlos / platform Public

domains: reconcile — the bucket decides, the agent materializes

One pass, five idempotent steps in spec order: account manifests →
registry accounts + route linkage, platform-domain minting (first ready
upstream, host order, deterministic), canary-host minting for hostless
pointers, removal ONLY of platform-flagged followers of retired canary
channels, then DNS/TLS health stamps. Unknowns stay unknown: resolver
failure stamps "", a missing or unparseable cert stamps 0. AppsDomain
unset = the whole pass is inert.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 5475b5226c056c40e2e0977f4cf46b09a984a0ca parent 090cee4
2 files changed, +713 −0
  • internal/domains/reconcile.go +384 −0
  • internal/domains/reconcile_test.go +329 −0
diff --git a/internal/domains/reconcile.go b/internal/domains/reconcile.go
new file mode 100644
index 000000000..80c846bfa
--- /dev/null
+++ b/internal/domains/reconcile.go
@@ -0,0 +1,384 @@
+package domains
+
+import (
+ "context"
+ "crypto/x509"
+ "encoding/json"
+ "encoding/pem"
+ "fmt"
+ "log"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "github.com/carlosframework/platform/internal/registry"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+// Config is everything one reconcile pass reads its world through. Every
+// field is a seam: the tests inject fixtures, the agent injects production
+// values (internal/host wires it).
+type Config struct {
+ // AppsDomain is the deployment's platform apps domain (flagship:
+ // "oncarlos.com"). "" makes the WHOLE pass inert — no minting, no
+ // manifests read, zero registry writes — the litestream convention:
+ // unconfigured means the feature does not exist.
+ AppsDomain string
+ // EdgeIPs are this deployment's edge addresses; a host resolving to any
+ // of them is "pointing".
+ EdgeIPs []string
+ // Resolve is the DNS seam (net resolver in production, a fake in
+ // tests); nil skips DNS stamping entirely.
+ Resolve func(host string) ([]string, error)
+ // AcmeDir is the edge's autocert DirCache directory; "" skips TLS
+ // stamping entirely.
+ AcmeDir string
+ // Now is the clock seam; nil = time.Now.
+ Now func() time.Time
+}
+
+// Reconcile runs the spec §3 steps in order, idempotently: bucket account
+// manifests → registry accounts + route linkage, platform-domain minting,
+// canary-host minting/removal, then DNS and TLS health stamps. Per-item
+// failures are logged and skipped — one bad manifest or one unresolvable
+// host must never take the pass down; only setup-level failures (listing the
+// bucket or the registry) return an error.
+//
+// The one destructive move — removing a route — happens ONLY for routes with
+// Platform set whose canary channel pointer is gone from the bucket. Custom
+// domains are owner property; reconciliation never touches them.
+func Reconcile(ctx context.Context, s release.Store, reg *registry.Registry, cfg Config) error {
+ if cfg.AppsDomain == "" {
+ return nil // inert until configured
+ }
+ if cfg.Now == nil {
+ cfg.Now = time.Now
+ }
+
+ // One bucket listing feeds both the manifest scan and the canary-pointer
+ // scan below.
+ keys, err := s.List(ctx, "apps/")
+ if err != nil {
+ return fmt.Errorf("domains: list bucket: %w", err)
+ }
+ routes, err := reg.List()
+ if err != nil {
+ return fmt.Errorf("domains: list routes: %w", err)
+ }
+
+ // --- 1. account materialization ------------------------------------------
+ // The console originates account truth as apps/<app>/account.json in the
+ // bucket; this is the only place it becomes registry state. A deployment
+ // with no console has no manifests: everything stays under ops.
+ accountOf := map[string]string{} // app -> manifest account name
+ for _, key := range keys {
+ app, ok := manifestApp(key)
+ if !ok {
+ continue
+ }
+ body, _, err := s.Get(ctx, key)
+ if err != nil {
+ log.Printf("domains: skipping %s: read failed: %v", key, err)
+ continue
+ }
+ var m struct{ Account, Owner string }
+ if err := json.Unmarshal(body, &m); err != nil || m.Account == "" {
+ log.Printf("domains: skipping %s: malformed manifest: %v", key, err)
+ continue
+ }
+ if _, err := reg.EnsureAccount(m.Account); err != nil {
+ log.Printf("domains: ensure account %q: %v", m.Account, err)
+ continue
+ }
+ accountOf[app] = m.Account
+ for i := range routes {
+ if routes[i].App != app || routes[i].Account == m.Account {
+ continue
+ }
+ if err := reg.SetAccount(routes[i].Host, m.Account); err != nil {
+ log.Printf("domains: stamp account on %s: %v", routes[i].Host, err)
+ continue
+ }
+ routes[i].Account = m.Account
+ }
+ }
+
+ // --- 2. platform-domain minting ------------------------------------------
+ byHost := map[string]bool{}
+ for _, rt := range routes {
+ byHost[rt.Host] = true
+ }
+ for _, app := range candidateApps(routes) {
+ acct := appAccount(routes, accountOf, app)
+ sq, err := accountSqid(reg, acct)
+ if err != nil {
+ log.Printf("domains: account id for %q: %v", acct, err)
+ continue
+ }
+ host := fmt.Sprintf("%s.%s.%s", app, sq, cfg.AppsDomain)
+ if byHost[host] {
+ continue // already minted — idempotent
+ }
+ src := firstReadyUpstream(routes, app)
+ if src == nil {
+ continue // nothing serving to copy an upstream from; next pass
+ }
+ nr := mintedRoute(host, app, acct, *src)
+ if err := reg.Upsert(nr); err != nil {
+ log.Printf("domains: mint %s: %v", host, err)
+ continue
+ }
+ log.Printf("domains: minted platform domain %s (upstream %s)", host, src.Host)
+ routes = append(routes, nr)
+ byHost[host] = true
+ }
+
+ // --- 3. canary hosts: mint for hostless pointers, remove for retired ----
+ canaryChannels := map[string]map[string]bool{} // app -> set of live canary/<name> channels
+ for _, key := range keys {
+ app, name, ok := canaryPointer(key)
+ if !ok {
+ continue
+ }
+ if canaryChannels[app] == nil {
+ canaryChannels[app] = map[string]bool{}
+ }
+ channel := "canary/" + name
+ canaryChannels[app][channel] = true
+ followed := false
+ for _, rt := range routes {
+ if rt.App == app && rt.Channel == channel {
+ followed = true
+ break
+ }
+ }
+ if followed {
+ continue
+ }
+ acct := appAccount(routes, accountOf, app)
+ sq, err := accountSqid(reg, acct)
+ if err != nil {
+ log.Printf("domains: account id for %q: %v", acct, err)
+ continue
+ }
+ src := firstReadyUpstream(routes, app)
+ if src == nil {
+ continue // app has nothing serving yet; the pointer waits
+ }
+ host := fmt.Sprintf("%s.%s.%s.%s", name, app, sq, cfg.AppsDomain)
+ nr := mintedRoute(host, app, acct, *src)
+ nr.Channel = channel
+ nr.Canary = true
+ if err := reg.Upsert(nr); err != nil {
+ log.Printf("domains: mint canary %s: %v", host, err)
+ continue
+ }
+ log.Printf("domains: minted canary domain %s (channel %s)", host, channel)
+ routes = append(routes, nr)
+ byHost[host] = true
+ }
+ // Removal: ONLY platform-minted followers of a canary channel whose
+ // pointer is gone. A custom route on the same channel is untouched — the
+ // platform flag, not the channel, is what grants removal authority.
+ kept := routes[:0]
+ for _, rt := range routes {
+ if rt.Platform && strings.HasPrefix(rt.Channel, "canary/") && !canaryChannels[rt.App][rt.Channel] {
+ if err := reg.Delete(rt.Host); err != nil {
+ log.Printf("domains: remove retired canary route %s: %v", rt.Host, err)
+ kept = append(kept, rt) // still there; stamp it honestly below
+ continue
+ }
+ log.Printf("domains: removed retired canary route %s (channel %s)", rt.Host, rt.Channel)
+ continue
+ }
+ kept = append(kept, rt)
+ }
+ routes = kept
+
+ // --- 4. DNS health --------------------------------------------------------
+ if cfg.Resolve != nil {
+ edge := map[string]bool{}
+ for _, ip := range cfg.EdgeIPs {
+ edge[ip] = true
+ }
+ 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
+ }
+ }
+ }
+ if err := reg.SetDNSStatus(rt.Host, status, now); err != nil {
+ log.Printf("domains: stamp dns on %s: %v", rt.Host, err)
+ }
+ }
+ }
+
+ // --- 5. TLS health --------------------------------------------------------
+ if cfg.AcmeDir != "" {
+ for _, rt := range routes {
+ at := certExpiry(filepath.Join(cfg.AcmeDir, rt.Host))
+ if err := reg.SetCertExpiresAt(rt.Host, at); err != nil {
+ log.Printf("domains: stamp cert expiry on %s: %v", rt.Host, err)
+ }
+ }
+ }
+ return nil
+}
+
+// manifestApp reports whether key is an account manifest (apps/<app>/
+// account.json, exactly one path level deep) and which app it names.
+func manifestApp(key string) (string, bool) {
+ rest, ok := strings.CutPrefix(key, "apps/")
+ if !ok {
+ return "", false
+ }
+ app, ok := strings.CutSuffix(rest, "/account.json")
+ if !ok || app == "" || strings.Contains(app, "/") {
+ return "", false
+ }
+ return app, true
+}
+
+// canaryPointer reports whether key is a canary channel pointer
+// (apps/<app>/channels/canary/<name>.json) and its app + canary name.
+func canaryPointer(key string) (app, name string, ok bool) {
+ rest, ok := strings.CutPrefix(key, "apps/")
+ if !ok {
+ return "", "", false
+ }
+ app, rest, ok = strings.Cut(rest, "/channels/canary/")
+ if !ok || app == "" || strings.Contains(app, "/") {
+ return "", "", false
+ }
+ name, ok = strings.CutSuffix(rest, ".json")
+ if !ok || name == "" || strings.Contains(name, "/") {
+ return "", "", false
+ }
+ return app, name, true
+}
+
+// candidateApps returns the distinct app names eligible for a platform
+// domain, in first-seen (host) order: non-static, non-service-namespace
+// (svc/*) apps with at least one route that is NOT itself platform-minted —
+// an app whose only routes are minted ones has been removed by its operator,
+// and re-minting from a mint would keep a ghost alive forever.
+func candidateApps(routes []registry.Route) []string {
+ seen := map[string]bool{}
+ real := map[string]bool{} // has a non-platform route
+ var order []string
+ for _, rt := range routes {
+ if rt.App == "" || rt.Kind == "static" || strings.HasPrefix(rt.App, "svc/") {
+ continue
+ }
+ if !seen[rt.App] {
+ seen[rt.App] = true
+ order = append(order, rt.App)
+ }
+ if !rt.Platform {
+ real[rt.App] = true
+ }
+ }
+ var out []string
+ for _, app := range order {
+ if real[app] {
+ out = append(out, app)
+ }
+ }
+ return out
+}
+
+// appAccount is the account an app's domains mint under: the bucket
+// manifest's word first, else whatever the app's routes already carry, else
+// the built-in ops — a deployment with no console lives entirely there.
+func appAccount(routes []registry.Route, accountOf map[string]string, app string) string {
+ if a := accountOf[app]; a != "" {
+ return a
+ }
+ for _, rt := range routes {
+ if rt.App == app && rt.Account != "" {
+ return rt.Account
+ }
+ }
+ return "ops"
+}
+
+// accountSqid resolves an account name to its frozen sqid, creating the
+// account row if needed (idempotent — the row is the sqid's anchor).
+func accountSqid(reg *registry.Registry, name string) (string, error) {
+ id, err := reg.EnsureAccount(name)
+ if err != nil {
+ return "", err
+ }
+ return Sqid(id), nil
+}
+
+// firstReadyUpstream picks the deterministic upstream a minted domain
+// proxies to: the app's first ready route in host order (reg.List's order)
+// that actually carries an upstream to copy (Socket or Addr) and is not
+// itself a minted route — minting from a mint would be self-reference.
+func firstReadyUpstream(routes []registry.Route, app string) *registry.Route {
+ for i := range routes {
+ rt := &routes[i]
+ if rt.App == app && !rt.Platform && rt.Status == "ready" &&
+ (rt.Socket != "" || rt.Addr != "") {
+ return rt
+ }
+ }
+ return nil
+}
+
+// mintedRoute is the platform route shape: same upstream trio
+// (Socket/Addr/Backing) and Kind as the source, ready (the upstream is —
+// that's how it was chosen), platform-flagged, account-stamped. Channel is
+// deliberately NOT copied for the apex domain: the platform domain is an
+// address for the app's primary backend, not a release-channel follower —
+// and a copied canary channel would make the apex route removable when that
+// canary retires.
+func mintedRoute(host, app, account string, src registry.Route) registry.Route {
+ return registry.Route{
+ Host: host,
+ Kind: src.Kind,
+ Socket: src.Socket,
+ Addr: src.Addr,
+ Backing: src.Backing,
+ App: app,
+ Status: "ready",
+ Platform: true,
+ Account: account,
+ }
+}
+
+// certExpiry reads the autocert DirCache entry at path (the PEM bundle
+// cachePut writes: private key block first, then the cert chain) and returns
+// the leaf's NotAfter as unix seconds. Anything short of a parseable
+// certificate — no file, no CERTIFICATE block, a corrupt one — is 0:
+// unknown, never a guess.
+func certExpiry(path string) int64 {
+ body, err := os.ReadFile(path)
+ if err != nil {
+ return 0
+ }
+ for len(body) > 0 {
+ var block *pem.Block
+ block, body = pem.Decode(body)
+ if block == nil {
+ return 0
+ }
+ if block.Type != "CERTIFICATE" {
+ continue // the private-key block(s) come first in the bundle
+ }
+ cert, err := x509.ParseCertificate(block.Bytes)
+ if err != nil {
+ return 0
+ }
+ return cert.NotAfter.Unix()
+ }
+ return 0
+}
diff --git a/internal/domains/reconcile_test.go b/internal/domains/reconcile_test.go
new file mode 100644
index 000000000..1ba90d3f5
--- /dev/null
+++ b/internal/domains/reconcile_test.go
@@ -0,0 +1,329 @@
+package domains_test
+
+import (
+ "bytes"
+ "context"
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "encoding/pem"
+ "errors"
+ "math/big"
+ "os"
+ "path/filepath"
+ "reflect"
+ "testing"
+ "time"
+
+ "github.com/carlosframework/platform/internal/domains"
+ "github.com/carlosframework/platform/internal/registry"
+ "github.com/carlosframework/platform/internal/release"
+)
+
+var ctx = context.Background()
+
+func dirStore(t *testing.T) *release.DirStore {
+ t.Helper()
+ return release.NewDirStore(t.TempDir())
+}
+
+func regWith(t *testing.T, routes ...registry.Route) *registry.Registry {
+ t.Helper()
+ reg, err := registry.Open(filepath.Join(t.TempDir(), "registry.db"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ for _, rt := range routes {
+ if err := reg.Upsert(rt); err != nil {
+ t.Fatal(err)
+ }
+ }
+ return reg
+}
+
+// manifest writes the console-shaped account manifest for app.
+func manifest(t *testing.T, s release.Store, app, account, owner string) {
+ t.Helper()
+ body := []byte(`{"account":"` + account + `","owner":"` + owner + `"}`)
+ if _, err := s.Put(ctx, "apps/"+app+"/account.json", body, ""); err != nil {
+ t.Fatal(err)
+ }
+}
+
+// cfg is the standard reconcile config: apps domain set, a fake resolver
+// saying every host points at the edge, DNS/TLS seams otherwise defaulted.
+func cfg(t *testing.T) domains.Config {
+ t.Helper()
+ return domains.Config{
+ AppsDomain: "oncarlos.test",
+ EdgeIPs: []string{"203.0.113.7"},
+ Resolve: func(host string) ([]string, error) { return []string{"203.0.113.7"}, nil },
+ Now: func() time.Time { return time.Unix(1754000000, 0) },
+ }
+}
+
+func mustGet(t *testing.T, reg *registry.Registry, host string) *registry.Route {
+ t.Helper()
+ rt, err := reg.Get(host)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if rt == nil {
+ t.Fatalf("route %s missing", host)
+ }
+ return rt
+}
+
+func routeCount(t *testing.T, reg *registry.Registry) int {
+ t.Helper()
+ routes, err := reg.List()
+ if err != nil {
+ t.Fatal(err)
+ }
+ return len(routes)
+}
+
+func TestReconcileMintsPlatformDomainUnderOps(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 := domains.Reconcile(ctx, s, reg, cfg(t)); err != nil {
+ t.Fatal(err)
+ }
+ // No manifest anywhere: the app lives under the built-in ops account
+ // (id 1, Sqid "bab" — frozen).
+ got := mustGet(t, reg, "kass.bab.oncarlos.test")
+ if !got.Platform || got.Addr != "127.0.0.1:9000" || got.Account != "ops" || got.App != "kass" {
+ t.Errorf("minted route = %+v", got)
+ }
+ n := routeCount(t, reg)
+ if err := domains.Reconcile(ctx, s, reg, cfg(t)); err != nil {
+ t.Fatal(err)
+ }
+ if routeCount(t, reg) != n {
+ t.Errorf("second Reconcile changed the route count: %d -> %d", n, routeCount(t, reg))
+ }
+}
+
+func TestReconcileUsesManifestAccount(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"})
+ manifest(t, s, "kass", "paul", "paul@keymail.dev")
+ if err := domains.Reconcile(ctx, s, reg, cfg(t)); err != nil {
+ t.Fatal(err)
+ }
+ // paul is the first ensured account after ops: id 2 → frozen sqid "bac".
+ got := mustGet(t, reg, "kass.bac.oncarlos.test")
+ if !got.Platform || got.Account != "paul" {
+ t.Errorf("minted route = %+v", got)
+ }
+ if src := mustGet(t, reg, "kass.training"); src.Account != "paul" {
+ t.Errorf("source route account not stamped from manifest: %+v", src)
+ }
+}
+
+func TestReconcileMintsCanaryHost(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"})
+ // The pointer's existence is what reconcile reads — content is not its
+ // business (the edge verifies signatures when serving).
+ if _, err := s.Put(ctx, "apps/kass/channels/canary/pricing.json", []byte("{}"), ""); err != nil {
+ t.Fatal(err)
+ }
+ if err := domains.Reconcile(ctx, s, reg, cfg(t)); err != nil {
+ t.Fatal(err)
+ }
+ got := mustGet(t, reg, "pricing.kass.bab.oncarlos.test")
+ if !got.Platform || got.Channel != "canary/pricing" || got.Addr != "127.0.0.1:9000" || !got.Canary {
+ t.Errorf("minted canary route = %+v", got)
+ }
+ // A followed canary never doubles up: the minted route follows the
+ // channel now, so a second pass mints nothing new.
+ n := routeCount(t, reg)
+ if err := domains.Reconcile(ctx, s, reg, cfg(t)); err != nil {
+ t.Fatal(err)
+ }
+ if routeCount(t, reg) != n {
+ t.Errorf("second Reconcile changed the route count: %d -> %d", n, routeCount(t, reg))
+ }
+}
+
+func TestReconcileRemovesRetiredCanaryHostOnly(t *testing.T) {
+ // The canary/old pointer is GONE from the bucket; its platform-minted
+ // follower must be removed, but the custom route following the same
+ // channel is owner property — reconciliation never touches it.
+ s, reg := dirStore(t), regWith(t,
+ registry.Route{Host: "kass.training", Kind: "instance", App: "kass",
+ Status: "ready", Addr: "127.0.0.1:9000"},
+ registry.Route{Host: "old.kass.bab.oncarlos.test", Kind: "instance", App: "kass",
+ Status: "ready", Addr: "127.0.0.1:9000", Channel: "canary/old", Platform: true, Canary: true},
+ registry.Route{Host: "rc.kass.training", Kind: "instance", App: "kass",
+ Status: "ready", Addr: "127.0.0.1:9000", Channel: "canary/old"},
+ )
+ if err := domains.Reconcile(ctx, s, reg, cfg(t)); err != nil {
+ t.Fatal(err)
+ }
+ if rt, err := reg.Get("old.kass.bab.oncarlos.test"); err != nil || rt != nil {
+ t.Errorf("retired canary's platform route still present: %+v %v", rt, err)
+ }
+ mustGet(t, reg, "rc.kass.training") // the custom follower survives
+}
+
+func TestReconcileNeverTouchesCustomDomains(t *testing.T) {
+ // A custom route whose app is dead (nothing ready, nothing shipped):
+ // reconciliation must leave it exactly alone — and mint nothing, since
+ // there is no ready upstream to copy.
+ s, reg := dirStore(t), regWith(t, registry.Route{Host: "kass.training", Kind: "instance",
+ App: "kass", Status: "failed", Addr: "127.0.0.1:9000"})
+ if err := domains.Reconcile(ctx, s, reg, cfg(t)); err != nil {
+ t.Fatal(err)
+ }
+ mustGet(t, reg, "kass.training")
+ if rt, err := reg.Get("kass.bab.oncarlos.test"); err != nil || rt != nil {
+ t.Errorf("minted a platform route with no ready upstream: %+v %v", rt, err)
+ }
+}
+
+func TestReconcileStampsDNS(t *testing.T) {
+ s, reg := dirStore(t), regWith(t,
+ registry.Route{Host: "points.test", Kind: "static", Status: "ready"},
+ registry.Route{Host: "elsewhere.test", Kind: "static", Status: "ready"},
+ registry.Route{Host: "broken.test", Kind: "static", Status: "ready"},
+ )
+ c := cfg(t)
+ c.Resolve = func(host string) ([]string, error) {
+ switch host {
+ case "points.test":
+ return []string{"198.51.100.9", "203.0.113.7"}, nil // edge IP among others
+ case "elsewhere.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 host, want := range map[string]string{
+ "points.test": "pointing",
+ "elsewhere.test": "elsewhere",
+ "broken.test": "", // resolver failure is UNKNOWN, never a guess
+ } {
+ got := mustGet(t, reg, host)
+ if got.DNSStatus != want {
+ t.Errorf("%s dns_status = %q, want %q", host, got.DNSStatus, want)
+ }
+ if got.DNSCheckedAt != now {
+ t.Errorf("%s dns_checked_at = %d, want %d", host, got.DNSCheckedAt, now)
+ }
+ }
+}
+
+// writeCertBundle writes what autocert's cachePut writes under DirCache key
+// <host>: the private key PEM block first, then the CERTIFICATE chain — so
+// the parser is exercised on skipping the key block.
+func writeCertBundle(t *testing.T, dir, host string, notAfter time.Time) {
+ t.Helper()
+ key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ if err != nil {
+ t.Fatal(err)
+ }
+ tmpl := &x509.Certificate{
+ SerialNumber: big.NewInt(1),
+ Subject: pkix.Name{CommonName: host},
+ DNSNames: []string{host},
+ NotBefore: notAfter.Add(-24 * time.Hour),
+ NotAfter: notAfter,
+ }
+ der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
+ if err != nil {
+ t.Fatal(err)
+ }
+ kb, err := x509.MarshalECPrivateKey(key)
+ if err != nil {
+ t.Fatal(err)
+ }
+ var buf bytes.Buffer
+ if err := pem.Encode(&buf, &pem.Block{Type: "EC PRIVATE KEY", Bytes: kb}); err != nil {
+ t.Fatal(err)
+ }
+ if err := pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: der}); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(filepath.Join(dir, host), buf.Bytes(), 0o600); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestReconcileStampsCertExpiry(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"},
+ registry.Route{Host: "junk.test", Kind: "static", Status: "ready"},
+ )
+ acmeDir := t.TempDir()
+ notAfter := time.Date(2026, 10, 12, 0, 0, 0, 0, time.UTC)
+ writeCertBundle(t, acmeDir, "kass.training", notAfter)
+ // An unparseable cache entry is unknown, same as absent — never a crash,
+ // never a guess.
+ if err := os.WriteFile(filepath.Join(acmeDir, "junk.test"), []byte("not pem at all"), 0o600); err != nil {
+ t.Fatal(err)
+ }
+ c := cfg(t)
+ c.AcmeDir = acmeDir
+ if err := domains.Reconcile(ctx, s, reg, c); err != nil {
+ t.Fatal(err)
+ }
+ if got := mustGet(t, reg, "kass.training"); got.CertExpiresAt != notAfter.Unix() {
+ t.Errorf("kass.training cert_expires_at = %d, want %d", got.CertExpiresAt, notAfter.Unix())
+ }
+ if got := mustGet(t, reg, "junk.test"); got.CertExpiresAt != 0 {
+ t.Errorf("junk.test cert_expires_at = %d, want 0 (unparseable = unknown)", got.CertExpiresAt)
+ }
+ // The freshly minted platform route has no cached cert yet: 0.
+ if got := mustGet(t, reg, "kass.bab.oncarlos.test"); got.CertExpiresAt != 0 {
+ t.Errorf("minted route cert_expires_at = %d, want 0 (absent = unknown)", got.CertExpiresAt)
+ }
+}
+
+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"})
+ manifest(t, s, "kass", "paul", "paul@keymail.dev") // present but must not be read
+ before, err := reg.List()
+ if err != nil {
+ t.Fatal(err)
+ }
+ c := cfg(t)
+ c.AppsDomain = ""
+ if err := domains.Reconcile(ctx, s, reg, c); err != nil {
+ t.Fatal(err)
+ }
+ after, err := reg.List()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !reflect.DeepEqual(before, after) {
+ t.Errorf("unconfigured Reconcile changed the registry:\nbefore %+v\nafter %+v", before, after)
+ }
+ if id, _ := reg.AccountID("paul"); id != 0 {
+ t.Error("unconfigured Reconcile read a manifest and minted an account")
+ }
+}
+
+func TestReconcileSkipsMalformedManifest(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 := s.Put(ctx, "apps/bad/account.json", []byte("{not json"), ""); err != nil {
+ t.Fatal(err)
+ }
+ manifest(t, s, "kass", "paul", "paul@keymail.dev")
+ if err := domains.Reconcile(ctx, s, reg, cfg(t)); err != nil {
+ t.Fatalf("one bad manifest must not fail the pass: %v", err)
+ }
+ if src := mustGet(t, reg, "kass.training"); src.Account != "paul" {
+ t.Errorf("good manifest not processed after the bad one: %+v", src)
+ }
+}