| 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 |