carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1// Alias reconcile pass — derives the registry's route_aliases table AND its
2// wildcard_domains and route_paths tables from the global claims recorded in
3// the bucket (#151). A claim contributes an ordinary alias row when it names
4// a Route (see the loop below); a Wildcard claim additionally contributes a
5// wildcard_domains row (a delegated-ACME wildcard-cert parent), a
6// Wildcard+Catchall claim also contributes a "*.<host>" route_aliases row
7// pointing at the same primary, and a claim's Paths map contributes one
8// route_paths row per prefix (path-based routing). See claims.go for the
9// claim shape and registry.go's RouteAlias/WildcardDomain/RoutePath for the
10// storage shapes; this file is the seam that turns one into the other.
11package domains
12
13import (
14 "context"
15 "log"
16 "slices"
17 "sort"
18 "strconv"
19 "strings"
20 "sync"
21 "time"
22
23 "github.com/carlosframework/platform/internal/registry"
24 "github.com/carlosframework/platform/internal/release"
25)
26
27// maxWildcardDomains caps wildcard_domains per box: every delegated parent
28// shares one ACME account's new-order budget, so an unbounded claim set
29// could starve platform issuance. Deterministic (claims come in list
30// order); drops are logged, never silent.
31const maxWildcardDomains = 32
32
33// sidecarSuffix mirrors provision.SidecarSuffix, pinned here rather than
34// imported — the same reason claims.go's hostLabelRe gives: internal/provision
35// (its reconcile.go) already imports internal/domains for account/route
36// materialization, so importing provision back would be a cycle.
37//
38// A sidecar host is refused a route at the structural level (edge.go's
39// Reload skips Kind=="sidecar"), but that guarantee protects only the
40// SIDECAR row itself. This constant guards the other direction, at the ONE
41// place that can mint a route_aliases row from nothing: a claim naming a
42// reserved-suffix host as either end of an alias would insert that host
43// into the edge's routing map serving an ORDINARY route's upstream — which
44// Reload's Kind check never sees, because the row it reads is the alias's
45// primary, not a Kind=="sidecar" row at all.
46const sidecarSuffix = ".sidecar.internal"
47
48// maxInstanceAliases caps the alias rows instance catch-alls derive on one
49// box. Every alias row costs a serial DNS lookup in the box's health stamp
50// and about 140 bytes in its status snapshot, which readers skip whole
51// above 1 MiB — and a skipped box drops out of the deployment's route and
52// edge-address folds. The cap keeps a large app from carrying a box past
53// that. Deterministic (claims in list order, routes in host order), so a
54// rerun keeps the same names; the overflow is logged, never silent.
55const maxInstanceAliases = 1000
56
57// instanceCatchallSaid holds the instance catch-alls whose refusal has
58// already been logged by this process, keyed by claim host and reason. The
59// pass runs every couple of seconds on every box, and a catch-all left
60// without its parent claim is a standing state, not an event.
61var instanceCatchallSaid sync.Map
62
63func sayInstanceCatchallOnce(host, reason string) {
64 if _, said := instanceCatchallSaid.LoadOrStore(host+"\x00"+reason, true); !said {
65 log.Printf("domains: instance catch-all %s: %s", host, reason)
66 }
67}
68
69// deriveInstanceAliases adds the alias rows the instance catch-alls derive
70// (Claim.Instances): for each catch-all "*.<parent>", one row
71// <label>.<parent> -> <instance host> per LOCAL instance route of the
72// claim's account and app, where <label> is the route's Slug — the first
73// label of an instance host, which is what the instance create door makes
74// it. The rows are the same rows an exact alias claim for each name would
75// produce, so every reader of route_aliases (the edge, the edges file, the
76// readings sweep, the issuer) treats them exactly as it treats those.
77//
78// What a derived name never beats, in order:
79//
80// - a claim on that exact name, WHOEVER holds it — judged from the
81// bucket LISTING, so a claim whose body failed to read this pass still
82// wins. An exact claim is a
83// decision somebody made about this one name; a derived name is a
84// default. This is also what lets a same-account exact claim (www,
85// auth) or another app's exact claim sit beside the catch-all without
86// a special case.
87// - a real route on this box, or one any live box in the deployment
88// publishes (deploymentRoutes) — "routes always win", as for every
89// alias.
90//
91// The catch-all itself is honoured only while its parent's own claim is
92// held by the same account (AttachInstanceCatchall's door rule, re-checked
93// here because the parent claim can be released afterwards). Any other
94// field on the claim (Route, Wildcard, Catchall, Paths) means it was not
95// written by the door, and it derives nothing.
96//
97// Instance routes are taken from this box's registry, so each box derives
98// the names of the instances it serves — the same per-box scope the plain
99// alias half has — and a name follows its instance when the instance moves.
100// Only provisioned, non-sidecar, non-replica rows whose Slug is a single
101// label prefixing their host count. That excludes hand-added routes (whose
102// account column may be a legacy name that reads as ops), replica rows (a
103// replica serves its home's account from another box under its own host;
104// the name belongs to the home) and first-route static rows (no Slug).
105//
106// A label two of the app's instances share derives NO name. That can only
107// happen after an operator changed the app's manifest domain between them
108// (every host under one domain has its own label), and the two instances
109// may well be homed on different boxes: a rule each box applied to its own
110// routes would map the one name to two databases depending on which edge a
111// request landed on. The duplicate is seen deployment-wide, from the app's
112// instance records in the bucket (labelHosts), so every box refuses alike,
113// and the refusal is logged.
114//
115// labelHosts is asked once per catch-all, and only when this box has an
116// instance it could name. If it fails, the catch-all's existing rows are
117// kept as they are for this pass rather than dropped: a listing blip must
118// not take every team's name down.
119func deriveInstanceAliases(claims []Claim, listed map[string]bool, routes []registry.Route, byHost map[string]registry.Route, existing []registry.RouteAlias, deploymentRoutes map[string]bool, desired map[string]bool, labelHosts func(account, app string) (map[string]int, error), reg *registry.Registry, now func() time.Time) {
120 claimed := make(map[string]Claim, len(claims))
121 for _, c := range claims {
122 claimed[c.Host] = c
123 }
124 derived := 0
125 have := make(map[string]registry.RouteAlias, len(existing))
126 for _, a := range existing {
127 have[a.Alias] = a
128 }
129 for _, c := range claims {
130 if !c.Instances {
131 continue
132 }
133 parent, ok := ParseInstanceCatchallHost(c.Host)
134 if !ok || InstanceCatchallHost(parent) != c.Host {
135 sayInstanceCatchallOnce(c.Host, "refused: not a normalised *.<parent> hostname")
136 continue
137 }
138 if c.Route != "" || c.Wildcard || c.Catchall || len(c.Paths) > 0 {
139 sayInstanceCatchallOnce(c.Host, "refused: the claim carries a route, wildcard, catchall or paths, which no door writes on a catch-all")
140 continue
141 }
142 if strings.HasSuffix(parent, sidecarSuffix) {
143 sayInstanceCatchallOnce(c.Host, "refused: reserved sidecar suffix")
144 continue
145 }
146 // A pending parent (#215) is not held: nobody has proved they
147 // control it, so it cannot lend a catch-all its names either.
148 if apex, held := claimed[parent]; !held || apex.Account != c.Account || apex.Pending {
149 sayInstanceCatchallOnce(c.Host, "inert: its parent "+parent+" is not claimed by account "+c.Account)
150 continue
151 }
152 var hostsOf map[string]int
153 for _, rt := range routes {
154 if !rt.Provisioned || rt.IsSidecar() || rt.Replica || rt.App != c.App || release.RouteAccount(rt) != c.Account {
155 continue
156 }
157 label := rt.Slug
158 if !hostLabelRe.MatchString(label) || !strings.HasPrefix(rt.Host, label+".") {
159 continue
160 }
161 if hostsOf == nil {
162 counts, err := labelHosts(c.Account, c.App)
163 if err != nil {
164 log.Printf("domains: instance catch-all %s: list the app's instances: %v — keeping its names as they are this pass", c.Host, err)
165 for _, a := range existing {
166 if a.Account == c.Account && a.App == c.App && strings.HasSuffix(a.Alias, "."+parent) &&
167 !strings.Contains(strings.TrimSuffix(a.Alias, "."+parent), ".") {
168 desired[a.Alias] = true
169 }
170 }
171 break
172 }
173 hostsOf = counts
174 }
175 if hostsOf[label] > 1 {
176 sayInstanceCatchallOnce(c.Host, "label "+label+" is shared by "+strconv.Itoa(hostsOf[label])+" instances of "+c.App+" (its instance domain was changed); "+label+"."+parent+" serves none of them until one is deleted")
177 continue
178 }
179 name := label + "." + parent
180 if n, ok := NormalizeAliasHost(name); !ok || n != name {
181 continue // too long once joined to parent
182 }
183 // listed, not claimed: a claim whose body could not be read this
184 // pass is still a claim (ClaimCache.listWithHosts).
185 if listed[name] {
186 continue
187 }
188 if _, taken := byHost[name]; taken || deploymentRoutes[name] || desired[name] {
189 continue
190 }
191 if derived >= maxInstanceAliases {
192 sayInstanceCatchallOnce(c.Host, "this box is at its cap of "+strconv.Itoa(maxInstanceAliases)+" derived names; the rest of the app's instances here serve on their own hosts only")
193 break
194 }
195 derived++
196 desired[name] = true
197 if a, ok := have[name]; ok && a.Host == rt.Host && a.Account == c.Account && a.App == c.App {
198 continue // already the row we want: no write, so no reload
199 }
200 if err := reg.UpsertAlias(registry.RouteAlias{
201 Alias: name, Host: rt.Host, Account: c.Account, App: c.App,
202 CreatedAt: now().Unix(),
203 }); err != nil {
204 log.Printf("domains: upsert instance alias %s: %v", name, err)
205 }
206 }
207 }
208}
209
210// ReconcileAliases derives route_aliases, wildcard_domains and route_paths
211// from the global claim records. All three tables are wholly pass-owned:
212// every row traces to a claim, so removal needs no ownership flag. The
213// authoritative tenancy gate is HERE — the door's route check is
214// best-effort (it cannot see the registry); a claim only materializes when
215// its primary route exists locally AND its account/app match the claim.
216// Routes always win a host collision: an alias (plain or catch-all) must
217// never shadow a real route.
218//
219// deploymentRoutes is every hostname a live box in this deployment publishes
220// as a REAL route (edgesfile.DeploymentRoutes). It extends "routes always
221// win" from this box to the whole fleet, which is the only place the rule
222// can be enforced at all: byHost below sees only local routes, and the
223// attach door cannot see the registry, because an operator-minted route has
224// no instance record to read. nil means "nothing known" and is PERMISSIVE by
225// contract — a bucket the fold could not read must never break ordinary
226// attach.
227// cache, when non-nil, folds the claims through a ClaimCache instead of
228// re-reading every claim body. This pass runs on the fast-converge leg — a
229// seconds cadence, on every serial bump — so the difference is one bucket
230// call against one plus a GET per claim in the deployment, every few
231// seconds, on every box. nil is legal and means today's uncached fold.
232func ReconcileAliases(ctx context.Context, s release.Store, reg *registry.Registry, now func() time.Time, deploymentRoutes map[string]bool, cache *ClaimCache) error {
233 claims, listed, err := cache.listWithHosts(ctx, s)
234 if err != nil {
235 return err
236 }
237 routes, err := reg.List()
238 if err != nil {
239 return err
240 }
241 byHost := make(map[string]registry.Route, len(routes))
242 for _, rt := range routes {
243 byHost[rt.Host] = rt
244 }
245 existing, err := reg.ListAliases()
246 if err != nil {
247 return err
248 }
249 desired := map[string]bool{}
250 desiredWild := map[string]registry.WildcardDomain{}
251 desiredPaths := map[string]registry.RoutePath{} // key: host + "\x00" + prefix
252 wildCount := 0
253 for _, c := range claims {
254 // A pending claim (#215) is a reservation by someone who has not
255 // proved they control the hostname, so it derives nothing at all:
256 // no alias, no prefix rows, no wildcard parent, no catch-all. That
257 // one skip is what keeps an unproven claim from being served and,
258 // because the edge orders certificates only for names it routes,
259 // from getting a certificate.
260 if c.Pending {
261 continue
262 }
263 // Plain alias derivation (#164): a routeless (first-route) claim is
264 // not an alias — that skip is scoped to THIS half only, so a
265 // routeless Wildcard claim still falls through to the wildcard
266 // derivation below.
267 if c.Route != "" {
268 rt, ok := byHost[c.Route]
269 if strings.HasSuffix(c.Route, sidecarSuffix) || strings.HasSuffix(c.Host, sidecarSuffix) {
270 // Refused on EITHER end: a reserved-suffix primary must
271 // never be given a routable name, and an alias whose OWN
272 // name is under the suffix would insert a sidecar
273 // hostname into the routing map serving an ordinary
274 // route's upstream — the direction edge.go's Kind check
275 // cannot see, because this row's Kind is never "sidecar".
276 log.Printf("domains: alias %s refused: reserved sidecar suffix (primary %s)", c.Host, c.Route)
277 } else if !ok {
278 // primary not on this box — another box's claim
279 } else if rt.App != c.App || release.RouteAccount(rt) != c.Account {
280 log.Printf("domains: alias %s refused: route %s belongs to %s/%s, claim says %s/%s",
281 c.Host, c.Route, release.RouteAccount(rt), rt.App, c.Account, c.App)
282 } else if _, taken := byHost[c.Host]; taken {
283 log.Printf("domains: alias %s skipped: a real route owns that host", c.Host)
284 } else if n, ok := NormalizeAliasHost(c.Host); !ok || n != c.Host {
285 // The DOOR's own check (NormalizeAliasHost: shape plus an
286 // interior dot, already lower-case): both doors normalise,
287 // but this row now becomes a key in a file every edge folds
288 // (#287), so a hand-written claim must not reach it.
289 log.Printf("domains: alias %s refused: not a normalised hostname", c.Host)
290 } else if deploymentRoutes[c.Host] {
291 // Another box in this deployment serves this hostname as a
292 // REAL route. byHost above can only see this box, and the
293 // attach door cannot see the registry at all, so this is the
294 // only layer that can refuse a claim on someone else's
295 // operator-minted route. Because the desired set is rebuilt
296 // every pass, a claim that materialized before this rule
297 // existed is dropped here too.
298 //
299 // Ordered AFTER the shape check on purpose: both refusals are
300 // log-only (no caller sees them, so there is no ordering
301 // oracle here), and a malformed hostname should be named as
302 // malformed rather than as somebody else's route.
303 log.Printf("domains: alias %s refused: another box serves that hostname as a route", c.Host)
304 } else {
305 desired[c.Host] = true
306 if err := reg.UpsertAlias(registry.RouteAlias{
307 Alias: c.Host, Host: c.Route, Account: c.Account, App: c.App,
308 CreatedAt: now().Unix(),
309 }); err != nil {
310 log.Printf("domains: upsert alias %s: %v", c.Host, err)
311 }
312 }
313 }
314
315 // Path derivation (prefix routing). A claim's Paths map splits its own
316 // hostname's traffic by leading URL segment; each entry names the
317 // hostname of the route that serves that segment.
318 //
319 // Gated on the claim's host being a LOCAL route whose tenancy matches
320 // the claim — the same per-box rule the alias half above follows, and
321 // for the same reason: the prefix map belongs to that route, so a box
322 // that does not serve the route derives nothing.
323 //
324 // The TARGET is resolved through the same local byHost, which makes
325 // co-location a hard constraint of the feature: a prefix's target
326 // route must be homed on the same box as the hostname's own route.
327 // There is no backhaul equivalent for a path target — backhaul fires
328 // on a host miss, and the host resolves fine here — so a target on
329 // another box derives no row anywhere at all. See the unresolved log
330 // below, and design spec §6.
331 //
332 // Ordered BEFORE the wildcard section on purpose: Paths is a third
333 // claim capability independent of Wildcard (claims.go's comment on
334 // the field), and the wildcard section below `continue`s for every
335 // non-Wildcard claim — placed after that, a Paths-only claim would
336 // never reach this block.
337 if len(c.Paths) > 0 {
338 hostRt, ok := byHost[c.Host]
339 if strings.HasSuffix(c.Host, sidecarSuffix) {
340 // The alias half's refusal, for the table path-based
341 // routing added afterwards. A route_paths row keyed on a
342 // reserved-suffix hostname is the same mistake in a
343 // different table: it inserts a sidecar's name into the
344 // edge's routing map, and edge.go's Kind check never sees
345 // it because the row it reads is a path row, not a
346 // Kind=="sidecar" route.
347 log.Printf("domains: paths on %s refused: reserved sidecar suffix", c.Host)
348 } else if !ok {
349 // The hostname's own route is not on this box; another box
350 // derives these rows.
351 } else if hostRt.App != c.App || release.RouteAccount(hostRt) != c.Account {
352 log.Printf("domains: paths on %s refused: route belongs to %s/%s, claim says %s/%s",
353 c.Host, release.RouteAccount(hostRt), hostRt.App, c.Account, c.App)
354 } else {
355 // Sorted so the cap below truncates DETERMINISTICALLY: every
356 // box must derive the identical subset, or the same URL would
357 // resolve differently depending on which edge answered.
358 prefixes := make([]string, 0, len(c.Paths))
359 for p := range c.Paths {
360 prefixes = append(prefixes, p)
361 }
362 sort.Strings(prefixes)
363 if len(prefixes) > MaxPathsPerClaim {
364 log.Printf("domains: paths on %s: %d prefixes exceeds the cap of %d, dropping %d in sorted order",
365 c.Host, len(prefixes), MaxPathsPerClaim, len(prefixes)-MaxPathsPerClaim)
366 prefixes = prefixes[:MaxPathsPerClaim]
367 }
368 // Unresolved targets are counted and named ONCE per host per
369 // pass, not logged per prefix: this pass runs every ~2s on
370 // every box, and a 10k-entry map whose targets are all
371 // elsewhere would otherwise write 10k lines every two
372 // seconds. The count is the operator's signal; the first few
373 // names are enough to identify which map is wrong.
374 unresolved := 0
375 var unresolvedEg []string
376 for _, prefix := range prefixes {
377 target := c.Paths[prefix]
378 if !validPathPrefix(prefix) {
379 log.Printf("domains: path %s/%s refused: not a bare path segment", c.Host, prefix)
380 continue
381 }
382 if strings.HasSuffix(target, sidecarSuffix) {
383 // The sharper half of the same refusal, and the one
384 // the account check above cannot make: a sidecar is
385 // the claim's OWN account's, so pointing a prefix at
386 // it passes every tenancy rule this block has. It
387 // must still be refused, because the target is the
388 // one field here that becomes a proxied upstream —
389 // this is the door through which an account could
390 // dial its own undialable member.
391 log.Printf("domains: path %s/%s refused: target %s is under the reserved sidecar suffix — nothing routes to a sidecar",
392 c.Host, prefix, target)
393 continue
394 }
395 tgtRt, ok := byHost[target]
396 // The authorization rule this feature adds: a prefix must
397 // never point another tenant's traffic at your backend.
398 // Account only — an account routing between its own apps
399 // (classic and Go ARE two apps) is the whole use case.
400 // Checked before readiness so a cross-tenant target is
401 // named as cross-tenant rather than swept into the
402 // unresolved count below.
403 if ok && release.RouteAccount(tgtRt) != c.Account {
404 log.Printf("domains: path %s/%s refused: target %s belongs to %s, claim says %s",
405 c.Host, prefix, target, release.RouteAccount(tgtRt), c.Account)
406 continue
407 }
408 if !ok || tgtRt.Status != "ready" {
409 // The target is not a local READY route, which has
410 // three distinct causes with three different
411 // remedies, and the log below cannot tell them apart
412 // from here:
413 //
414 // 1. The target route does not exist anywhere — a
415 // typo in the published map, or a route deleted
416 // out from under it. Remedy: fix the map, or
417 // re-create the route.
418 // 2. The target exists but is HOMED ON ANOTHER BOX.
419 // byHost holds this box's routes only, and there
420 // is no backhaul equivalent for a path target
421 // (backhaul fires on a HOST miss), so nothing
422 // derives the row anywhere. Remedy: co-locate —
423 // a prefix's target must be homed on the same box
424 // as the hostname's own route (design spec §6).
425 // 3. The target is local but not ready yet — a
426 // route mid-provision. Self-resolving; the row
427 // appears on the pass after it goes ready.
428 //
429 // No row either way, so the prefix falls through to
430 // the host's own route — the right failure direction
431 // for a migration: a missing destination serves the
432 // ORIGIN, never a 404. Silent traffic is the danger
433 // here, not broken traffic, which is why this is
434 // logged at all.
435 unresolved++
436 if len(unresolvedEg) < 3 && !slices.Contains(unresolvedEg, target) {
437 unresolvedEg = append(unresolvedEg, target)
438 }
439 continue
440 }
441 desiredPaths[c.Host+"\x00"+prefix] = registry.RoutePath{
442 Host: c.Host, Prefix: prefix, Target: target,
443 Account: c.Account, App: c.App,
444 }
445 }
446 if unresolved > 0 {
447 log.Printf("domains: paths on %s: %d prefix(es) name no local ready route (e.g. %s) — the target is missing, or it is homed on another box; a path target must be homed on the SAME box as %s. Those prefixes serve %s's own route.",
448 c.Host, unresolved, strings.Join(unresolvedEg, ", "), c.Host, c.Host)
449 }
450 }
451 }
452
453 // Wildcard/catch-all derivation. A claim with Catchall but not
454 // Wildcard cannot come from the write path (SetClaimCapabilities
455 // refuses it), but a hand-written or pre-migration claim could carry
456 // it — tolerated, logged, nothing derived.
457 if c.Catchall && !c.Wildcard {
458 log.Printf("domains: catch-all %s skipped: claim has catchall without wildcard", c.Host)
459 }
460 if !c.Wildcard {
461 continue
462 }
463 if !validHostShape(c.Host) {
464 log.Printf("domains: wildcard %s refused: invalid host shape", c.Host)
465 continue
466 }
467 primaryHost := c.Route
468 if primaryHost == "" {
469 primaryHost = c.Host // first-route claim: its own materialized primary
470 }
471 // A named-Route claim's own Host can itself be a local route (e.g. a
472 // domain already routed AND also carrying a wildcard cert opt-in for
473 // its subdomains). That's fine when the tenant matches; a mismatched
474 // tenant means a different app/account already owns this hostname
475 // directly, and the wildcard claim must not shadow it. The
476 // empty-Route (first-route) case is safe by construction: there
477 // primaryHost == c.Host, so this is the same route checked below.
478 if c.Route != "" {
479 if hostRt, taken := byHost[c.Host]; taken {
480 if hostRt.App != c.App || release.RouteAccount(hostRt) != c.Account {
481 log.Printf("domains: wildcard %s refused: a real route owns that host under %s/%s",
482 c.Host, release.RouteAccount(hostRt), hostRt.App)
483 continue
484 }
485 }
486 }
487 rt, ok := byHost[primaryHost]
488 if !ok {
489 continue // primary not on this box — another box's claim
490 }
491 if rt.App != c.App || release.RouteAccount(rt) != c.Account {
492 log.Printf("domains: wildcard %s refused: route %s belongs to %s/%s, claim says %s/%s",
493 c.Host, primaryHost, release.RouteAccount(rt), rt.App, c.Account, c.App)
494 continue
495 }
496 if wildCount >= maxWildcardDomains {
497 log.Printf("domains: wildcard %s dropped: per-box cap %d reached", c.Host, maxWildcardDomains)
498 continue
499 }
500 wildCount++
501 desiredWild[c.Host] = registry.WildcardDomain{
502 Host: c.Host, Account: c.Account, App: c.App, CreatedAt: now().Unix(),
503 }
504 if c.Catchall {
505 star := "*." + c.Host
506 if strings.HasSuffix(star, sidecarSuffix) || strings.HasSuffix(primaryHost, sidecarSuffix) {
507 // Same both-ends guard as the plain-alias derivation above,
508 // through this route_aliases writer's OTHER call site: a
509 // catch-all's alias name is star, and star inherits c.Host's
510 // suffix, so this also catches a wildcard host minted under
511 // the reserved suffix.
512 log.Printf("domains: catch-all %s refused: reserved sidecar suffix (primary %s)", star, primaryHost)
513 } else if _, taken := byHost[star]; taken {
514 log.Printf("domains: catch-all %s skipped: a real route owns that host", star)
515 } else if deploymentRoutes[star] {
516 // THE STAR, never c.Host. A first-route claim's own host is
517 // published as a real (non-alias) route by the claimant's OWN
518 // box — that is what "first route" means — so keying this on
519 // c.Host would refuse the catch-all for every wildcard
520 // first-route claim in the deployment, on the very box that
521 // owns it. The woodstar.app class, live today. What must not
522 // be shadowed here is a real route literally NAMED "*.<host>",
523 // which is what star asks about.
524 log.Printf("domains: catch-all %s refused: another box serves that hostname as a route", star)
525 } else {
526 desired[star] = true
527 if err := reg.UpsertAlias(registry.RouteAlias{
528 Alias: star, Host: primaryHost, Account: c.Account, App: c.App,
529 CreatedAt: now().Unix(),
530 }); err != nil {
531 log.Printf("domains: upsert catch-all %s: %v", star, err)
532 }
533 }
534 }
535 }
536 deriveInstanceAliases(claims, listed, routes, byHost, existing, deploymentRoutes, desired,
537 func(account, app string) (map[string]int, error) { return InstanceLabelCounts(ctx, s, account, app) }, reg, now)
538 for _, a := range existing {
539 if !desired[a.Alias] {
540 if err := reg.DeleteAlias(a.Alias); err != nil {
541 log.Printf("domains: remove alias %s: %v", a.Alias, err)
542 }
543 }
544 }
545
546 // wildcard_domains is wholly pass-owned, like route_aliases.
547 existingWild, err := reg.ListWildcardDomains()
548 if err != nil {
549 return err
550 }
551 for h, w := range desiredWild {
552 cur := false
553 for _, e := range existingWild {
554 if e.Host == h && e.Account == w.Account && e.App == w.App {
555 cur = true
556 break
557 }
558 }
559 if !cur {
560 if err := reg.UpsertWildcardDomain(w); err != nil {
561 log.Printf("domains: upsert wildcard %s: %v", h, err)
562 }
563 }
564 }
565 for _, e := range existingWild {
566 if _, ok := desiredWild[e.Host]; !ok {
567 if err := reg.DeleteWildcardDomain(e.Host); err != nil {
568 log.Printf("domains: remove wildcard %s: %v", e.Host, err)
569 }
570 }
571 }
572
573 // route_paths is wholly pass-owned, like route_aliases and
574 // wildcard_domains: every row traces to a claim's Paths entry, so a
575 // prefix removed from a republished map disappears here with no delete
576 // verb anywhere. That is exactly how a per-account rollback reaches the
577 // edge.
578 existingPaths, err := reg.ListPaths()
579 if err != nil {
580 return err
581 }
582 for _, p := range desiredPaths {
583 if err := reg.UpsertPath(p); err != nil {
584 log.Printf("domains: upsert path %s/%s: %v", p.Host, p.Prefix, err)
585 }
586 }
587 for _, p := range existingPaths {
588 if _, ok := desiredPaths[p.Host+"\x00"+p.Prefix]; !ok {
589 if err := reg.DeletePath(p.Host, p.Prefix); err != nil {
590 log.Printf("domains: remove path %s/%s: %v", p.Host, p.Prefix, err)
591 }
592 }
593 }
594 return nil
595}
596