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 "bytes"
5 "context"
6 "errors"
7 "log"
8 "slices"
9 "strings"
10 "testing"
11 "time"
12
13 "github.com/carlosframework/platform/internal/release"
14 "github.com/carlosframework/platform/internal/status"
15)
16
17// The fold can only WIDEN: a box the deployment can see adds its EIP, and the
18// operator's env list is always included. That is the safe direction —
19// nothing here deletes, and a short fold must degrade to today's behaviour
20// (env only), never below it.
21func TestEdgeIPsUnionsTheFoldWithTheEnvList(t *testing.T) {
22 s := foldTestStore(t)
23 now := time.Date(2026, 8, 27, 12, 0, 0, 0, time.UTC)
24 putEdgesFile(t, s, "annie", `{"v":1,"box":"annie","eip":"1.1.1.1","hosts":{}}`)
25 putEdgesFile(t, s, "betty", `{"v":1,"box":"betty","eip":"2.2.2.2","hosts":{}}`)
26 putFreshBox(t, s, "annie", now)
27 putFreshBox(t, s, "betty", now)
28
29 got := EdgeIPs(context.Background(), s, []string{"9.9.9.9"}, now)
30 slices.Sort(got)
31 want := []string{"1.1.1.1", "2.2.2.2", "9.9.9.9"}
32 if !slices.Equal(got, want) {
33 t.Fatalf("EdgeIPs = %v, want %v", got, want)
34 }
35}
36
37// A box whose STATUS SNAPSHOT has gone stale drops out — that is what keeps a
38// retired box's lingering edges file from holding a dead customer domain at
39// "pointing" forever (a lying green). Generation is NOT the signal: an
40// unchanged file is never republished, so a healthy box's Generation is
41// arbitrarily old.
42func TestEdgeIPsDropsABoxWhoseSnapshotIsStale(t *testing.T) {
43 s := foldTestStore(t)
44 now := time.Date(2026, 8, 27, 12, 0, 0, 0, time.UTC)
45 putEdgesFile(t, s, "annie", `{"v":1,"box":"annie","eip":"1.1.1.1","hosts":{}}`)
46 putEdgesFile(t, s, "retired", `{"v":1,"box":"retired","eip":"3.3.3.3","hosts":{}}`)
47 putFreshBox(t, s, "annie", now)
48 putFreshBox(t, s, "retired", now.Add(-72*time.Hour))
49
50 got := EdgeIPs(context.Background(), s, nil, now)
51 if slices.Contains(got, "3.3.3.3") {
52 t.Fatalf("EdgeIPs = %v, want the retired box's EIP dropped", got)
53 }
54 if !slices.Contains(got, "1.1.1.1") {
55 t.Fatalf("EdgeIPs = %v, want the live box kept", got)
56 }
57}
58
59// An edges file with no matching status snapshot at all is not evidence of
60// liveness. Same treatment as a stale one.
61func TestEdgeIPsDropsABoxWithNoSnapshot(t *testing.T) {
62 s := foldTestStore(t)
63 now := time.Date(2026, 8, 27, 12, 0, 0, 0, time.UTC)
64 putEdgesFile(t, s, "ghost", `{"v":1,"box":"ghost","eip":"4.4.4.4","hosts":{}}`)
65 if got := EdgeIPs(context.Background(), s, []string{"9.9.9.9"}, now); !slices.Equal(got, []string{"9.9.9.9"}) {
66 t.Fatalf("EdgeIPs = %v, want env only", got)
67 }
68}
69
70// A store that cannot be listed degrades to the env list — never to empty. An
71// empty edge set would stamp every custom domain "elsewhere" at once.
72func TestEdgeIPsFallsBackToEnvWhenTheStoreFails(t *testing.T) {
73 s := brokenStore{}
74 now := time.Date(2026, 8, 27, 12, 0, 0, 0, time.UTC)
75 if got := EdgeIPs(context.Background(), s, []string{"9.9.9.9"}, now); !slices.Equal(got, []string{"9.9.9.9"}) {
76 t.Fatalf("EdgeIPs = %v, want the env list on a store failure", got)
77 }
78}
79
80// The fixtures above call now "fresh" and now-72h "stale" in their own
81// words; this asserts the RULE agrees, so a change to status.SnapshotPeriod
82// or to BoxStale's factor turns this test red rather than quietly turning
83// the two tests above into assertions about nothing.
84func TestFoldFixtureAgreesWithTheStalenessRule(t *testing.T) {
85 now := time.Date(2026, 8, 27, 12, 0, 0, 0, time.UTC)
86 if status.BoxStale(status.Box{GeneratedAt: now}, now, status.SnapshotPeriod) {
87 t.Error("a snapshot written this instant must read as fresh")
88 }
89 if !status.BoxStale(status.Box{GeneratedAt: now.Add(-72 * time.Hour)}, now, status.SnapshotPeriod) {
90 t.Error("a snapshot 72h old must read as stale — the retired-box fixture depends on it")
91 }
92}
93
94// A box publishing no EIP at all (readPublicIP never answered) contributes
95// nothing rather than an empty string, which would match no resolution and
96// only pollute the guidance strings the console builds from the same set.
97func TestEdgeIPsSkipsABoxWithNoAddress(t *testing.T) {
98 s := foldTestStore(t)
99 now := time.Date(2026, 8, 27, 12, 0, 0, 0, time.UTC)
100 putEdgesFile(t, s, "annie", `{"v":1,"box":"annie","eip":"","hosts":{}}`)
101 putFreshBox(t, s, "annie", now)
102 if got := EdgeIPs(context.Background(), s, nil, now); len(got) != 0 {
103 t.Fatalf("EdgeIPs = %v, want nothing — a blank EIP is not an address", got)
104 }
105}
106
107// ---- fixtures ---------------------------------------------------------------
108
109// foldTestStore is a real DirStore over a temp dir: the fold reads through
110// release.Store and status.LoadBoxes, and a hand-rolled fake would have to
111// re-implement both prefixes' listing semantics to say anything true.
112func foldTestStore(t *testing.T) release.Store {
113 t.Helper()
114 return release.NewDirStore(t.TempDir())
115}
116
117func putEdgesFile(t *testing.T, s release.Store, box, body string) {
118 t.Helper()
119 if _, err := s.Put(context.Background(), Key(box), []byte(body), ""); err != nil {
120 t.Fatalf("put edges file for %s: %v", box, err)
121 }
122}
123
124// putFreshBox writes one box snapshot through status.WriteBox — the SAME
125// writer statusTick uses — so the key derivation and the Label field the
126// fold joins on cannot drift from production. at is the snapshot's
127// GeneratedAt: the caller decides whether that reads as fresh or stale, and
128// TestFoldFixtureAgreesWithTheStalenessRule pins both readings.
129func putFreshBox(t *testing.T, s release.Store, label string, at time.Time) {
130 t.Helper()
131 if err := status.WriteBox(context.Background(), s, status.Box{Label: label, GeneratedAt: at}); err != nil {
132 t.Fatalf("write box snapshot for %s: %v", label, err)
133 }
134}
135
136// brokenStore is a store whose every read fails — the bucket-is-down case.
137type brokenStore struct{}
138
139var errBrokenStore = errors.New("store is down")
140
141func (brokenStore) Get(context.Context, string) ([]byte, string, error) {
142 return nil, "", errBrokenStore
143}
144func (brokenStore) Put(context.Context, string, []byte, string) (string, error) {
145 return "", errBrokenStore
146}
147func (brokenStore) List(context.Context, string) ([]string, error) { return nil, errBrokenStore }
148func (brokenStore) ListDirs(context.Context, string) ([]string, error) { return nil, errBrokenStore }
149func (brokenStore) Delete(context.Context, string) error { return errBrokenStore }
150
151// Edges files exist and NONE of them proved live: the answer degrades to the
152// env list with no error and nothing red, so the log line is the only thing
153// an operator can see. The likeliest cause is configuration rather than a
154// dead fleet — edges files are written through openDeployStore while box
155// snapshots go through openStatusStore, which honours CARLOS_STATUS_BUCKET
156// and CARLOS_STATUS_PREFIX, so the two halves of this join are the same
157// store by default but not by guarantee. Operational, not contractual: what
158// is pinned is that the line appears at all and names where to look.
159func TestEdgeIPsSaysSoWhenNothingJoined(t *testing.T) {
160 buf := captureLog(t)
161 s := foldTestStore(t)
162 now := time.Date(2026, 8, 27, 12, 0, 0, 0, time.UTC)
163 putEdgesFile(t, s, "annie", `{"v":1,"box":"annie","eip":"1.1.1.1","hosts":{}}`)
164 putEdgesFile(t, s, "betty", `{"v":1,"box":"betty","eip":"2.2.2.2","hosts":{}}`)
165 // No snapshots at all — exactly what a split-bucket deployment looks
166 // like from in here.
167 if got := EdgeIPs(context.Background(), s, []string{"9.9.9.9"}, now); !slices.Equal(got, []string{"9.9.9.9"}) {
168 t.Fatalf("EdgeIPs = %v, want env only", got)
169 }
170 line := buf.String()
171 if !strings.Contains(line, "no fresh box snapshot matched") {
172 t.Errorf("log = %q, want the total-miss tripwire", line)
173 }
174 if !strings.Contains(line, "CARLOS_STATUS_BUCKET") {
175 t.Errorf("log = %q, want it to name where to look", line)
176 }
177}
178
179// A deployment that has published no edges files yet is an ordinary state,
180// not a fault — and so is a partial miss, where one retired box drops out and
181// the rest join. Neither may cry wolf, or the line stops being read.
182func TestEdgeIPsStaysQuietWhenThereIsNothingToJoinOrSomethingDid(t *testing.T) {
183 now := time.Date(2026, 8, 27, 12, 0, 0, 0, time.UTC)
184
185 buf := captureLog(t)
186 empty := foldTestStore(t)
187 EdgeIPs(context.Background(), empty, []string{"9.9.9.9"}, now)
188 if strings.Contains(buf.String(), "no fresh box snapshot matched") {
189 t.Errorf("log = %q, want silence: no edges files is not a fault", buf.String())
190 }
191
192 buf2 := captureLog(t)
193 s := foldTestStore(t)
194 putEdgesFile(t, s, "annie", `{"v":1,"box":"annie","eip":"1.1.1.1","hosts":{}}`)
195 putEdgesFile(t, s, "retired", `{"v":1,"box":"retired","eip":"3.3.3.3","hosts":{}}`)
196 putFreshBox(t, s, "annie", now)
197 putFreshBox(t, s, "retired", now.Add(-72*time.Hour))
198 EdgeIPs(context.Background(), s, nil, now)
199 if strings.Contains(buf2.String(), "no fresh box snapshot matched") {
200 t.Errorf("log = %q, want silence: one box joined, which is the rule working", buf2.String())
201 }
202}
203
204// captureLog redirects the standard logger for one test — the same
205// save/restore-under-Cleanup shape internal/steering's tests use.
206func captureLog(t *testing.T) *bytes.Buffer {
207 t.Helper()
208 var buf bytes.Buffer
209 prev, flags := log.Writer(), log.Flags()
210 log.SetOutput(&buf)
211 log.SetFlags(0)
212 t.Cleanup(func() { log.SetOutput(prev); log.SetFlags(flags) })
213 return &buf
214}
215
216// DeploymentRoutes is the evidence a hostname claim is checked against
217// before it may materialize as an alias, so what it counts and what it
218// ignores is the whole rule: NON-alias home entries on LIVE boxes, and
219// nothing else.
220//
221// The alias exclusion is the load-bearing half. An alias IS a materialized
222// claim, so counting one would make every claim reserve its own hostname
223// against itself and no re-derivation would ever succeed again — the
224// self-poisoning failure this exclusion exists to prevent.
225func TestDeploymentRoutesCountsRealRoutesAndIgnoresAliases(t *testing.T) {
226 s := foldTestStore(t)
227 now := time.Date(2026, 8, 27, 12, 0, 0, 0, time.UTC)
228 putEdgesFile(t, s, "annie", `{"v":1,"box":"annie","eip":"1.1.1.1","hosts":{
229 "go.tito.io":{"role":"home"},
230 "tickets.example.com":{"role":"home","alias":true},
231 "app.bab.oncarlos.com":{"role":"home","platform":true}}}`)
232 putFreshBox(t, s, "annie", now)
233
234 got := DeploymentRoutes(context.Background(), s, now)
235 if !got["go.tito.io"] {
236 t.Errorf("routes = %v, want the operator-minted route counted", got)
237 }
238 if got["tickets.example.com"] {
239 t.Errorf("routes = %v, want the ALIAS entry ignored — counting it would make every claim reserve its own hostname against itself", got)
240 }
241 // A platform mint is still a real route: its hostname is genuinely
242 // served and is not a claim's own output.
243 if !got["app.bab.oncarlos.com"] {
244 t.Errorf("routes = %v, want a platform-minted route counted", got)
245 }
246}
247
248// A retired box's lingering edges file must not go on reserving hostnames
249// forever — the same staleness rule EdgeIPs uses, for the same reason, and
250// with sharper consequences: here a phantom entry is a REFUSAL, so a dead
251// box would permanently block a hostname nobody serves.
252func TestDeploymentRoutesDropsABoxWhoseSnapshotIsStale(t *testing.T) {
253 s := foldTestStore(t)
254 now := time.Date(2026, 8, 27, 12, 0, 0, 0, time.UTC)
255 putEdgesFile(t, s, "annie", `{"v":1,"box":"annie","eip":"1.1.1.1","hosts":{"live.example":{"role":"home"}}}`)
256 putEdgesFile(t, s, "retired", `{"v":1,"box":"retired","eip":"3.3.3.3","hosts":{"dead.example":{"role":"home"}}}`)
257 putFreshBox(t, s, "annie", now)
258 putFreshBox(t, s, "retired", now.Add(-72*time.Hour))
259
260 got := DeploymentRoutes(context.Background(), s, now)
261 if got["dead.example"] {
262 t.Errorf("routes = %v, want the retired box's hostnames dropped", got)
263 }
264 if !got["live.example"] {
265 t.Errorf("routes = %v, want the live box's hostname kept", got)
266 }
267}
268
269// Best-effort by contract: a store that cannot be read, and no store at all,
270// both answer nil — "nothing known", which callers treat as permissive.
271// Fail-OPEN is deliberate here even though this is a security guard: a
272// bucket that cannot be listed must not take every tenant's custom domain
273// down with it, and the per-box route check plus the fold's own route
274// preference are the layers underneath.
275func TestDeploymentRoutesIsNilWhenNothingCanBeRead(t *testing.T) {
276 now := time.Date(2026, 8, 27, 12, 0, 0, 0, time.UTC)
277 if got := DeploymentRoutes(context.Background(), nil, now); got != nil {
278 t.Errorf("nil store: routes = %v, want nil", got)
279 }
280 if got := DeploymentRoutes(context.Background(), brokenStore{}, now); got != nil {
281 t.Errorf("broken store: routes = %v, want nil", got)
282 }
283}
284
285// The total-miss tripwire, EdgeIPs' line and then some: there a total miss
286// degrades to the operator's configured edge list, which is merely the
287// pre-fold behaviour. Here it degrades to "no hostname is anyone's", which
288// silently DISARMS the guard — an empty map, no error, nothing red, and
289// every claim materializing exactly as it did before the rule existed. The
290// same three causes (split buckets, every box dark, box-name drift) and the
291// same invisibility, so the log line is all an operator gets.
292func TestDeploymentRoutesSaysSoWhenNothingJoined(t *testing.T) {
293 buf := captureLog(t)
294 s := foldTestStore(t)
295 now := time.Date(2026, 8, 27, 12, 0, 0, 0, time.UTC)
296 putEdgesFile(t, s, "annie", `{"v":1,"box":"annie","eip":"1.1.1.1","hosts":{"go.tito.io":{"role":"home"}}}`)
297 // No snapshots at all — what a split-bucket deployment looks like here.
298 if got := DeploymentRoutes(context.Background(), s, now); len(got) != 0 {
299 t.Fatalf("routes = %v, want empty", got)
300 }
301 line := buf.String()
302 if !strings.Contains(line, "no fresh box snapshot matched") {
303 t.Errorf("log = %q, want the total-miss tripwire", line)
304 }
305 if !strings.Contains(line, "CARLOS_STATUS_BUCKET") {
306 t.Errorf("log = %q, want it to name where to look", line)
307 }
308
309 // And silence when there is nothing to join: an empty deployment is an
310 // ordinary state, not a fault, or the line stops being read.
311 buf2 := captureLog(t)
312 DeploymentRoutes(context.Background(), foldTestStore(t), now)
313 if strings.Contains(buf2.String(), "no fresh box snapshot matched") {
314 t.Errorf("log = %q, want silence: no edges files is not a fault", buf2.String())
315 }
316}
317