// Package edgesfile READS the control/edges/.json wire shape — the // per-box publication of what a box homes (multi-region ingress spec, Part // 3) — for consumers that need the deployment-wide picture rather than one // box's own view. // // The WRITER's shape lives in internal/host (edgesFile/edgeHost): the box // agent owns what it publishes, and this package deliberately does not try // to own it too. internal/host cannot host this reader — the console does // not import internal/host, and internal/host imports internal/edge and // never the reverse — so the reader lives one level down where the console, // internal/domains and the box agent can all reach it. // // The two shapes are joined by a test, not by a shared type: keeping them // separate lets the writer add a field and roll before every reader // understands it, which is the whole point of an additive wire. // // Two readers deliberately keep their own narrow decoders and are NOT // migrated: internal/edge/homemap.go (the serving hot path, with its own // polling and staleness machinery) and internal/steering (which reads no // role values — since #162 it does read the per-host replica mark, but its // decode stays its own for the same isolation reason). Both are named here // so the consumer list stays complete rather than merely short. package edgesfile import ( "encoding/json" "fmt" ) // Prefix is the flat prefix every box's file lives under. const Prefix = "control/edges/" // RoleHome is the role a box publishes for a host it actually serves. An // entry with any other role is either a cert-only role (see Host.Wildcard) // or a role a newer build publishes; readers must ignore what they do not // recognise rather than route on it or fail the parse. const RoleHome = "home" // Key is the bucket key for box's file. func Key(box string) string { return Prefix + box + ".json" } // Host is one per-host entry, decoded. Every mark is additive, and each // defaults to false on a file written before that mark existed — but they // fail in OPPOSITE directions, which is deliberate and load-bearing: // // - Platform absent reads as "not platform", so a pre-mark publisher's rows // derive no wildcard parent (#300 fails CLOSED). // - Alias absent reads as "not an alias", so a pre-mark publisher's rows // keep full route authority and cannot be outranked by a claimed alias // (#287 fails OPEN, which is why its readers must roll first). // // The writer always emits "platform" and omits the other two when false; // this reader does not care which, because absent and false decode alike. // What the reader must never do is re-serialize a Host and call it a // publication — that is the writer's job, and round-tripping through this // struct would drop any field a newer box publishes. type Host struct { Role string `json:"role"` Platform bool `json:"platform"` Alias bool `json:"alias"` // Wildcard marks a delegated customer wildcard parent (wildcard_domains, // #305): the key is "*.". With Role RoleHome the box also serves // the catch-all; with any other role it is the cert half alone. Wildcard bool `json:"wildcard"` } // File is one box's published file. type File struct { V int `json:"v"` Box string `json:"box"` EIP string `json:"eip"` Region string `json:"region,omitempty"` Generation string `json:"generation,omitempty"` Hosts map[string]Host `json:"hosts"` } // Parse decodes one box's body. Pure — no store I/O. V < 1 (no version field // at all) is refused as not a file this reader understands; a HIGHER version // is accepted, because the schema grows additively and a newer box's file // still carries box/eip/hosts verbatim. func Parse(body []byte) (File, error) { var f File if err := json.Unmarshal(body, &f); err != nil { return File{}, fmt.Errorf("edgesfile: parse: %w", err) } if f.V < 1 { return File{}, fmt.Errorf("edgesfile: unversioned edges file") } return f, nil }