carlos / platform Public

domains: static apps mint too — the channel is a static route's backend

platform-site is the case that proves it: a shipped static tree deserves
an addressable URL as much as an instance does. Static mints copy Kind
AND Channel, because the edge's static handler resolves the channel
pointer to find the tar — a channel-less static route serves nothing.
Proxied routes still deliberately don't copy Channel (Socket/Addr is
their backend; a copied canary channel would make the apex look like a
canary follower). That asymmetry now carries its why-comment.

Removal of retired canary hosts additionally requires the Canary flag:
a static apex legitimately rides a canary channel, and sweeping it on
retirement would delete the app's platform domain and re-mint it every
pass, forever.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell d2d386ec5a2d12df13c31053e80a891c5826b7c5 parent 8364197
2 files changed, +91 −20
  • internal/domains/reconcile.go +43 −20
  • internal/domains/reconcile_test.go +48 −0
diff --git a/internal/domains/reconcile.go b/internal/domains/reconcile.go
index 80c846bfa..3f3cc8dce 100644
--- a/internal/domains/reconcile.go
+++ b/internal/domains/reconcile.go
@@ -178,12 +178,17 @@ func Reconcile(ctx context.Context, s release.Store, reg *registry.Registry, cfg
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.
+ // Removal: ONLY canary hosts this pass minted, whose pointer is now gone
+ // from the bucket. Three conditions, each load-bearing: Platform (a
+ // custom route on the same channel is owner property — the flag, not the
+ // channel, grants removal authority), Canary (a STATIC app's apex mint
+ // legitimately carries a canary channel too, since that's how it finds
+ // its tar — removing it on retirement would delete the app's platform
+ // domain and re-mint it next pass, forever), and the channel prefix.
kept := routes[:0]
for _, rt := range routes {
- if rt.Platform && strings.HasPrefix(rt.Channel, "canary/") && !canaryChannels[rt.App][rt.Channel] {
+ if rt.Platform && rt.Canary && 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
@@ -265,16 +270,18 @@ func canaryPointer(key string) (app, name string, ok bool) {
}
// 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.
+// domain, in first-seen (host) order: every app — static sites included, a
+// shipped static tree deserves an addressable URL as much as an instance
+// does — outside the platform-reserved svc/* namespace, 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/") {
+ if rt.App == "" || strings.HasPrefix(rt.App, "svc/") {
continue
}
if !seen[rt.App] {
@@ -320,14 +327,19 @@ func accountSqid(reg *registry.Registry, name string) (string, error) {
}
// 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.
+// serves from: the app's first ready route in host order (reg.List's order)
+// that has something to copy and is not itself a minted route — minting
+// from a mint would be self-reference. "Something to copy" is a proxy
+// upstream (Socket or Addr) for instances and services, or — for static
+// routes — the release channel, since a static route's backend IS its
+// channel pointer, not a socket.
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 != "") {
+ if rt.App != app || rt.Platform || rt.Status != "ready" {
+ continue
+ }
+ if rt.Socket != "" || rt.Addr != "" || (rt.Kind == "static" && rt.Channel != "") {
return rt
}
}
@@ -336,13 +348,20 @@ func firstReadyUpstream(routes []registry.Route, app string) *registry.Route {
// 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.
+// that's how it was chosen), platform-flagged, account-stamped.
+//
+// Channel is copied for static routes and NOT for anything else, because
+// the two serving classes reach their backend by different mechanisms. A
+// proxied route (instance/service) reaches its backend through Socket/Addr,
+// already copied above — the channel would be decoration, and a copied
+// canary channel would make the apex route look like a canary follower, so
+// retiring that canary would delete the app's platform domain. A static
+// route has no socket at all: the edge's static handler resolves the app's
+// CHANNEL POINTER to find the shipped tar, so a channel-less static mint
+// would serve nothing. Hence the asymmetry — same field, opposite verdicts,
+// for reasons that live one layer down in internal/edge.
func mintedRoute(host, app, account string, src registry.Route) registry.Route {
- return registry.Route{
+ rt := registry.Route{
Host: host,
Kind: src.Kind,
Socket: src.Socket,
@@ -353,6 +372,10 @@ func mintedRoute(host, app, account string, src registry.Route) registry.Route {
Platform: true,
Account: account,
}
+ if src.Kind == "static" {
+ rt.Channel = src.Channel
+ }
+ return rt
}
// certExpiry reads the autocert DirCache entry at path (the PEM bundle
diff --git a/internal/domains/reconcile_test.go b/internal/domains/reconcile_test.go
index 1ba90d3f5..1379d33ae 100644
--- a/internal/domains/reconcile_test.go
+++ b/internal/domains/reconcile_test.go
@@ -123,6 +123,54 @@ func TestReconcileUsesManifestAccount(t *testing.T) {
}
}
+// A static app deserves an addressable URL as much as an instance does
+// (platform-site is exactly that case). Its minted route must carry
+// Kind=static AND the source route's Channel — the static handler resolves
+// that channel pointer to find the tar, so a channel-less static route has
+// nothing to serve.
+func TestReconcileMintsStaticAppWithChannel(t *testing.T) {
+ s, reg := dirStore(t), regWith(t, registry.Route{Host: "carlosframework.com", Kind: "static",
+ App: "platform-site", Status: "ready", Channel: "stable"})
+ if err := domains.Reconcile(ctx, s, reg, cfg(t)); err != nil {
+ t.Fatal(err)
+ }
+ got := mustGet(t, reg, "platform-site.bab.oncarlos.test")
+ if !got.Platform || got.Kind != "static" || got.Channel != "stable" || got.Account != "ops" {
+ t.Errorf("minted static route = %+v, want platform static on channel stable", got)
+ }
+ // Idempotence with the channel copy in place: the minted route now
+ // follows "stable" itself, which must not confuse a second pass into
+ // minting again (nor into treating it as a canary follower).
+ 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))
+ }
+ if got := mustGet(t, reg, "platform-site.bab.oncarlos.test"); got.Channel != "stable" {
+ t.Errorf("second pass lost the minted route's channel: %+v", got)
+ }
+}
+
+// A static app riding a canary channel mints an apex route ON that channel
+// (it must, to serve anything) — and that route must NOT be swept away as a
+// "retired canary follower" when the channel goes: only genuinely
+// canary-minted hosts are removal candidates.
+func TestReconcileKeepsStaticApexOnRetiredCanaryChannel(t *testing.T) {
+ s, reg := dirStore(t), regWith(t,
+ registry.Route{Host: "site.custom", Kind: "static", App: "site",
+ Status: "ready", Channel: "canary/x"},
+ registry.Route{Host: "site.bab.oncarlos.test", Kind: "static", App: "site",
+ Status: "ready", Channel: "canary/x", Platform: true},
+ )
+ // No canary/x pointer exists in the bucket at all (retired).
+ if err := domains.Reconcile(ctx, s, reg, cfg(t)); err != nil {
+ t.Fatal(err)
+ }
+ mustGet(t, reg, "site.bab.oncarlos.test") // the apex mint survives
+}
+
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"})