carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package edgesfile
2
3import (
4 "context"
5 "log"
6 "slices"
7 "time"
8
9 "github.com/carlosframework/platform/internal/release"
10 "github.com/carlosframework/platform/internal/status"
11)
12
13// EdgeIPs is the deployment's live edge address set: every EIP published by a
14// box whose STATUS SNAPSHOT is still fresh, unioned with env (the operator's
15// PLATFORM_EDGE_IPS).
16//
17// The union is the whole failure story. Nothing here deletes anything, so the
18// fold can only WIDEN what counts as "pointing at us" — which means a short
19// fold, an unreadable store, or a deployment with no snapshots at all
20// degrades to exactly the pre-fold behaviour (env only) and never below it.
21// Contrast internal/steering, which fails CLOSED on a short fold because a
22// short fold THERE deletes DNS records.
23//
24// Liveness is the status snapshot, never the edges file's own Generation:
25// Generation moves only when the file is WRITTEN, and the writer's change
26// gate exists precisely so an unchanged file is never republished — so a
27// healthy box's Generation is arbitrarily old and would fail any age bound.
28// statusTick's WriteBox is unconditional every tick, so BoxStale is a real
29// liveness signal. Without one, a retired box's lingering edges file would
30// keep a DEAD customer domain reading "pointing" forever.
31//
32// LIMITATION, and the reason for the log line below: that join reads both
33// halves out of the ONE store it is handed, but the two halves are written
34// through different ones. Edges files go through openDeployStore
35// (CARLOS_DEPLOYMENT_BUCKET, unprefixed); box snapshots go through
36// openStatusStore, which honours CARLOS_STATUS_BUCKET and rebases everything
37// under CARLOS_STATUS_PREFIX. They are the same store by DEFAULT, not by
38// guarantee — README.md's fleet section suggests a separate status bucket
39// outright — and on a deployment that splits them nothing here can see the
40// other half, so every edges file is dropped as unproven and the answer is
41// the env list. That degradation is SAFE (env-only is exactly the
42// pre-fold behaviour) but it is INVISIBLE, which is the failure class this
43// whole rule exists to avoid. So when files were found and none of them
44// joined, say so — configuration is the likely cause and no test can catch
45// it.
46func EdgeIPs(ctx context.Context, s release.Store, env []string, now time.Time) []string {
47 out := append([]string(nil), env...)
48 add := func(ip string) {
49 if ip != "" && !slices.Contains(out, ip) {
50 out = append(out, ip)
51 }
52 }
53 if s == nil {
54 return out
55 }
56 live := map[string]bool{}
57 boxes, err := status.LoadBoxes(ctx, s)
58 if err != nil {
59 log.Printf("edgesfile: could not load box snapshots (%v) — edge set falls back to the configured list", err)
60 return out
61 }
62 for _, b := range boxes {
63 if !status.BoxStale(b, now, status.SnapshotPeriod) {
64 live[b.Label] = true
65 }
66 }
67 keys, err := s.List(ctx, Prefix)
68 if err != nil {
69 log.Printf("edgesfile: could not list %s (%v) — edge set falls back to the configured list", Prefix, err)
70 return out
71 }
72 joined := 0
73 for _, key := range keys {
74 body, _, err := s.Get(ctx, key)
75 if err != nil {
76 log.Printf("edgesfile: %s: %v", key, err)
77 continue
78 }
79 f, err := Parse(body)
80 if err != nil {
81 log.Printf("edgesfile: %s: %v", key, err)
82 continue
83 }
84 if !live[f.Box] {
85 continue // no fresh snapshot: not evidence of a live edge
86 }
87 joined++
88 add(f.EIP)
89 }
90 // Files exist and not one of them proved live. A single retired box is
91 // the ordinary reading of a partial miss, but a TOTAL miss usually is
92 // not: either every box in the deployment went dark at once, or the two
93 // halves of the join are looking at different buckets (see the doc
94 // comment), or the box names on the two sides have drifted apart. All
95 // three are silent otherwise — the answer is simply the env list, with
96 // no error to return and nothing red anywhere. This line is the
97 // tripwire. Nothing is logged when there are no files at all: a
98 // deployment that has not published any is an ordinary state, not a
99 // fault.
100 if len(keys) > 0 && joined == 0 {
101 log.Printf("edgesfile: %d edges file(s) under %s but no fresh box snapshot matched any of them — edge set is the configured list only (are edges and status in different buckets? see CARLOS_STATUS_BUCKET/CARLOS_STATUS_PREFIX)", len(keys), Prefix)
102 }
103 return out
104}
105
106// DeploymentRoutes is every hostname a LIVE box publishes as a real route —
107// a home entry WITHOUT the alias mark. It is the evidence a hostname claim is
108// checked against before it may materialize as an alias.
109//
110// Alias entries are excluded deliberately and the exclusion is the whole
111// point: an alias IS a materialized claim, so counting one would make every
112// claim reserve its own hostname against itself and no re-attach or
113// re-derivation would ever succeed again. Only a NON-alias home entry — an
114// operator-minted or reconciler-minted route row — is evidence that some
115// other tenant's traffic already belongs to that name.
116//
117// Live is the same box-snapshot rule EdgeIPs uses, and for the same reason: a
118// retired box's lingering file must not go on reserving hostnames forever.
119//
120// Best-effort by contract: a store that cannot be read returns nil, and a nil
121// answer means "nothing known", which callers must treat as permissive. The
122// per-box byHost check and the fold's route preference are the other two
123// layers; this one is the only one that can act across boxes, but it must not
124// be able to break ordinary attach when the bucket is unavailable.
125func DeploymentRoutes(ctx context.Context, s release.Store, now time.Time) map[string]bool {
126 if s == nil {
127 return nil
128 }
129 live := map[string]bool{}
130 boxes, err := status.LoadBoxes(ctx, s)
131 if err != nil {
132 log.Printf("edgesfile: could not load box snapshots (%v) — deployment route set unknown this pass", err)
133 return nil
134 }
135 for _, b := range boxes {
136 if !status.BoxStale(b, now, status.SnapshotPeriod) {
137 live[b.Label] = true
138 }
139 }
140 keys, err := s.List(ctx, Prefix)
141 if err != nil {
142 log.Printf("edgesfile: could not list %s (%v) — deployment route set unknown this pass", Prefix, err)
143 return nil
144 }
145 out := map[string]bool{}
146 joined := 0
147 for _, key := range keys {
148 body, _, err := s.Get(ctx, key)
149 if err != nil {
150 log.Printf("edgesfile: %s: %v", key, err)
151 continue
152 }
153 f, err := Parse(body)
154 if err != nil {
155 log.Printf("edgesfile: %s: %v", key, err)
156 continue
157 }
158 if !live[f.Box] {
159 continue // no fresh snapshot: not evidence of a live route
160 }
161 joined++
162 for host, h := range f.Hosts {
163 if h.Role == RoleHome && !h.Alias {
164 out[host] = true
165 }
166 }
167 }
168 // EdgeIPs' tripwire, and it matters MORE here. There a total miss
169 // degrades to the operator's configured edge list, which is merely the
170 // pre-fold behaviour; here it degrades to "no hostname is anyone's",
171 // which silently disarms a SECURITY guard — the answer is an empty map,
172 // no error, nothing red, and every claim materializes exactly as it did
173 // before this rule existed. The likely causes are the same three
174 // (deployment vs status bucket split, every box dark at once, box-name
175 // drift), and they are all invisible otherwise. Silent on zero files: a
176 // deployment that has published none is an ordinary state, not a fault.
177 if len(keys) > 0 && joined == 0 {
178 log.Printf("edgesfile: %d edges file(s) under %s but no fresh box snapshot matched any of them — the deployment route set is EMPTY this pass, so hostname claims are unchecked against other boxes' routes (are edges and status in different buckets? see CARLOS_STATUS_BUCKET/CARLOS_STATUS_PREFIX)", len(keys), Prefix)
179 }
180 return out
181}
182