carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package provision
2
3import (
4 "context"
5 "fmt"
6 "os"
7 "path/filepath"
8 "strings"
9 "testing"
10 "time"
11
12 "github.com/carlosframework/platform/internal/registry"
13 "github.com/carlosframework/platform/internal/release"
14)
15
16// --- fixtures -----------------------------------------------------------------
17
18// failStore is the read-failure seam these tests need and no other store
19// shape provides: a Store whose Get (and List) refuse a chosen prefix, so a
20// pass can be put in exactly the state the fail-safe exists for — a record
21// that IS there and could not be read — rather than the state that looks the
22// same from the outside and means the opposite (a record the bucket retired).
23type failStore struct {
24 release.Store
25 fail []string
26}
27
28func (s *failStore) failReadsFor(prefix string) { s.fail = append(s.fail, prefix) }
29
30func (s *failStore) Get(ctx context.Context, key string) ([]byte, string, error) {
31 for _, p := range s.fail {
32 if strings.HasPrefix(key, p) {
33 return nil, "", fmt.Errorf("failStore: reads disabled under %s", p)
34 }
35 }
36 return s.Store.Get(ctx, key)
37}
38
39func (s *failStore) List(ctx context.Context, prefix string) ([]string, error) {
40 for _, p := range s.fail {
41 if strings.HasPrefix(prefix, p) {
42 return nil, fmt.Errorf("failStore: reads disabled under %s", p)
43 }
44 }
45 return s.Store.List(ctx, prefix)
46}
47
48type sidecarFixture struct {
49 store *failStore
50 reg *registry.Registry
51 dir string
52 sqid string
53}
54
55// sidecarSetup writes a valid account manifest and hands back a fixture whose
56// data dir and registry are empty — the sidecar family's equivalent of
57// setup() above, minus the instances manifest (a sidecar app opts in through
58// its OWN manifest, sidecars.json).
59func sidecarSetup(t *testing.T) *sidecarFixture {
60 t.Helper()
61 s := &failStore{Store: dirStore(t)}
62 sqid := writeAccountManifest(t, s, acctID, "Eleven")
63 return &sidecarFixture{store: s, reg: regWith(t), dir: t.TempDir(), sqid: sqid}
64}
65
66func (f *sidecarFixture) host(name, app string) string { return SidecarHost(name, app, f.sqid) }
67
68func (f *sidecarFixture) putManifest(t *testing.T, app string, m SidecarManifest) {
69 t.Helper()
70 if err := PutSidecarManifest(ctx, f.store, f.sqid, app, m); err != nil {
71 t.Fatal(err)
72 }
73}
74
75func (f *sidecarFixture) putRecord(t *testing.T, rec SidecarRecord) {
76 t.Helper()
77 if rec.Account == "" {
78 rec.Account = f.sqid
79 }
80 if rec.Host == "" {
81 rec.Host = SidecarHost(rec.Name, rec.App, rec.Account)
82 }
83 if err := CreateSidecarRecord(ctx, f.store, rec); err != nil {
84 t.Fatal(err)
85 }
86}
87
88func (f *sidecarFixture) mustUpsert(t *testing.T, rt registry.Route) {
89 t.Helper()
90 if err := f.reg.Upsert(rt); err != nil {
91 t.Fatal(err)
92 }
93}
94
95func (f *sidecarFixture) reconcile(t *testing.T, cfg ReconcileConfig) {
96 t.Helper()
97 cfg.DataDir = f.dir
98 if err := ReconcileSidecars(ctx, f.store, f.reg, cfg); err != nil {
99 t.Fatalf("ReconcileSidecars: %v", err)
100 }
101}
102
103// noRoute asserts a host has no row. registry.Get answers a missing host with
104// (nil, nil), so a bare `err == nil` check would pass whether the row is
105// there or not.
106func noRoute(t *testing.T, reg *registry.Registry, host, msg string) {
107 t.Helper()
108 rt, err := reg.Get(host)
109 if err != nil {
110 t.Fatal(err)
111 }
112 if rt != nil {
113 t.Fatal(msg)
114 }
115}
116
117func hasRoute(t *testing.T, reg *registry.Registry, host, msg string) *registry.Route {
118 t.Helper()
119 rt, err := reg.Get(host)
120 if err != nil {
121 t.Fatal(err)
122 }
123 if rt == nil {
124 t.Fatal(msg)
125 }
126 return rt
127}
128
129// --- the removal-authority collision -------------------------------------------
130
131// TestInstanceReconcileNeverRemovesSidecarRows is the guard on the OTHER
132// pass. A sidecar row is Provisioned (that flag is what grants a pass removal
133// authority) and can never appear in the instance pass's record set, so
134// without a Kind guard the instance pass deletes every sidecar row on every
135// tick — minted by one pass, destroyed by the other, forever, with no error
136// anywhere and nothing in the logs that reads as a failure.
137func TestInstanceReconcileNeverRemovesSidecarRows(t *testing.T) {
138 f := sidecarSetup(t)
139 host := f.host("bot", "eleven")
140 f.mustUpsert(t, registry.Route{Host: host, Kind: "sidecar", Slug: "bot",
141 App: "eleven", Account: f.sqid, Provisioned: true, Status: "ready"})
142
143 // No instance records at all: the pass's removal phase runs in full.
144 if err := Reconcile(ctx, f.store, f.reg, ReconcileConfig{DataDir: f.dir}); err != nil {
145 t.Fatalf("Reconcile: %v", err)
146 }
147 hasRoute(t, f.reg, host, "the instance pass deleted a sidecar row — each pass's removal authority is scoped to its OWN kind, or the two passes destroy each other's work forever")
148}
149
150// TestSidecarReconcileRemovesOnlyItsOwnKind is the same guard from the other
151// side: this pass retires its own kind and must never reach an instance row,
152// which can never appear in ITS record set either.
153func TestSidecarReconcileRemovesOnlyItsOwnKind(t *testing.T) {
154 f := sidecarSetup(t)
155 gone := f.host("gone", "eleven")
156 f.mustUpsert(t, registry.Route{Host: gone, Kind: "sidecar", Slug: "gone",
157 App: "eleven", Account: f.sqid, Provisioned: true})
158 f.mustUpsert(t, registry.Route{Host: "app.example.com", Kind: "instance",
159 App: "eleven", Account: f.sqid, Provisioned: true})
160 // A sidecar-kind row nothing provisioned: Provisioned is the flag that
161 // GRANTS removal authority, so a row without it is not this pass's to
162 // retire however well it matches on every other column.
163 hand := f.host("hand", "eleven")
164 f.mustUpsert(t, registry.Route{Host: hand, Kind: "sidecar", Slug: "hand",
165 App: "eleven", Account: f.sqid})
166
167 f.reconcile(t, ReconcileConfig{})
168
169 noRoute(t, f.reg, gone, "a provisioned sidecar with no record must be removed")
170 hasRoute(t, f.reg, "app.example.com", "the sidecar pass must never touch an instance row")
171 hasRoute(t, f.reg, hand, "a sidecar row that was never provisioned is not this pass's to remove")
172}
173
174// --- mint / converge / remove --------------------------------------------------
175
176func TestSidecarReconcileMints(t *testing.T) {
177 f := sidecarSetup(t)
178 f.putManifest(t, "eleven", SidecarManifest{
179 Schema: 1, App: "eleven", Args: []string{"sidecar", "run"}, Channel: "stable"})
180 f.putRecord(t, SidecarRecord{Schema: 1, Name: "bot", App: "eleven", Channel: "stable"})
181
182 f.reconcile(t, ReconcileConfig{})
183
184 rt := hasRoute(t, f.reg, f.host("bot", "eleven"), "no sidecar route minted")
185 if rt.Kind != "sidecar" {
186 t.Fatalf("Kind = %q", rt.Kind)
187 }
188 if rt.Socket != "" || rt.Addr != "" {
189 t.Fatalf("a sidecar row must carry NO socket and NO addr, got socket=%q addr=%q", rt.Socket, rt.Addr)
190 }
191 if rt.Slug != "bot" || rt.App != "eleven" || rt.Account != f.sqid || rt.Channel != "stable" {
192 t.Fatalf("route identity = %+v", rt)
193 }
194 if rt.DBPath != DefaultDBPath(f.dir, rt.Host) || !rt.Provisioned || rt.Args != `["sidecar","run"]` {
195 t.Fatalf("route = %+v", rt)
196 }
197 if !rt.Hibernate {
198 t.Fatal("Hibernate defaults true (HibernateOrDefault), and the row is what the activator reads")
199 }
200 if rt.IdleSeconds != 300 || rt.MaxRunSeconds != 3600 {
201 t.Fatalf("ceilings = %d/%d, want the manifest defaults 300/3600 materialized onto the row — the sweeper must never need a bucket read",
202 rt.IdleSeconds, rt.MaxRunSeconds)
203 }
204}
205
206func TestSidecarReconcileConvergesArgsFromTheManifest(t *testing.T) {
207 f := sidecarSetup(t)
208 f.putManifest(t, "eleven", SidecarManifest{Schema: 1, App: "eleven", Args: []string{"sidecar", "run"}})
209 f.putRecord(t, SidecarRecord{Schema: 1, Name: "bot", App: "eleven", Channel: "stable"})
210 f.reconcile(t, ReconcileConfig{})
211
212 f.putManifest(t, "eleven", SidecarManifest{Schema: 1, App: "eleven", Args: []string{"worker", "run"}})
213 f.reconcile(t, ReconcileConfig{})
214
215 rt := hasRoute(t, f.reg, f.host("bot", "eleven"), "route gone")
216 if rt.Args != `["worker","run"]` {
217 t.Fatalf("Args = %q — truth lives in the bucket and is re-stamped every pass, exactly as seed is", rt.Args)
218 }
219}
220
221func TestSidecarReconcileConvergesTheCeilings(t *testing.T) {
222 f := sidecarSetup(t)
223 f.putManifest(t, "eleven", SidecarManifest{Schema: 1, App: "eleven",
224 Args: []string{"sidecar", "run"}, IdleSeconds: 300, MaxRunSeconds: 3600})
225 f.putRecord(t, SidecarRecord{Schema: 1, Name: "bot", App: "eleven", Channel: "stable"})
226 f.reconcile(t, ReconcileConfig{})
227
228 f.putManifest(t, "eleven", SidecarManifest{Schema: 1, App: "eleven",
229 Args: []string{"sidecar", "run"}, IdleSeconds: 60, MaxRunSeconds: 120})
230 f.reconcile(t, ReconcileConfig{})
231
232 rt := hasRoute(t, f.reg, f.host("bot", "eleven"), "route gone")
233 if rt.IdleSeconds != 60 || rt.MaxRunSeconds != 120 {
234 t.Fatalf("ceilings = %d/%d, want 60/120 — the sweeper reads the row, so the row must follow the manifest", rt.IdleSeconds, rt.MaxRunSeconds)
235 }
236}
237
238// TestSidecarReconcileConvergesChannelEnvironmentAndHibernate pins the rest
239// of the owned columns: the record owns channel and environment (the #130
240// discipline), and the manifest owns hibernation.
241func TestSidecarReconcileConvergesChannelEnvironmentAndHibernate(t *testing.T) {
242 f := sidecarSetup(t)
243 off := false
244 f.putManifest(t, "eleven", SidecarManifest{Schema: 1, App: "eleven",
245 Args: []string{"sidecar", "run"}, Hibernate: &off})
246 f.putRecord(t, SidecarRecord{Schema: 1, Name: "bot", App: "eleven",
247 Channel: "canary/eleven", Environment: "relay"})
248 f.reconcile(t, ReconcileConfig{})
249
250 rt := hasRoute(t, f.reg, f.host("bot", "eleven"), "route gone")
251 if rt.Channel != "canary/eleven" || rt.Environment != "relay" || rt.Hibernate {
252 t.Fatalf("minted route = channel %q env %q hibernate %v", rt.Channel, rt.Environment, rt.Hibernate)
253 }
254
255 // Drift the row the way `carlos route` would, then converge it back.
256 if err := f.reg.SetChannel(rt.Host, "stable"); err != nil {
257 t.Fatal(err)
258 }
259 if err := f.reg.SetEnvironment(rt.Host, "other"); err != nil {
260 t.Fatal(err)
261 }
262 if err := f.reg.SetHibernate(rt.Host, true); err != nil {
263 t.Fatal(err)
264 }
265 f.reconcile(t, ReconcileConfig{})
266
267 rt = hasRoute(t, f.reg, f.host("bot", "eleven"), "route gone")
268 if rt.Channel != "canary/eleven" || rt.Environment != "relay" || rt.Hibernate {
269 t.Fatalf("after converge = channel %q env %q hibernate %v — the record and manifest own these columns",
270 rt.Channel, rt.Environment, rt.Hibernate)
271 }
272}
273
274// --- the fail-safe --------------------------------------------------------------
275
276// TestSidecarReconcileSkipsRemovalWhenAReadFailed pins the fail-safe: a
277// transient read failure must never mass-retire a tenant's fleet. Minting
278// still proceeds, because minting is additive.
279func TestSidecarReconcileSkipsRemovalWhenAReadFailed(t *testing.T) {
280 f := sidecarSetup(t)
281 f.putManifest(t, "eleven", SidecarManifest{Schema: 1, App: "eleven", Args: []string{"sidecar", "run"}})
282 f.putRecord(t, SidecarRecord{Schema: 1, Name: "bot", App: "eleven", Channel: "stable"})
283 keep := f.host("keep", "eleven")
284 f.mustUpsert(t, registry.Route{Host: keep, Kind: "sidecar", Slug: "keep",
285 App: "eleven", Account: f.sqid, Provisioned: true})
286
287 // The record IS there; this pass simply cannot read it.
288 f.store.failReadsFor(SidecarsPrefix(f.sqid, "eleven"))
289 f.reconcile(t, ReconcileConfig{})
290
291 hasRoute(t, f.reg, keep, "a read failure skipped nothing — one bad list must not retire a fleet")
292}
293
294// TestSidecarReconcileProtectsAMalformedRecord is the narrower guard: only
295// the host whose body could not be trusted is held back, and it is held back
296// rather than read as "the bucket retired it" — a non-additive record
297// encoding change reaching an older binary would otherwise delete every
298// sidecar in scope in one tick.
299func TestSidecarReconcileProtectsAMalformedRecord(t *testing.T) {
300 f := sidecarSetup(t)
301 host := f.host("bot", "eleven")
302 f.mustUpsert(t, registry.Route{Host: host, Kind: "sidecar", Slug: "bot",
303 App: "eleven", Account: f.sqid, Provisioned: true})
304 if _, err := f.store.Put(ctx, SidecarRecordKey(f.sqid, "eleven", "bot"), []byte("{not json"), ""); err != nil {
305 t.Fatal(err)
306 }
307
308 f.reconcile(t, ReconcileConfig{})
309
310 hasRoute(t, f.reg, host, "a record that cannot be PARSED is not a record that was retired")
311}
312
313// TestSidecarReconcileProtectsAChannellessRecord: a route with no channel
314// resolves no release, so it has no build to exec — the worker boot pass
315// would try to run it against a symlink that does not exist. The console
316// stamps a channel at create, so a record without one is malformed, and
317// malformed is exactly what protect is for: not minted, and not read as "the
318// bucket retired it" either.
319func TestSidecarReconcileProtectsAChannellessRecord(t *testing.T) {
320 f := sidecarSetup(t)
321 host := f.host("bot", "eleven")
322 f.putManifest(t, "eleven", SidecarManifest{Schema: 1, App: "eleven",
323 Args: []string{"sidecar", "run"}, Channel: "stable"})
324 f.putRecord(t, SidecarRecord{Schema: 1, Name: "bot", App: "eleven"}) // no channel, no row yet
325 // A channel-less record whose row already exists: protected, not retired.
326 held := f.host("held", "eleven")
327 f.putRecord(t, SidecarRecord{Schema: 1, Name: "held", App: "eleven"})
328 f.mustUpsert(t, registry.Route{Host: held, Kind: "sidecar", Slug: "held",
329 App: "eleven", Account: f.sqid, Channel: "stable", Provisioned: true})
330 // A row with no record at all, to prove the removal phase still ran.
331 old := f.host("old", "eleven")
332 f.mustUpsert(t, registry.Route{Host: old, Kind: "sidecar", Slug: "old",
333 App: "eleven", Account: f.sqid, Provisioned: true})
334
335 f.reconcile(t, ReconcileConfig{})
336
337 noRoute(t, f.reg, host, "a channel-less record must not mint a row that can never resolve a release — the manifest's channel is the console door's default at CREATE time, not a runtime rescue")
338 hasRoute(t, f.reg, held, "a record that cannot be USED is not a record that was retired")
339 noRoute(t, f.reg, old, "protect is narrow: an unusable record holds back ITS OWN host only, and the pass's removal phase still runs")
340}
341
342// --- decline-but-keep -------------------------------------------------------------
343
344// TestSidecarReconcileKeepsRowsWhenTheManifestIsAbsent pins the invariant that
345// stands between "an app's sidecars.json was deleted" and "every sidecar row
346// of that app is retired". An absent manifest is a confirmed answer, not a
347// blip, so it does NOT arm the pass-wide fail-safe — which is exactly why the
348// record must stay in the record set on its own account. Declining to mint is
349// the whole of the response.
350//
351// The hazard is one line away: manifestFor is consulted in the record scan
352// (placement needs it), so a natural-looking `if !mKnown { continue }` there
353// would convert a missing manifest into a fleet evacuation.
354func TestSidecarReconcileKeepsRowsWhenTheManifestIsAbsent(t *testing.T) {
355 f := sidecarSetup(t)
356 host := f.host("bot", "eleven")
357 // No sidecars.json for this app at all — the feature is not enabled.
358 f.putRecord(t, SidecarRecord{Schema: 1, Name: "bot", App: "eleven", Channel: "stable"})
359 f.putRecord(t, SidecarRecord{Schema: 1, Name: "new", App: "eleven", Channel: "stable"})
360 f.mustUpsert(t, registry.Route{Host: host, Kind: "sidecar", Slug: "bot", App: "eleven",
361 Account: f.sqid, Channel: "stable", Provisioned: true, Args: `["sidecar","run"]`})
362
363 f.reconcile(t, ReconcileConfig{})
364
365 rt := hasRoute(t, f.reg, host, "an absent sidecars manifest must not RETIRE a running sidecar — it is the app's opt-in door, not a statement that this row is gone")
366 if rt.Args != `["sidecar","run"]` {
367 t.Fatalf("Args = %q — with no manifest there is nothing authoritative to converge from, so the row must be left exactly as it was", rt.Args)
368 }
369 noRoute(t, f.reg, f.host("new", "eleven"), "with no manifest there is no argv to run, so nothing may be minted either")
370}
371
372// TestSidecarReconcileKeepsRowsOfATrashedApp: a trashed app mints nothing
373// here, and its rows are not this pass's to retire either. domains.Reconcile
374// drops every route of a trashed app on its own authority (with its own
375// production-flag guard) — this pass merely declines to create, or the two
376// loops would fight forever, one re-raising what the other removes.
377func TestSidecarReconcileKeepsRowsOfATrashedApp(t *testing.T) {
378 f := sidecarSetup(t)
379 host := f.host("bot", "eleven")
380 f.putManifest(t, "eleven", SidecarManifest{Schema: 1, App: "eleven", Args: []string{"sidecar", "run"}})
381 f.putRecord(t, SidecarRecord{Schema: 1, Name: "bot", App: "eleven", Channel: "stable"})
382 f.putRecord(t, SidecarRecord{Schema: 1, Name: "new", App: "eleven", Channel: "stable"})
383 f.mustUpsert(t, registry.Route{Host: host, Kind: "sidecar", Slug: "bot", App: "eleven",
384 Account: f.sqid, Channel: "stable", Provisioned: true})
385 if err := release.TrashApp(ctx, f.store, f.sqid, "eleven", "someone@example.com", time.Now()); err != nil {
386 t.Fatal(err)
387 }
388
389 f.reconcile(t, ReconcileConfig{})
390
391 hasRoute(t, f.reg, host, "dropping a trashed app's routes is domains.Reconcile's call, with its production-flag guard — this pass only declines to create")
392 noRoute(t, f.reg, f.host("new", "eleven"), "a trashed app provisions nothing")
393}
394
395// --- placement -------------------------------------------------------------------
396
397// TestSidecarReconcilePlacementGate pins §8's second half: a no-inbound
398// workload lands only on a box that declares it accepts no inbound work. Both
399// spellings are honoured — the record's own placement and the app manifest's.
400func TestSidecarReconcilePlacementGate(t *testing.T) {
401 f := sidecarSetup(t)
402 f.putManifest(t, "eleven", SidecarManifest{Schema: 1, App: "eleven", Args: []string{"sidecar", "run"}})
403 f.putRecord(t, SidecarRecord{Schema: 1, Name: "bot", App: "eleven", Channel: "stable", Placement: PlacementNoInbound})
404 // A second app whose MANIFEST asks for the placement, record silent.
405 f.putManifest(t, "relay", SidecarManifest{Schema: 1, App: "relay",
406 Args: []string{"relay"}, Placement: PlacementNoInbound})
407 f.putRecord(t, SidecarRecord{Schema: 1, Name: "hub", App: "relay", Channel: "stable"})
408
409 f.reconcile(t, ReconcileConfig{}) // an ordinary box: accepts inbound work
410 noRoute(t, f.reg, f.host("bot", "eleven"), "a no-inbound workload must not land on a box that accepts inbound work")
411 noRoute(t, f.reg, f.host("hub", "relay"), "a manifest-stated no-inbound placement must gate exactly as a record-stated one does")
412
413 f.reconcile(t, ReconcileConfig{Inbound: InboundNone})
414 hasRoute(t, f.reg, f.host("bot", "eleven"), "it must land on a box that declares CARLOS_INBOUND=none")
415 hasRoute(t, f.reg, f.host("hub", "relay"), "it must land on a box that declares CARLOS_INBOUND=none")
416}
417
418// TestSidecarReconcilePlacementIsMostRestrictive: a record may NARROW the
419// app's placement but never widen it. "any" on a record under a no-inbound
420// manifest is the widening direction — the console door refuses to write it
421// at all, and this pass must refuse to honour one that reached the bucket by
422// any other route, or app-wide policy is only a suggestion.
423func TestSidecarReconcilePlacementIsMostRestrictive(t *testing.T) {
424 f := sidecarSetup(t)
425 // App-wide: everything this app runs sits somewhere with no inbound path.
426 f.putManifest(t, "eleven", SidecarManifest{Schema: 1, App: "eleven",
427 Args: []string{"sidecar", "run"}, Placement: PlacementNoInbound})
428 f.putRecord(t, SidecarRecord{Schema: 1, Name: "bot", App: "eleven", Channel: "stable", Placement: PlacementAny})
429
430 f.reconcile(t, ReconcileConfig{}) // an ordinary box: accepts inbound work
431 noRoute(t, f.reg, f.host("bot", "eleven"),
432 `a record's "any" must not override the app's no-inbound placement — that is the widening direction the console door already refuses`)
433
434 f.reconcile(t, ReconcileConfig{Inbound: InboundNone})
435 hasRoute(t, f.reg, f.host("bot", "eleven"), "it still lands where the app's own policy says it may")
436}
437
438// The narrowing direction is untouched: one bot that must run with no inbound
439// path, under an app that states nothing, is honoured.
440func TestSidecarReconcileRecordMayNarrowAnUnplacedApp(t *testing.T) {
441 f := sidecarSetup(t)
442 f.putManifest(t, "eleven", SidecarManifest{Schema: 1, App: "eleven", Args: []string{"sidecar", "run"}})
443 f.putRecord(t, SidecarRecord{Schema: 1, Name: "bot", App: "eleven", Channel: "stable", Placement: PlacementNoInbound})
444
445 f.reconcile(t, ReconcileConfig{})
446 noRoute(t, f.reg, f.host("bot", "eleven"), "a record asking for no-inbound must not land on an inbound box")
447 f.reconcile(t, ReconcileConfig{Inbound: InboundNone})
448 hasRoute(t, f.reg, f.host("bot", "eleven"), "a record may narrow an app that states no placement of its own")
449}
450
451// TestSidecarReconcilePlacementChangeEvacuatesTheRow: placement is a SCOPE
452// axis, beside region and pool, so flipping a manifest to no-inbound takes the
453// row off a box that accepts inbound work — the same way repointing a
454// record's region moves an instance. Decline-but-keep would break the only
455// guarantee the axis exists to give: a member who asks that a bot only ever
456// run where there is no inbound path would find it still running on an
457// inbound box.
458func TestSidecarReconcilePlacementChangeEvacuatesTheRow(t *testing.T) {
459 f := sidecarSetup(t)
460 host := f.host("bot", "eleven")
461 f.putManifest(t, "eleven", SidecarManifest{Schema: 1, App: "eleven", Args: []string{"sidecar", "run"}})
462 f.putRecord(t, SidecarRecord{Schema: 1, Name: "bot", App: "eleven", Channel: "stable"})
463 f.reconcile(t, ReconcileConfig{})
464 hasRoute(t, f.reg, host, "an unplaced record mints here")
465
466 // The app's owner now requires no inbound path anywhere it runs.
467 f.putManifest(t, "eleven", SidecarManifest{Schema: 1, App: "eleven",
468 Args: []string{"sidecar", "run"}, Placement: PlacementNoInbound})
469
470 f.reconcile(t, ReconcileConfig{}) // this box accepts inbound work
471 noRoute(t, f.reg, host, "flipping placement to no-inbound must EVACUATE the row from an inbound box, not leave the bot running there")
472
473 f.reconcile(t, ReconcileConfig{Inbound: InboundNone}) // and land where it belongs
474 hasRoute(t, f.reg, host, "the same record must mint on a box that declares CARLOS_INBOUND=none")
475}
476
477// TestSidecarReconcilePlacementEvacuationObeysTheFailSafe: evacuation is a
478// removal, so it is governed by the pass-wide fail-safe like every other
479// removal. A read that failed anywhere this pass means this box cannot tell a
480// placement change from a blip, and a transient blip must never evacuate a
481// tenant's fleet.
482func TestSidecarReconcilePlacementEvacuationObeysTheFailSafe(t *testing.T) {
483 f := sidecarSetup(t)
484 host := f.host("bot", "eleven")
485 f.putManifest(t, "eleven", SidecarManifest{Schema: 1, App: "eleven",
486 Args: []string{"sidecar", "run"}, Placement: PlacementNoInbound})
487 f.putRecord(t, SidecarRecord{Schema: 1, Name: "bot", App: "eleven", Channel: "stable"})
488 f.mustUpsert(t, registry.Route{Host: host, Kind: "sidecar", Slug: "bot",
489 App: "eleven", Account: f.sqid, Provisioned: true})
490 // An UNRELATED app's records cannot be read this pass.
491 f.putManifest(t, "relay", SidecarManifest{Schema: 1, App: "relay", Args: []string{"relay"}})
492 f.putRecord(t, SidecarRecord{Schema: 1, Name: "hub", App: "relay", Channel: "stable"})
493 f.store.failReadsFor(SidecarsPrefix(f.sqid, "relay"))
494
495 f.reconcile(t, ReconcileConfig{}) // placement does not match, but a read failed
496
497 hasRoute(t, f.reg, host, "a pass that could not read everything must evacuate nothing — the fail-safe governs placement removals too")
498}
499
500// --- the co-spawn conflict ---------------------------------------------------------
501
502// TestSidecarReconcileRefusesHostWithAStagedCoSpawnFile: the file-staged
503// co-spawn is legacy but LIVE (Eleven runs on it today). One host, one
504// mechanism.
505func TestSidecarReconcileRefusesHostWithAStagedCoSpawnFile(t *testing.T) {
506 f := sidecarSetup(t)
507 host := f.host("bot", "eleven")
508 if err := os.WriteFile(filepath.Join(f.dir, host+"-sidecar.env"), []byte("X=1\n"), 0o600); err != nil {
509 t.Fatal(err)
510 }
511 f.putManifest(t, "eleven", SidecarManifest{Schema: 1, App: "eleven", Args: []string{"sidecar", "run"}})
512 f.putRecord(t, SidecarRecord{Schema: 1, Name: "bot", App: "eleven", Channel: "stable"})
513
514 f.reconcile(t, ReconcileConfig{})
515
516 noRoute(t, f.reg, host, "a host with a staged legacy co-spawn file must not also get a native sidecar row — one host, one mechanism")
517}
518
519// --- scope ---------------------------------------------------------------------------
520
521// TestSidecarReconcileSkipsAnotherBoxesScope: region and pool scope the
522// sidecar pass exactly as they scope the instance one — a record for another
523// agent's jurisdiction is neither minted nor (for the row) removed here.
524func TestSidecarReconcileSkipsAnotherBoxesScope(t *testing.T) {
525 f := sidecarSetup(t)
526 f.putManifest(t, "eleven", SidecarManifest{Schema: 1, App: "eleven", Args: []string{"sidecar", "run"}})
527 f.putRecord(t, SidecarRecord{Schema: 1, Name: "bot", App: "eleven", Channel: "stable", Region: "ie", Pool: "eleven"})
528 // A foreign box's own provisioned sidecar row, visible in this registry.
529 foreign := f.host("far", "eleven")
530 f.mustUpsert(t, registry.Route{Host: foreign, Kind: "sidecar", Slug: "far", App: "eleven",
531 Account: f.sqid, Region: "de", Pool: "other", Provisioned: true})
532
533 f.reconcile(t, ReconcileConfig{}) // this box: no region, no pools
534
535 noRoute(t, f.reg, f.host("bot", "eleven"), "a record for another region/pool is not this pass's business")
536 hasRoute(t, f.reg, foreign, "a foreign box's provisioned route is not this pass's to retire")
537
538 f.reconcile(t, ReconcileConfig{Region: "ie", Pools: []string{"eleven"}})
539 hasRoute(t, f.reg, f.host("bot", "eleven"), "the in-scope box must mint it")
540}
541
542// TestSidecarReconcileDryRunWritesNothing mirrors the instance pass's §4.3
543// mitigation: a pass that reports is a pass that writes nothing at all.
544func TestSidecarReconcileDryRunWritesNothing(t *testing.T) {
545 f := sidecarSetup(t)
546 f.putManifest(t, "eleven", SidecarManifest{Schema: 1, App: "eleven", Args: []string{"sidecar", "run"}})
547 f.putRecord(t, SidecarRecord{Schema: 1, Name: "bot", App: "eleven", Channel: "stable"})
548 gone := f.host("gone", "eleven")
549 f.mustUpsert(t, registry.Route{Host: gone, Kind: "sidecar", Slug: "gone",
550 App: "eleven", Account: f.sqid, Provisioned: true})
551
552 f.reconcile(t, ReconcileConfig{DryRun: true})
553
554 noRoute(t, f.reg, f.host("bot", "eleven"), "dry-run minted a route")
555 hasRoute(t, f.reg, gone, "dry-run removed a route")
556}
557
558// TestSidecarReconcileLeavesAForeignRowAlone: a row this pass does not own at
559// a host a record names is left exactly as alone as before. There is no
560// adoption path for sidecars — the suffix is reserved and refused at every
561// door, so nothing but this pass can have minted one — and adoption is an
562// authority transfer, never a shape coincidence.
563func TestSidecarReconcileLeavesAForeignRowAlone(t *testing.T) {
564 f := sidecarSetup(t)
565 host := f.host("bot", "eleven")
566 f.putManifest(t, "eleven", SidecarManifest{Schema: 1, App: "eleven", Args: []string{"sidecar", "run"}})
567 f.putRecord(t, SidecarRecord{Schema: 1, Name: "bot", App: "eleven", Channel: "stable"})
568 f.mustUpsert(t, registry.Route{Host: host, Kind: "sidecar", Slug: "bot",
569 App: "eleven", Account: f.sqid, Args: `["hand","written"]`})
570
571 f.reconcile(t, ReconcileConfig{})
572
573 rt := hasRoute(t, f.reg, host, "route gone")
574 if rt.Provisioned {
575 t.Fatal("an un-provisioned row must not be adopted by this pass — adoption grants removal authority")
576 }
577 if rt.Args != `["hand","written"]` {
578 t.Fatalf("Args = %q — a row this pass does not own must be left alone", rt.Args)
579 }
580}
581