| 1 | // Package edgesfile READS the control/edges/<box>.json wire shape — the |
| 2 | // per-box publication of what a box homes (multi-region ingress spec, Part |
| 3 | // 3) — for consumers that need the deployment-wide picture rather than one |
| 4 | // box's own view. |
| 5 | // |
| 6 | // The WRITER's shape lives in internal/host (edgesFile/edgeHost): the box |
| 7 | // agent owns what it publishes, and this package deliberately does not try |
| 8 | // to own it too. internal/host cannot host this reader — the console does |
| 9 | // not import internal/host, and internal/host imports internal/edge and |
| 10 | // never the reverse — so the reader lives one level down where the console, |
| 11 | // internal/domains and the box agent can all reach it. |
| 12 | // |
| 13 | // The two shapes are joined by a test, not by a shared type: keeping them |
| 14 | // separate lets the writer add a field and roll before every reader |
| 15 | // understands it, which is the whole point of an additive wire. |
| 16 | // |
| 17 | // Two readers deliberately keep their own narrow decoders and are NOT |
| 18 | // migrated: internal/edge/homemap.go (the serving hot path, with its own |
| 19 | // polling and staleness machinery) and internal/steering (which reads no |
| 20 | // role values — since #162 it does read the per-host replica mark, but its |
| 21 | // decode stays its own for the same isolation reason). Both are named here |
| 22 | // so the consumer list stays complete rather than merely short. |
| 23 | package edgesfile |
| 24 | |
| 25 | import ( |
| 26 | "encoding/json" |
| 27 | "fmt" |
| 28 | ) |
| 29 | |
| 30 | // Prefix is the flat prefix every box's file lives under. |
| 31 | const Prefix = "control/edges/" |
| 32 | |
| 33 | // RoleHome is the role a box publishes for a host it actually serves. An |
| 34 | // entry with any other role is either a cert-only role (see Host.Wildcard) |
| 35 | // or a role a newer build publishes; readers must ignore what they do not |
| 36 | // recognise rather than route on it or fail the parse. |
| 37 | const RoleHome = "home" |
| 38 | |
| 39 | // Key is the bucket key for box's file. |
| 40 | func Key(box string) string { return Prefix + box + ".json" } |
| 41 | |
| 42 | // Host is one per-host entry, decoded. Every mark is additive, and each |
| 43 | // defaults to false on a file written before that mark existed — but they |
| 44 | // fail in OPPOSITE directions, which is deliberate and load-bearing: |
| 45 | // |
| 46 | // - Platform absent reads as "not platform", so a pre-mark publisher's rows |
| 47 | // derive no wildcard parent (#300 fails CLOSED). |
| 48 | // - Alias absent reads as "not an alias", so a pre-mark publisher's rows |
| 49 | // keep full route authority and cannot be outranked by a claimed alias |
| 50 | // (#287 fails OPEN, which is why its readers must roll first). |
| 51 | // |
| 52 | // The writer always emits "platform" and omits the other two when false; |
| 53 | // this reader does not care which, because absent and false decode alike. |
| 54 | // What the reader must never do is re-serialize a Host and call it a |
| 55 | // publication — that is the writer's job, and round-tripping through this |
| 56 | // struct would drop any field a newer box publishes. |
| 57 | type Host struct { |
| 58 | Role string `json:"role"` |
| 59 | Platform bool `json:"platform"` |
| 60 | Alias bool `json:"alias"` |
| 61 | // Wildcard marks a delegated customer wildcard parent (wildcard_domains, |
| 62 | // #305): the key is "*.<parent>". With Role RoleHome the box also serves |
| 63 | // the catch-all; with any other role it is the cert half alone. |
| 64 | Wildcard bool `json:"wildcard"` |
| 65 | } |
| 66 | |
| 67 | // File is one box's published file. |
| 68 | type File struct { |
| 69 | V int `json:"v"` |
| 70 | Box string `json:"box"` |
| 71 | EIP string `json:"eip"` |
| 72 | Region string `json:"region,omitempty"` |
| 73 | Generation string `json:"generation,omitempty"` |
| 74 | Hosts map[string]Host `json:"hosts"` |
| 75 | } |
| 76 | |
| 77 | // Parse decodes one box's body. Pure — no store I/O. V < 1 (no version field |
| 78 | // at all) is refused as not a file this reader understands; a HIGHER version |
| 79 | // is accepted, because the schema grows additively and a newer box's file |
| 80 | // still carries box/eip/hosts verbatim. |
| 81 | func Parse(body []byte) (File, error) { |
| 82 | var f File |
| 83 | if err := json.Unmarshal(body, &f); err != nil { |
| 84 | return File{}, fmt.Errorf("edgesfile: parse: %w", err) |
| 85 | } |
| 86 | if f.V < 1 { |
| 87 | return File{}, fmt.Errorf("edgesfile: unversioned edges file") |
| 88 | } |
| 89 | return f, nil |
| 90 | } |
| 91 | |