| index 7c68ed826..234d43a1a 100644 |
| --- a/internal/provision/provision.go |
| +++ b/internal/provision/provision.go |
| @@ -18,6 +18,7 @@ import ( |
| "os" |
| "path/filepath" |
| "regexp" |
| + "slices" |
| "sort" |
| "strconv" |
| "strings" |
| @@ -140,6 +141,32 @@ type Record struct { |
| // host. Converged into routes.gate by Reconcile's owned-converge arm |
| // (reconcile.go) and read by the edge on every request. Spec docs/superpowers/specs/2026-09-07-password-gate-design.md. |
| Gate string `json:"gate,omitempty"` |
| + |
| + // --- replica placements (amadan carlos/platform #162) --- |
| + |
| + // Role is what this instance IS within its app: "" (the default every |
| + // record written before this field existed decodes to) is the serving |
| + // home, the instance whose data is authoritative. "replica" is a |
| + // SECOND instance of the same account holding a one-way mirror of it, |
| + // placed near the buyers (spec §2). Written only at create; no PATCH |
| + // flips a record into or out of the role, because which database is |
| + // authoritative is a data-plane fact, not a shape edit. ValidRole is |
| + // the door. |
| + Role string `json:"role,omitempty"` |
| + |
| + // Replicas names the placements an account's replicas live in — the |
| + // HOME record's own opt-in and the residence gate (spec §2). Never |
| + // defaulted, never inferred from Region. A record carrying any |
| + // placement here must also carry Steering "latency", or the account's |
| + // host resolves to one edge and the replica serves nothing (spec §5). |
| + Replicas []string `json:"replicas,omitempty"` |
| + |
| + // Home is the public host a REPLICA record mirrors: the home record's |
| + // own host. Written at create and never converged — repointing a |
| + // replica is delete-and-recreate, because what it serves is a mirror |
| + // OF something. Carried onto the route row as replica_of, which is |
| + // what the alias derivation and the edges-file writer read (spec §3). |
| + Home string `json:"home,omitempty"` |
| } |
| |
| // Health is the per-route probe config (HA spec Part II). Zero value = |
| @@ -367,6 +394,55 @@ func ValidSteering(s string) bool { |
| return false |
| } |
| |
| +// ValidRole pins the record's role vocabulary (spec §2): "" (the serving |
| +// home — every record written before the field existed decodes to it, |
| +// which is what makes the field additive) or "replica". Case-sensitive |
| +// and closed, ValidSteering's discipline: a value a future reader has |
| +// never heard of is refused where it is typed, rather than discovered as |
| +// a row nothing routes for. |
| +func ValidRole(s string) bool { |
| + switch s { |
| + case "", "replica": |
| + return true |
| + } |
| + return false |
| +} |
| + |
| +// MaxReplicas caps the placement set one record may name. Each entry is a |
| +// decision about where an account's order data lives until a drain |
| +// confirms (spec §2), not a cache hint, so the cap is small and deliberate. |
| +const MaxReplicas = 4 |
| + |
| +// ValidReplicas normalizes and validates a replica placement set: trim, |
| +// drop empties, deduplicate, sort into the canonical order, and check each |
| +// element against ValidRegion — the same vocabulary a record's own Region |
| +// speaks. Exported because the console's door canonicalizes the request |
| +// body through the SAME derivation ValidateShape re-checks, so neither |
| +// side trusts the other about what is in the bucket. |
| +func ValidReplicas(regions []string) ([]string, error) { |
| + if len(regions) > MaxReplicas { |
| + return nil, fmt.Errorf("replicas: %d placements, at most %d — a replica holds the account's order data in the placement it names until a drain confirms, so each one is a residence decision", len(regions), MaxReplicas) |
| + } |
| + seen := make(map[string]bool, len(regions)) |
| + out := make([]string, 0, len(regions)) |
| + for _, r := range regions { |
| + r = strings.TrimSpace(r) |
| + if r == "" { |
| + return nil, errors.New("replicas: empty placement code") |
| + } |
| + if !ValidRegion(r) { |
| + return nil, fmt.Errorf("replica placement %q: want a placement code (e.g. ie, uk)", r) |
| + } |
| + if seen[r] { |
| + continue |
| + } |
| + seen[r] = true |
| + out = append(out, r) |
| + } |
| + sort.Strings(out) |
| + return out, nil |
| +} |
| + |
| // MaxUpstreams caps the Addrs set one record may name. The prober probes a |
| // host's upstreams SEQUENTIALLY once per interval (internal/edge/prober.go), |
| // and every scale figure the multi-upstream design rests on — probe traffic, |
| @@ -388,6 +464,42 @@ func ValidateShape(rec Record) error { |
| if !ValidSteering(rec.Steering) { |
| return fmt.Errorf("steering %q: want \"\" or \"latency\"", rec.Steering) |
| } |
| + if !ValidRole(rec.Role) { |
| + return fmt.Errorf("role %q: want \"\" or \"replica\"", rec.Role) |
| + } |
| + if rec.Role == "replica" { |
| + // A replica is a process holding a mirror of live data — never a |
| + // file tree, never a foreign upstream the platform did not spawn. |
| + if rec.KindOrDefault() != "instance" { |
| + return errors.New("role replica with kind " + rec.KindOrDefault() + ": a replica is an instance of the account — drop kind or drop role") |
| + } |
| + if rec.Home == "" { |
| + return errors.New("role replica with no home: a replica names the public host it mirrors") |
| + } |
| + // ValidRegion("") is true, so the placement rail is written out. |
| + if rec.Region == "" { |
| + return errors.New("role replica with no region: a replica is always in a named placement") |
| + } |
| + if len(rec.Replicas) > 0 { |
| + return errors.New("role replica with replicas: a replica of a replica is not a shape the platform carries") |
| + } |
| + } |
| + if len(rec.Replicas) > 0 { |
| + norm, err := ValidReplicas(rec.Replicas) |
| + if err != nil { |
| + return err |
| + } |
| + if slices.Contains(norm, rec.Region) && rec.Region != "" { |
| + return fmt.Errorf("replica placement %q is the record's own placement: a replica lives somewhere else", rec.Region) |
| + } |
| + // The home region normalizes to "" on a record (homeRegionIn, |
| + // console apiinstancemove.go), so this rail cannot see a home in |
| + // the deployment's own region naming it as a replica placement. |
| + // That comparison lives at the door, which knows the code (spec §2). |
| + if rec.Steering != "latency" { |
| + return errors.New("replicas without steering latency: the account's host would resolve to one edge and the replica would serve nothing — set steering first") |
| + } |
| + } |
| if rec.Gate != "" { |
| if _, err := bcrypt.Cost([]byte(rec.Gate)); err != nil { |
| return errors.New("gate: want a bcrypt hash — the console hashes the password; a record never carries it in clear") |