| index 46a90b812..9db796216 100644 |
| --- a/internal/domains/aliases.go |
| +++ b/internal/domains/aliases.go |
| @@ -1,6 +1,11 @@ |
| -// Alias reconcile pass — derives the registry's route_aliases table from |
| -// the global claims recorded in the bucket (#151). See claims.go for the |
| -// claim shape and registry.go's RouteAlias for the storage shape; this file |
| +// Alias reconcile pass — derives the registry's route_aliases table AND its |
| +// wildcard_domains table from the global claims recorded in the bucket |
| +// (#151). A claim contributes an ordinary alias row when it names a Route |
| +// (see the loop below); a Wildcard claim additionally contributes a |
| +// wildcard_domains row (a delegated-ACME wildcard-cert parent), and a |
| +// Wildcard+Catchall claim also contributes a "*.<host>" route_aliases row |
| +// pointing at the same primary. See claims.go for the claim shape and |
| +// registry.go's RouteAlias/WildcardDomain for the storage shapes; this file |
| // is the seam that turns one into the other. |
| package domains |
| |
| @@ -13,13 +18,19 @@ import ( |
| "github.com/carlosframework/platform/internal/release" |
| ) |
| |
| -// ReconcileAliases derives route_aliases from the global claim records. |
| -// The table is wholly pass-owned: every row traces to a claim, so removal |
| -// needs no ownership flag. The authoritative tenancy gate is HERE — the |
| -// door's route check is best-effort (it cannot see the registry); a claim |
| -// only materializes when the named route exists locally AND its |
| -// account/app match the claim. Routes always win a host collision: an |
| -// alias must never shadow a real route. |
| +// maxWildcardDomains caps wildcard_domains per box: every delegated parent |
| +// shares one ACME account's new-order budget, so an unbounded claim set |
| +// could starve platform issuance. Deterministic (claims come in list |
| +// order); drops are logged, never silent. |
| +const maxWildcardDomains = 32 |
| + |
| +// ReconcileAliases derives route_aliases and wildcard_domains from the |
| +// global claim records. Both tables are wholly pass-owned: every row traces |
| +// to a claim, so removal needs no ownership flag. The authoritative tenancy |
| +// gate is HERE — the door's route check is best-effort (it cannot see the |
| +// registry); a claim only materializes when its primary route exists |
| +// locally AND its account/app match the claim. Routes always win a host |
| +// collision: an alias (plain or catch-all) must never shadow a real route. |
| func ReconcileAliases(ctx context.Context, s release.Store, reg *registry.Registry, now func() time.Time) error { |
| claims, err := ListClaims(ctx, s) |
| if err != nil { |
| @@ -38,29 +49,81 @@ func ReconcileAliases(ctx context.Context, s release.Store, reg *registry.Regist |
| return err |
| } |
| desired := map[string]bool{} |
| + desiredWild := map[string]registry.WildcardDomain{} |
| + wildCount := 0 |
| for _, c := range claims { |
| - if c.Route == "" { // first-route claims (#82's case) are not aliases |
| + // Plain alias derivation (#164): a routeless (first-route) claim is |
| + // not an alias — that skip is scoped to THIS half only, so a |
| + // routeless Wildcard claim still falls through to the wildcard |
| + // derivation below. |
| + if c.Route != "" { |
| + rt, ok := byHost[c.Route] |
| + if !ok { |
| + // primary not on this box — another box's claim |
| + } else if rt.App != c.App || release.RouteAccount(rt) != c.Account { |
| + log.Printf("domains: alias %s refused: route %s belongs to %s/%s, claim says %s/%s", |
| + c.Host, c.Route, release.RouteAccount(rt), rt.App, c.Account, c.App) |
| + } else if _, taken := byHost[c.Host]; taken { |
| + log.Printf("domains: alias %s skipped: a real route owns that host", c.Host) |
| + } else { |
| + desired[c.Host] = true |
| + if err := reg.UpsertAlias(registry.RouteAlias{ |
| + Alias: c.Host, Host: c.Route, Account: c.Account, App: c.App, |
| + CreatedAt: now().Unix(), |
| + }); err != nil { |
| + log.Printf("domains: upsert alias %s: %v", c.Host, err) |
| + } |
| + } |
| + } |
| + |
| + // Wildcard/catch-all derivation. A claim with Catchall but not |
| + // Wildcard cannot come from the write path (SetClaimCapabilities |
| + // refuses it), but a hand-written or pre-migration claim could carry |
| + // it — tolerated, logged, nothing derived. |
| + if c.Catchall && !c.Wildcard { |
| + log.Printf("domains: catch-all %s skipped: claim has catchall without wildcard", c.Host) |
| + } |
| + if !c.Wildcard { |
| continue |
| } |
| - rt, ok := byHost[c.Route] |
| + if !validHostShape(c.Host) { |
| + log.Printf("domains: wildcard %s refused: invalid host shape", c.Host) |
| + continue |
| + } |
| + primaryHost := c.Route |
| + if primaryHost == "" { |
| + primaryHost = c.Host // first-route claim: its own materialized primary |
| + } |
| + rt, ok := byHost[primaryHost] |
| if !ok { |
| continue // primary not on this box — another box's claim |
| } |
| if rt.App != c.App || release.RouteAccount(rt) != c.Account { |
| - log.Printf("domains: alias %s refused: route %s belongs to %s/%s, claim says %s/%s", |
| - c.Host, c.Route, release.RouteAccount(rt), rt.App, c.Account, c.App) |
| + log.Printf("domains: wildcard %s refused: route %s belongs to %s/%s, claim says %s/%s", |
| + c.Host, primaryHost, release.RouteAccount(rt), rt.App, c.Account, c.App) |
| continue |
| } |
| - if _, taken := byHost[c.Host]; taken { |
| - log.Printf("domains: alias %s skipped: a real route owns that host", c.Host) |
| + if wildCount >= maxWildcardDomains { |
| + log.Printf("domains: wildcard %s dropped: per-box cap %d reached", c.Host, maxWildcardDomains) |
| continue |
| } |
| - desired[c.Host] = true |
| - if err := reg.UpsertAlias(registry.RouteAlias{ |
| - Alias: c.Host, Host: c.Route, Account: c.Account, App: c.App, |
| - CreatedAt: now().Unix(), |
| - }); err != nil { |
| - log.Printf("domains: upsert alias %s: %v", c.Host, err) |
| + wildCount++ |
| + desiredWild[c.Host] = registry.WildcardDomain{ |
| + Host: c.Host, Account: c.Account, App: c.App, CreatedAt: now().Unix(), |
| + } |
| + if c.Catchall { |
| + star := "*." + c.Host |
| + if _, taken := byHost[star]; taken { |
| + log.Printf("domains: catch-all %s skipped: a real route owns that host", star) |
| + } else { |
| + desired[star] = true |
| + if err := reg.UpsertAlias(registry.RouteAlias{ |
| + Alias: star, Host: primaryHost, Account: c.Account, App: c.App, |
| + CreatedAt: now().Unix(), |
| + }); err != nil { |
| + log.Printf("domains: upsert catch-all %s: %v", star, err) |
| + } |
| + } |
| } |
| } |
| for _, a := range existing { |
| @@ -70,5 +133,32 @@ func ReconcileAliases(ctx context.Context, s release.Store, reg *registry.Regist |
| } |
| } |
| } |
| + |
| + // wildcard_domains is wholly pass-owned, like route_aliases. |
| + existingWild, err := reg.ListWildcardDomains() |
| + if err != nil { |
| + return err |
| + } |
| + for h, w := range desiredWild { |
| + cur := false |
| + for _, e := range existingWild { |
| + if e.Host == h && e.Account == w.Account && e.App == w.App { |
| + cur = true |
| + break |
| + } |
| + } |
| + if !cur { |
| + if err := reg.UpsertWildcardDomain(w); err != nil { |
| + log.Printf("domains: upsert wildcard %s: %v", h, err) |
| + } |
| + } |
| + } |
| + for _, e := range existingWild { |
| + if _, ok := desiredWild[e.Host]; !ok { |
| + if err := reg.DeleteWildcardDomain(e.Host); err != nil { |
| + log.Printf("domains: remove wildcard %s: %v", e.Host, err) |
| + } |
| + } |
| + } |
| return nil |
| } |