| index 130626a37..fb026b591 100644 |
| --- a/internal/registry/registry.go |
| +++ b/internal/registry/registry.go |
| @@ -415,6 +415,27 @@ CREATE TABLE IF NOT EXISTS wildcard_domains ( |
| account TEXT NOT NULL DEFAULT '', |
| app TEXT NOT NULL DEFAULT '', |
| created_at INTEGER NOT NULL DEFAULT 0 |
| +);`); err != nil { |
| + return err |
| + } |
| + // route_paths is a derived table (the domains reconcile pass owns every |
| + // row, materializing a claim's Paths map into it) — a plain CREATE TABLE |
| + // IF NOT EXISTS, the same additive shape as route_aliases and |
| + // wildcard_domains above, so existing DBs migrate on open with zero rows |
| + // and no behaviour change. |
| + // |
| + // The key is the PAIR: `routes` has host as its primary key and must |
| + // stay one row per hostname (that row owns the cert, the home box, the |
| + // DNS state and the aliases), so a hostname's prefixes live here instead. |
| + // prefix is a BARE segment — "paulca", never "/paulca". |
| + if _, err := r.sql.Exec(` |
| +CREATE TABLE IF NOT EXISTS route_paths ( |
| + host TEXT NOT NULL, |
| + prefix TEXT NOT NULL, |
| + target TEXT NOT NULL, |
| + account TEXT NOT NULL DEFAULT '', |
| + app TEXT NOT NULL DEFAULT '', |
| + PRIMARY KEY (host, prefix) |
| );`); err != nil { |
| return err |
| } |
| @@ -1594,6 +1615,66 @@ func (r *Registry) DeleteWildcardDomain(host string) error { |
| return err |
| } |
| |
| +// RoutePath is one derived row of the route_paths table: a leading URL path |
| +// segment on Host that is served by the route at Target instead of by Host's |
| +// own route. Wholly pass-owned, like route_aliases and wildcard_domains: |
| +// every row traces to a claim's Paths entry, so removal needs no ownership |
| +// flag. |
| +// |
| +// A path row deliberately carries NO upstream, channel or environment of its |
| +// own. It is a pointer; everything about how the traffic is served comes from |
| +// Target's ordinary route row. That is what makes pointing a subset of |
| +// prefixes at a second route a working canary rather than a special case. |
| +// |
| +// Prefix is a bare segment — "paulca", never "/paulca". |
| +type RoutePath struct { |
| + Host string |
| + Prefix string |
| + Target string |
| + Account string |
| + App string |
| +} |
| + |
| +// ListPaths returns every path route, ordered by host then prefix. |
| +func (r *Registry) ListPaths() ([]RoutePath, error) { |
| + rows, err := r.sql.Query(`SELECT host, prefix, target, account, app |
| +FROM route_paths ORDER BY host, prefix`) |
| + if err != nil { |
| + return nil, err |
| + } |
| + defer rows.Close() |
| + var out []RoutePath |
| + for rows.Next() { |
| + var p RoutePath |
| + if err := rows.Scan(&p.Host, &p.Prefix, &p.Target, &p.Account, &p.App); err != nil { |
| + return nil, err |
| + } |
| + out = append(out, p) |
| + } |
| + return out, rows.Err() |
| +} |
| + |
| +// UpsertPath inserts or repoints one path route by its (host, prefix) key. |
| +// Unlike UpsertAlias there are no stamp columns to preserve — nothing on this |
| +// table is maintained on a separate cadence — so every column is overwritten. |
| +func (r *Registry) UpsertPath(p RoutePath) error { |
| + _, err := r.sql.Exec(` |
| +INSERT INTO route_paths (host, prefix, target, account, app) |
| +VALUES (?, ?, ?, ?, ?) |
| +ON CONFLICT(host, prefix) DO UPDATE SET |
| + target=excluded.target, account=excluded.account, app=excluded.app`, |
| + p.Host, p.Prefix, p.Target, p.Account, p.App) |
| + return err |
| +} |
| + |
| +// DeletePath removes one path route. Idempotent: a row already gone is a |
| +// success, the same contract as DeleteAlias — the reconcile pass deletes |
| +// unconditionally from its own view of what is no longer desired. |
| +func (r *Registry) DeletePath(host, prefix string) error { |
| + _, err := r.sql.Exec(`DELETE FROM route_paths WHERE host = ? AND prefix = ?`, host, prefix) |
| + return err |
| +} |
| + |
| // Fingerprint summarizes the routes, fleets, fleet_boxes, route_aliases and |
| // wildcard_domains tables over a fresh connection, for the edge's cheap |
| // change poll. It is content-accurate: it hashes the actual VALUES of the |
| @@ -1669,6 +1750,13 @@ func (r *Registry) DeleteWildcardDomain(host string) error { |
| // columns are hashed — there is no cadence-stamped column on this table to |
| // exclude, unlike route_aliases' dns_status/cert_expires_at. |
| // |
| +// route_paths is hashed for the same reload-latency reason, and it is the |
| +// section most worth stating explicitly: a path flip is the ONLY routing |
| +// change in the system whose whole observable effect is in this table. A |
| +// missing section here would not degrade a flip, it would make flips do |
| +// nothing at all — reconciled correctly, stored correctly, never served — |
| +// until some unrelated route write happened to move the digest. |
| +// |
| // The ORDER BY clauses are what make the digest deterministic across polls; |
| // a hypothetical reordering would only cost a redundant reload (idempotent), |
| // never a missed change — the failure mode worth engineering against is the |
| @@ -1792,6 +1880,30 @@ FROM fleet_boxes ORDER BY fleet, label`) |
| return "", err |
| } |
| |
| + // route_paths: which prefix on which hostname reaches which route is a |
| + // routing decision in exactly the sense route_aliases' alias/host is, so |
| + // it hashes here or a flip never reaches the live map. All five columns |
| + // hash — unlike route_aliases there is no box-stamped, cadence-driven |
| + // column on this table to exclude, so there is nothing here that could |
| + // force a pointless fleet-wide reload. |
| + fmt.Fprint(h, "\x1droute_paths\x1e") |
| + prows, err := db.Query(`SELECT host, prefix, target, account, app |
| +FROM route_paths ORDER BY host, prefix`) |
| + if err != nil { |
| + return "", err |
| + } |
| + defer prows.Close() |
| + for prows.Next() { |
| + var host, prefix, target, account, app string |
| + if err := prows.Scan(&host, &prefix, &target, &account, &app); err != nil { |
| + return "", err |
| + } |
| + fmt.Fprintf(h, "%s\x1f%s\x1f%s\x1f%s\x1f%s\x1e", host, prefix, target, account, app) |
| + } |
| + if err := prows.Err(); err != nil { |
| + return "", err |
| + } |
| + |
| return fmt.Sprintf("%d:%x", n, h.Sum(nil)), nil |
| } |
| |