carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package appconfig
2
3import (
4 "bytes"
5 "context"
6 "crypto/ecdh"
7 "encoding/json"
8 "errors"
9 "os"
10 "path/filepath"
11 "strings"
12 "sync"
13 "testing"
14
15 "github.com/carlosframework/platform/internal/registry"
16 "github.com/carlosframework/platform/internal/release"
17)
18
19// putInstanceRaw writes an instance-layer object without going through the
20// package's own writer, which does not exist until task 2. Tests in this
21// file that only READ the layer use it, so task 1 can be verified on its
22// own.
23func putInstanceRaw(t *testing.T, ctx context.Context, s release.Store, acct, app, host string, m *Manifest) string {
24 t.Helper()
25 body, err := json.MarshalIndent(m, "", " ")
26 if err != nil {
27 t.Fatalf("marshal instance manifest: %v", err)
28 }
29 etag, err := s.Put(ctx, InstanceConfigKey(acct, app, host), body, "")
30 if err != nil {
31 t.Fatalf("put instance manifest: %v", err)
32 }
33 return etag
34}
35
36// The layer sits under the app's own prefix, beside config.json and
37// environments/, so release.PurgeApp's prefix sweep reaches it and a purged
38// app leaves no instance overlay behind holding sealed secrets.
39func TestInstanceConfigKeysSitUnderTheAppsOwnPrefix(t *testing.T) {
40 if got, want := InstancesConfigPrefix("bab", "titogo"), "apps/bab/titogo/instances-config/"; got != want {
41 t.Fatalf("InstancesConfigPrefix = %q, want %q", got, want)
42 }
43 if got, want := InstanceConfigKey("bab", "titogo", "book.customer.example.com"),
44 "apps/bab/titogo/instances-config/book.customer.example.com.json"; got != want {
45 t.Fatalf("InstanceConfigKey = %q, want %q", got, want)
46 }
47}
48
49// A host becomes a bucket key segment here, so the one gate every caller
50// passes through is where a separator, a traversal or an empty name is
51// refused. The shape is a bare lowercase hostname, the same thing a route
52// row's Host column holds and the same thing sync.go's own path guard
53// (rt.Host != filepath.Base(rt.Host)) already insists on before it derives
54// a file name from one.
55func TestValidInstanceHostRefusesAnythingThatIsNotABareHost(t *testing.T) {
56 for _, bad := range []string{
57 "", ".", "..", "a/b", "../etc", "Book.example.com", "book..example.com",
58 "-lead.example.com", "trail-.example.com", "book.example.com/",
59 strings.Repeat("a", 254),
60 } {
61 if ValidInstanceHost(bad) {
62 t.Errorf("ValidInstanceHost(%q) = true", bad)
63 }
64 }
65 for _, good := range []string{"book.customer.example.com", "inst1", "a-b.c-d.example.com", "127-0-0-1.example.com"} {
66 if !ValidInstanceHost(good) {
67 t.Errorf("ValidInstanceHost(%q) = false", good)
68 }
69 }
70}
71
72// The reader answers three ways, and the caller needs all three apart: a
73// manifest and the etag it was read at, "no object" without an error (an
74// instance with no overlay is the normal case, not a fault), and an error
75// for anything else. The etag is what the delivery ack carries (design §6.5),
76// so a read that dropped it would leave the agent unable to say which
77// version of the layer it rendered.
78func TestLoadInstanceConfigReadsTheLayerAndItsETag(t *testing.T) {
79 ctx := context.Background()
80 s := release.NewDirStore(t.TempDir())
81
82 m, etag, found, err := LoadInstanceConfig(ctx, s, "bab", "titogo", "book.customer.example.com")
83 if err != nil || found || m != nil || etag != "" {
84 t.Fatalf("absent overlay = %+v %q %v %v, want (nil, \"\", false, nil)", m, etag, found, err)
85 }
86
87 want := putInstanceRaw(t, ctx, s, "bab", "titogo", "book.customer.example.com",
88 &Manifest{Schema: 1, App: "titogo", Vars: map[string]Var{"TENANT_SMTP_USER": {Kind: "env", Value: "book"}}})
89 m, etag, found, err = LoadInstanceConfig(ctx, s, "bab", "titogo", "book.customer.example.com")
90 if err != nil || !found || m == nil {
91 t.Fatalf("present overlay = %+v %v %v", m, found, err)
92 }
93 if etag == "" || etag != want {
94 t.Fatalf("etag = %q, want the object's own %q", etag, want)
95 }
96 if m.Vars["TENANT_SMTP_USER"].Value != "book" {
97 t.Fatalf("overlay lost its var: %+v", m.Vars)
98 }
99
100 // The same newer-schema refusal every other bundle read makes: acting on
101 // a manifest whose unknown fields json.Unmarshal has already dropped
102 // would silently discard whatever they meant.
103 putInstanceRaw(t, ctx, s, "bab", "titogo", "newer.example.com", &Manifest{Schema: 2, App: "titogo"})
104 if _, _, _, err := LoadInstanceConfig(ctx, s, "bab", "titogo", "newer.example.com"); err == nil {
105 t.Fatal("a schema-2 overlay was accepted, want a refusal naming the upgrade")
106 }
107
108 // A host that is not a bare hostname never reaches the bucket at all.
109 if _, _, _, err := LoadInstanceConfig(ctx, s, "bab", "titogo", "../config"); err == nil {
110 t.Fatal("LoadInstanceConfig accepted a traversal host")
111 }
112}
113
114// The write door. It returns the etag it wrote at, which is the whole
115// mechanism behind the delivery check (design §6.5): the caller keeps that
116// string and compares it with what an agent's ack says it rendered.
117func TestApplyInstanceWritesTheLayerAndReturnsItsETag(t *testing.T) {
118 ctx := context.Background()
119 s := release.NewDirStore(t.TempDir())
120
121 removed, etag, err := ApplyInstance(ctx, s, "bab", "titogo", "book.customer.example.com",
122 map[string]Var{"TENANT_SMTP_USER": {Kind: "env", Value: "book"}}, nil)
123 if err != nil {
124 t.Fatalf("ApplyInstance: %v", err)
125 }
126 if removed != 0 {
127 t.Fatalf("removed = %d on a create, want 0", removed)
128 }
129 if etag == "" {
130 t.Fatal("ApplyInstance returned an empty etag, so nothing can check delivery against it")
131 }
132 m, readETag, found, err := LoadInstanceConfig(ctx, s, "bab", "titogo", "book.customer.example.com")
133 if err != nil || !found {
134 t.Fatalf("LoadInstanceConfig after the write = %+v %v %v", m, found, err)
135 }
136 if readETag != etag {
137 t.Fatalf("the reader saw etag %q, the writer returned %q — the delivery check compares these two", readETag, etag)
138 }
139 if m.Schema != 1 || m.App != "titogo" || m.Vars["TENANT_SMTP_USER"].Value != "book" {
140 t.Fatalf("the written overlay is not a schema-1 manifest of this app: %+v", m)
141 }
142
143 // A second write moves the etag, which is what makes a stale ack
144 // detectable at all.
145 _, etag2, err := ApplyInstance(ctx, s, "bab", "titogo", "book.customer.example.com",
146 map[string]Var{"TENANT_SMTP_USER": {Kind: "env", Value: "book2"}}, nil)
147 if err != nil {
148 t.Fatalf("ApplyInstance (second): %v", err)
149 }
150 if etag2 == etag {
151 t.Fatalf("etag did not move on a changed write: %q", etag2)
152 }
153
154 // Unset reports how many of the keys were actually there, the same
155 // contract every other bundle's unset has.
156 removed, _, err = ApplyInstance(ctx, s, "bab", "titogo", "book.customer.example.com", nil,
157 []string{"TENANT_SMTP_USER", "NEVER_SET"})
158 if err != nil || removed != 1 {
159 t.Fatalf("unset = %d %v, want 1 and no error", removed, err)
160 }
161
162 // The var-name and var-value rules are the bundle's, not this layer's:
163 // applyAt refuses before anything reaches the bucket.
164 if _, _, err := ApplyInstance(ctx, s, "bab", "titogo", "book.customer.example.com",
165 map[string]Var{"not a name": {Kind: "env", Value: "x"}}, nil); err == nil {
166 t.Fatal("an invalid var name was accepted into the instance layer")
167 }
168 if _, _, err := ApplyInstance(ctx, s, "bab", "titogo", "../config", nil, nil); err == nil {
169 t.Fatal("ApplyInstance accepted a traversal host")
170 }
171}
172
173// An unset against an overlay that does not exist is refused rather than
174// minting an empty one, the same refusal applyAt already makes for the
175// other two bundles and for the same reason: an empty manifest that EXISTS
176// means "configured, then fully unset" by this package's contract, which is
177// a state the sweeps converge a file to.
178func TestApplyInstanceRefusesUnsetOnlyAgainstAMissingOverlay(t *testing.T) {
179 ctx := context.Background()
180 s := release.NewDirStore(t.TempDir())
181 _, _, err := ApplyInstance(ctx, s, "bab", "titogo", "book.customer.example.com", nil, []string{"K"})
182 if err == nil {
183 t.Fatal("unsetting from a nonexistent overlay was turned into a create")
184 }
185 if !strings.Contains(err.Error(), "book.customer.example.com") {
186 t.Fatalf("the refusal does not name the instance: %v", err)
187 }
188}
189
190// Writing the layer bumps the app's generation object, the way
191// SetExecDeliveryAt does. It has to: ConfigRouteFingerprints digests
192// registry columns only (routefingerprint.go:58) and cannot see a
193// bucket-side change, so without the bump an overlay would wait out the
194// agent's hourly floor with nothing to signal it.
195func TestWritingTheInstanceLayerBumpsTheGeneration(t *testing.T) {
196 ctx := context.Background()
197 s := release.NewDirStore(t.TempDir())
198 if err := SetEnv(ctx, s, "bab", "titogo", map[string]string{"LOG_LEVEL": "debug"}); err != nil {
199 t.Fatalf("SetEnv: %v", err)
200 }
201 genETag := func() string {
202 t.Helper()
203 _, etag, err := s.Get(ctx, GenKey("bab", "titogo"))
204 if err != nil {
205 t.Fatalf("read generation: %v", err)
206 }
207 return etag
208 }
209 before := genETag()
210 if _, _, err := ApplyInstance(ctx, s, "bab", "titogo", "book.customer.example.com",
211 map[string]Var{"TENANT_SMTP_USER": {Kind: "env", Value: "book"}}, nil); err != nil {
212 t.Fatalf("ApplyInstance: %v", err)
213 }
214 afterWrite := genETag()
215 if afterWrite == before {
216 t.Fatal("writing the instance layer did not bump the generation, so the fast loop would never see it")
217 }
218 if err := DeleteInstanceConfig(ctx, s, "bab", "titogo", "book.customer.example.com"); err != nil {
219 t.Fatalf("DeleteInstanceConfig: %v", err)
220 }
221 if genETag() == afterWrite {
222 t.Fatal("removing the instance layer did not bump the generation, so the route would keep its last merge")
223 }
224 if _, _, found, err := LoadInstanceConfig(ctx, s, "bab", "titogo", "book.customer.example.com"); err != nil || found {
225 t.Fatalf("the overlay survived its delete: found=%v err=%v", found, err)
226 }
227 // Deleting again is a no-op success, so a retried teardown converges.
228 if err := DeleteInstanceConfig(ctx, s, "bab", "titogo", "book.customer.example.com"); err != nil {
229 t.Fatalf("second DeleteInstanceConfig: %v", err)
230 }
231}
232
233// The precedence the design states: default < environment < instance
234// (§6.2). Three layers, each winning per key over the one below it, and the
235// instance layer's etag handed back so the caller can later check what was
236// delivered.
237func TestLoadMergedHostResolvesDefaultThenEnvironmentThenInstance(t *testing.T) {
238 ctx := context.Background()
239 s := release.NewDirStore(t.TempDir())
240 if err := SetEnv(ctx, s, "bab", "titogo", map[string]string{
241 "FROM_DEFAULT": "default", "FROM_ENV": "default", "FROM_INSTANCE": "default"}); err != nil {
242 t.Fatalf("SetEnv: %v", err)
243 }
244 if _, err := ApplyEnvironment(ctx, s, "bab", "titogo", "prod", map[string]Var{
245 "FROM_ENV": {Kind: "env", Value: "environment"},
246 "FROM_INSTANCE": {Kind: "env", Value: "environment"}}, nil); err != nil {
247 t.Fatalf("ApplyEnvironment: %v", err)
248 }
249 _, want, err := ApplyInstance(ctx, s, "bab", "titogo", "book.customer.example.com", map[string]Var{
250 "FROM_INSTANCE": {Kind: "env", Value: "instance"}}, nil)
251 if err != nil {
252 t.Fatalf("ApplyInstance: %v", err)
253 }
254
255 m, etag, err := LoadMergedHost(ctx, s, "bab", "titogo", "prod", "book.customer.example.com")
256 if err != nil {
257 t.Fatalf("LoadMergedHost: %v", err)
258 }
259 if etag != want {
260 t.Fatalf("etag = %q, want the overlay's own %q", etag, want)
261 }
262 for k, wantVal := range map[string]string{
263 "FROM_DEFAULT": "default", "FROM_ENV": "environment", "FROM_INSTANCE": "instance"} {
264 if got := m.Vars[k].Value; got != wantVal {
265 t.Errorf("%s = %q, want %q", k, got, wantVal)
266 }
267 }
268
269 // A sibling with no overlay of its own gets the two-layer merge and an
270 // empty etag, which is what says "this host has no overlay" rather than
271 // "this agent did not render one".
272 m2, etag2, err := LoadMergedHost(ctx, s, "bab", "titogo", "prod", "other.customer.example.com")
273 if err != nil {
274 t.Fatalf("LoadMergedHost (sibling): %v", err)
275 }
276 if etag2 != "" {
277 t.Fatalf("a host with no overlay reported etag %q", etag2)
278 }
279 if m2.Vars["FROM_INSTANCE"].Value != "environment" {
280 t.Fatalf("the sibling saw another instance's value: %+v", m2.Vars)
281 }
282}
283
284// With no overlay, LoadMergedHost is LoadMerged, to the field. That matters
285// for the unbound case in particular: LoadMerged returns the default bundle
286// OBJECT there, exec_delivery and all, and a merge that quietly rebuilt it
287// would strip a flag other code reads off the same manifest.
288func TestLoadMergedHostWithNoOverlayIsExactlyLoadMerged(t *testing.T) {
289 ctx := context.Background()
290 s := release.NewDirStore(t.TempDir())
291 if err := SetEnv(ctx, s, "bab", "titogo", map[string]string{"K": "v"}); err != nil {
292 t.Fatalf("SetEnv: %v", err)
293 }
294 if err := SetExecDelivery(ctx, s, "bab", "titogo", ExecDeliveryFile); err != nil {
295 t.Fatalf("SetExecDelivery: %v", err)
296 }
297 want, err := LoadMerged(ctx, s, "bab", "titogo", "")
298 if err != nil {
299 t.Fatalf("LoadMerged: %v", err)
300 }
301 got, etag, err := LoadMergedHost(ctx, s, "bab", "titogo", "", "book.customer.example.com")
302 if err != nil || etag != "" {
303 t.Fatalf("LoadMergedHost = %q %v", etag, err)
304 }
305 if got.ExecDelivery != want.ExecDelivery || got.Vars["K"] != want.Vars["K"] || len(got.Vars) != len(want.Vars) {
306 t.Fatalf("LoadMergedHost diverged from LoadMerged with no overlay: %+v vs %+v", got, want)
307 }
308}
309
310// exec_delivery is never read from this layer (the ruling at
311// appconfig.go:66-71). An overlay that carries the field does not put it in
312// the merged view, so nothing downstream can act on it.
313func TestInstanceLayerNeverCarriesExecDelivery(t *testing.T) {
314 ctx := context.Background()
315 s := release.NewDirStore(t.TempDir())
316 if err := SetEnv(ctx, s, "bab", "titogo", map[string]string{"K": "v"}); err != nil {
317 t.Fatalf("SetEnv: %v", err)
318 }
319 putInstanceRaw(t, ctx, s, "bab", "titogo", "book.customer.example.com", &Manifest{
320 Schema: 1, App: "titogo", Vars: map[string]Var{"OWN": {Kind: "env", Value: "1"}},
321 ExecDelivery: ExecDeliveryFile})
322 m, _, err := LoadMergedHost(ctx, s, "bab", "titogo", "", "book.customer.example.com")
323 if err != nil {
324 t.Fatalf("LoadMergedHost: %v", err)
325 }
326 if m.ExecDelivery != "" {
327 t.Fatalf("exec_delivery = %q: the instance layer must never decide delivery", m.ExecDelivery)
328 }
329 if m.Vars["OWN"].Value != "1" {
330 t.Fatalf("the overlay's vars were dropped along with its flag: %+v", m.Vars)
331 }
332}
333
334// A route bound to a bundle that does not exist is still an ERROR, overlay
335// or no overlay. The binding asserted the bundle exists; an overlay does not
336// make that assertion true, and answering with a partial merge would hand a
337// live route a file built from a binding nobody honoured.
338func TestLoadMergedHostStillRefusesAMissingBoundBundle(t *testing.T) {
339 ctx := context.Background()
340 s := release.NewDirStore(t.TempDir())
341 if err := SetEnv(ctx, s, "bab", "titogo", map[string]string{"K": "v"}); err != nil {
342 t.Fatalf("SetEnv: %v", err)
343 }
344 if _, _, err := ApplyInstance(ctx, s, "bab", "titogo", "book.customer.example.com",
345 map[string]Var{"OWN": {Kind: "env", Value: "1"}}, nil); err != nil {
346 t.Fatalf("ApplyInstance: %v", err)
347 }
348 if _, _, err := LoadMergedHost(ctx, s, "bab", "titogo", "gone", "book.customer.example.com"); err == nil {
349 t.Fatal("a missing bound bundle resolved anyway because the host had an overlay")
350 }
351}
352
353// The index the sweeps use: one listing per app answers "which of this
354// app's instances have an overlay at all", so a pass costs a Get only for
355// the instances that do.
356func TestInstanceOverlaysListsOnlyThisAppsHosts(t *testing.T) {
357 ctx := context.Background()
358 s := release.NewDirStore(t.TempDir())
359 have, err := instanceOverlays(ctx, s, "bab", "titogo")
360 if err != nil || len(have) != 0 {
361 t.Fatalf("empty app = %v %v, want an empty set and no error", have, err)
362 }
363 for _, host := range []string{"book.customer.example.com", "cart.customer.example.com"} {
364 if _, _, err := ApplyInstance(ctx, s, "bab", "titogo", host,
365 map[string]Var{"OWN": {Kind: "env", Value: host}}, nil); err != nil {
366 t.Fatalf("ApplyInstance %s: %v", host, err)
367 }
368 }
369 if _, _, err := ApplyInstance(ctx, s, "bab", "eleven", "other.example.com",
370 map[string]Var{"OWN": {Kind: "env", Value: "x"}}, nil); err != nil {
371 t.Fatalf("ApplyInstance (other app): %v", err)
372 }
373 have, err = instanceOverlays(ctx, s, "bab", "titogo")
374 if err != nil {
375 t.Fatalf("instanceOverlays: %v", err)
376 }
377 if len(have) != 2 || !have["book.customer.example.com"] || !have["cart.customer.example.com"] {
378 t.Fatalf("instanceOverlays = %v, want just this app's two hosts", have)
379 }
380 if have["other.example.com"] {
381 t.Fatal("another app's overlay leaked into this app's index")
382 }
383}
384
385// countingStore counts the reads and writes a pass makes, per key, so a
386// test can pin the COST of a feature and not just its output. The instance
387// layer's promise is one extra listing per app and one extra Get per
388// instance that actually has an overlay, which is a claim only a counter
389// can keep honest.
390type countingStore struct {
391 release.Store
392 mu sync.Mutex
393 gets map[string]int
394 lists map[string]int
395 puts map[string]int
396}
397
398func newCountingStore(s release.Store) *countingStore {
399 return &countingStore{Store: s, gets: map[string]int{}, lists: map[string]int{}, puts: map[string]int{}}
400}
401
402func (c *countingStore) Get(ctx context.Context, key string) ([]byte, string, error) {
403 c.mu.Lock()
404 c.gets[key]++
405 c.mu.Unlock()
406 return c.Store.Get(ctx, key)
407}
408
409func (c *countingStore) List(ctx context.Context, prefix string) ([]string, error) {
410 c.mu.Lock()
411 c.lists[prefix]++
412 c.mu.Unlock()
413 return c.Store.List(ctx, prefix)
414}
415
416func (c *countingStore) Put(ctx context.Context, key string, body []byte, ifMatch string) (string, error) {
417 c.mu.Lock()
418 c.puts[key]++
419 c.mu.Unlock()
420 return c.Store.Put(ctx, key, body, ifMatch)
421}
422
423func (c *countingStore) n(m map[string]int, k string) int {
424 c.mu.Lock()
425 defer c.mu.Unlock()
426 return m[k]
427}
428
429// total sums every count in m, so a test can assert "this pass wrote
430// nothing to the release store at all" without naming every key it might
431// have touched.
432func (c *countingStore) total(m map[string]int) int {
433 c.mu.Lock()
434 defer c.mu.Unlock()
435 n := 0
436 for _, v := range m {
437 n += v
438 }
439 return n
440}
441
442// syncInstanceKit is the unit-path fixture these tests share: a store, a
443// registry, an env dir and a parsed key pair.
444func syncInstanceKit(t *testing.T) (context.Context, release.Store, *registry.Registry, string, *ecdh.PrivateKey) {
445 t.Helper()
446 ctx := context.Background()
447 s := release.NewDirStore(t.TempDir())
448 reg := testRegistry(t)
449 priv, _, err := GenerateKey()
450 if err != nil {
451 t.Fatalf("GenerateKey: %v", err)
452 }
453 privKey, err := ParsePrivateKey(priv, "test")
454 if err != nil {
455 t.Fatalf("ParsePrivateKey: %v", err)
456 }
457 return ctx, s, reg, t.TempDir(), privKey
458}
459
460func unitRoute(t *testing.T, reg *registry.Registry, host, app, acct, env string) {
461 t.Helper()
462 if err := reg.Upsert(registry.Route{
463 Host: host, Kind: "instance", App: app, Account: acct, Status: "ready",
464 Environment: env, Backing: registry.BackingUnit,
465 }); err != nil {
466 t.Fatalf("Upsert %s: %v", host, err)
467 }
468}
469
470// The delivery this layer exists for: one instance of an app gets a value
471// its siblings never see. The sibling's file is the app merge, untouched.
472func TestSyncRendersTheInstanceOverlayForItsOwnRouteOnly(t *testing.T) {
473 ctx, s, reg, envDir, priv := syncInstanceKit(t)
474 if err := SetEnv(ctx, s, "bab", "titogo", map[string]string{"APP_WIDE": "yes", "TENANT": "none"}); err != nil {
475 t.Fatalf("SetEnv: %v", err)
476 }
477 if _, _, err := ApplyInstance(ctx, s, "bab", "titogo", "book.example.com",
478 map[string]Var{"TENANT": {Kind: "env", Value: "book"}}, nil); err != nil {
479 t.Fatalf("ApplyInstance: %v", err)
480 }
481 unitRoute(t, reg, "book.example.com", "titogo", "bab", "")
482 unitRoute(t, reg, "cart.example.com", "titogo", "bab", "")
483
484 if err := Sync(ctx, reg, s, priv, envDir, func(string) error { return nil }, nil); err != nil {
485 t.Fatalf("Sync: %v", err)
486 }
487 book, err := os.ReadFile(filepath.Join(envDir, "book.example.com.env"))
488 if err != nil {
489 t.Fatalf("read book env: %v", err)
490 }
491 if !bytes.Contains(book, []byte("TENANT=book\n")) || !bytes.Contains(book, []byte("APP_WIDE=yes\n")) {
492 t.Fatalf("the overlaid route's file is wrong:\n%s", book)
493 }
494 cart, err := os.ReadFile(filepath.Join(envDir, "cart.example.com.env"))
495 if err != nil {
496 t.Fatalf("read cart env: %v", err)
497 }
498 if bytes.Contains(cart, []byte("book")) || !bytes.Contains(cart, []byte("TENANT=none\n")) {
499 t.Fatalf("a sibling saw another instance's config:\n%s", cart)
500 }
501
502 // The sweep and the single-shot API must agree about what a host gets.
503 // The sweeps resolve through the group cache and never call
504 // LoadMergedHost, so this is the only thing holding the two together.
505 want, _, err := LoadMergedHost(ctx, s, "bab", "titogo", "", "book.example.com")
506 if err != nil {
507 t.Fatalf("LoadMergedHost: %v", err)
508 }
509 rendered, err := RenderEnv(want, priv, Binding{Account: "bab", App: "titogo"})
510 if err != nil {
511 t.Fatalf("RenderEnv: %v", err)
512 }
513 if !bytes.Contains(book, rendered) {
514 t.Fatalf("the pass and LoadMergedHost disagree:\npass:\n%s\nLoadMergedHost:\n%s", book, rendered)
515 }
516}
517
518// The cost. One listing per app, a Get for the instance that has an
519// overlay, and NOTHING for the instance that does not.
520func TestSyncCostsOneListingPerAppAndOneGetPerOverlay(t *testing.T) {
521 ctx, base, reg, envDir, priv := syncInstanceKit(t)
522 if err := SetEnv(ctx, base, "bab", "titogo", map[string]string{"APP_WIDE": "yes"}); err != nil {
523 t.Fatalf("SetEnv: %v", err)
524 }
525 if _, _, err := ApplyInstance(ctx, base, "bab", "titogo", "book.example.com",
526 map[string]Var{"TENANT": {Kind: "env", Value: "book"}}, nil); err != nil {
527 t.Fatalf("ApplyInstance: %v", err)
528 }
529 unitRoute(t, reg, "book.example.com", "titogo", "bab", "")
530 unitRoute(t, reg, "cart.example.com", "titogo", "bab", "")
531 unitRoute(t, reg, "desk.example.com", "titogo", "bab", "")
532
533 s := newCountingStore(base)
534 if err := Sync(ctx, reg, s, priv, envDir, func(string) error { return nil }, nil); err != nil {
535 t.Fatalf("Sync: %v", err)
536 }
537 if got := s.n(s.lists, InstancesConfigPrefix("bab", "titogo")); got != 1 {
538 t.Fatalf("instance overlay listings = %d, want exactly 1 for three routes of one app", got)
539 }
540 if got := s.n(s.gets, InstanceConfigKey("bab", "titogo", "book.example.com")); got != 1 {
541 t.Fatalf("overlay Gets for book = %d, want 1", got)
542 }
543 for _, host := range []string{"cart.example.com", "desk.example.com"} {
544 if got := s.n(s.gets, InstanceConfigKey("bab", "titogo", host)); got != 0 {
545 t.Fatalf("overlay Gets for %s = %d, want none: the listing already said it has no overlay", host, got)
546 }
547 }
548 // A convergence pass reads the release store, it never writes to it —
549 // every write this test triggers (SetEnv, ApplyInstance) went through
550 // the unwrapped base store during setup, before s ever saw a call.
551 if got := s.total(s.puts); got != 0 {
552 t.Fatalf("Sync put %d object(s) to the release store, want 0: this pass only writes local env files", got)
553 }
554}
555
556// The unit pass never delivers an exec-backed route's config at all
557// (TestSyncSkipsExecBackedRoutes) — SyncExec does, and reports its own
558// overlay failures on it correctly. Final review, plan 4: the unit pass
559// used to fetch the overlay for EVERY syncable route regardless of Backing
560// and only checked Backing 53 lines later, so it paid a Get for exactly the
561// exec-backed, overlay-bearing routes step 5 targets, and threw the result
562// away. This pins the fixed cost: zero Gets against that key from the unit
563// pass, whatever the route's overlay actually holds.
564func TestSyncPaysNoGetForAnExecBackedRoutesOverlay(t *testing.T) {
565 ctx, base, reg, envDir, priv := syncInstanceKit(t)
566 if err := SetEnv(ctx, base, "bab", "titogo", map[string]string{"APP_WIDE": "yes"}); err != nil {
567 t.Fatalf("SetEnv: %v", err)
568 }
569 if _, _, err := ApplyInstance(ctx, base, "bab", "titogo", "book.example.com",
570 map[string]Var{"TENANT": {Kind: "env", Value: "book"}}, nil); err != nil {
571 t.Fatalf("ApplyInstance: %v", err)
572 }
573 // Exec-backed: Backing left at its zero value, the fleet default (same
574 // shape as TestSyncSkipsExecBackedRoutes).
575 if err := reg.Upsert(registry.Route{
576 Host: "book.example.com", Kind: "instance", App: "titogo", Account: "bab", Status: "ready",
577 }); err != nil {
578 t.Fatalf("Upsert: %v", err)
579 }
580 s := newCountingStore(base)
581 if err := Sync(ctx, reg, s, priv, envDir, func(string) error { return nil }, nil); err != nil {
582 t.Fatalf("Sync: %v", err)
583 }
584 if got := s.n(s.gets, InstanceConfigKey("bab", "titogo", "book.example.com")); got != 0 {
585 t.Fatalf("unit pass Get'd an exec-backed route's overlay %d time(s), want 0: SyncExec owns delivering it, not Sync", got)
586 }
587 if _, err := os.Stat(filepath.Join(envDir, "book.example.com.env")); !os.IsNotExist(err) {
588 t.Fatalf("expected no env file for an exec-backed route, stat err = %v", err)
589 }
590}
591
592// A Get failure against an exec-backed route's overlay must be invisible to
593// the unit pass: gating the fetch on Backing == BackingUnit (rather than
594// merely discarding its result) means the failure never happens here at
595// all, so the pass cannot print the misleading "env file left as-is" claim
596// about a file it never writes for this route. SyncExec, exercised
597// separately, is the one that reports a failure on this exact key.
598func TestSyncPrintsNothingForAnExecBackedRoutesOverlayGetFailure(t *testing.T) {
599 ctx, base, reg, envDir, priv := syncInstanceKit(t)
600 if err := SetEnv(ctx, base, "bab", "titogo", map[string]string{"APP_WIDE": "yes"}); err != nil {
601 t.Fatalf("SetEnv: %v", err)
602 }
603 if _, _, err := ApplyInstance(ctx, base, "bab", "titogo", "book.example.com",
604 map[string]Var{"TENANT": {Kind: "env", Value: "book"}}, nil); err != nil {
605 t.Fatalf("ApplyInstance: %v", err)
606 }
607 if err := reg.Upsert(registry.Route{
608 Host: "book.example.com", Kind: "instance", App: "titogo", Account: "bab", Status: "ready",
609 }); err != nil {
610 t.Fatalf("Upsert: %v", err)
611 }
612 failing := &failingKeyStore{Store: base, failKey: InstanceConfigKey("bab", "titogo", "book.example.com"),
613 err: errors.New("s3: transient 503 SlowDown")}
614 out := captureStdout(t, func() {
615 if err := Sync(ctx, reg, failing, priv, envDir, func(string) error { return nil }, nil); err != nil {
616 t.Fatalf("Sync: %v", err)
617 }
618 })
619 if strings.Contains(out, "unresolved") {
620 t.Fatalf("the unit pass printed about an exec-backed route's overlay, which it never delivers or Gets:\n%s", out)
621 }
622 if !strings.Contains(out, "exec-backed") {
623 t.Fatalf("expected the ordinary exec-backed notice (this route still has a default bundle), got:\n%s", out)
624 }
625 if _, err := os.Stat(filepath.Join(envDir, "book.example.com.env")); !os.IsNotExist(err) {
626 t.Fatalf("expected no env file for an exec-backed route, stat err = %v", err)
627 }
628}
629
630// Two independently corrupt overlays are two independent failures, not one.
631// The listing itself succeeds (both hosts are named), so the "once per app"
632// dedup for a failed LISTING must not swallow the second route's Get
633// failure — that would leave one of two stale credential files invisible,
634// which is exactly what this layer exists to surface.
635//
636// book converges with a valid overlay first, so the case below covers the
637// sharper claim too: an already-delivered route's file survives untouched
638// when its overlay later becomes unreadable, not just that a route that
639// never had a file stays fileless.
640func TestSyncReportsEachRoutesOverlayGetFailureSeparately(t *testing.T) {
641 ctx, s, reg, envDir, priv := syncInstanceKit(t)
642 if err := SetEnv(ctx, s, "bab", "titogo", map[string]string{"APP_WIDE": "yes"}); err != nil {
643 t.Fatalf("SetEnv: %v", err)
644 }
645 // Only book is on the box for the baseline pass, so cart genuinely never
646 // has a file at any point — registering it before the baseline would
647 // let it converge on the shared APP_WIDE bundle too, which would blur
648 // the two cases this test is pinning apart.
649 unitRoute(t, reg, "book.example.com", "titogo", "bab", "")
650
651 if _, _, err := ApplyInstance(ctx, s, "bab", "titogo", "book.example.com",
652 map[string]Var{"TENANT": {Kind: "env", Value: "book"}}, nil); err != nil {
653 t.Fatalf("ApplyInstance: %v", err)
654 }
655 if err := Sync(ctx, reg, s, priv, envDir, func(string) error { return nil }, nil); err != nil {
656 t.Fatalf("Sync (baseline): %v", err)
657 }
658 delivered, err := os.ReadFile(filepath.Join(envDir, "book.example.com.env"))
659 if err != nil {
660 t.Fatalf("read book's file after the baseline pass: %v", err)
661 }
662 if !bytes.Contains(delivered, []byte("TENANT=book\n")) {
663 t.Fatalf("the baseline overlay did not land: %q", delivered)
664 }
665 unitRoute(t, reg, "cart.example.com", "titogo", "bab", "")
666
667 // Schema 2 is refused by getIfExistsAtETag ("newer than this binary
668 // understands"), which is a Get-time failure specific to the object
669 // read, not a listing failure — instanceOverlays only reads keys, never
670 // their bodies, so the listing itself succeeds for both hosts. Deleting
671 // book's valid overlay first, then putting the corrupt one raw (Put with
672 // ifMatch "" is create-only), is what turns an already-delivered route's
673 // overlay unreadable, mid-flight.
674 if err := DeleteInstanceConfig(ctx, s, "bab", "titogo", "book.example.com"); err != nil {
675 t.Fatalf("DeleteInstanceConfig: %v", err)
676 }
677 putInstanceRaw(t, ctx, s, "bab", "titogo", "book.example.com", &Manifest{Schema: 2, App: "titogo"})
678 putInstanceRaw(t, ctx, s, "bab", "titogo", "cart.example.com", &Manifest{Schema: 2, App: "titogo"})
679
680 out := captureStdout(t, func() {
681 if err := Sync(ctx, reg, s, priv, envDir, func(string) error { return nil }, nil); err != nil {
682 t.Fatalf("Sync: %v", err)
683 }
684 })
685 for _, host := range []string{"book.example.com", "cart.example.com"} {
686 if !strings.Contains(out, host) {
687 t.Fatalf("no line for %s's overlay failure, one route's stale file went unreported:\n%s", host, out)
688 }
689 }
690 if n := strings.Count(out, "instance config unresolved"); n != 2 {
691 t.Fatalf("instance config unresolved lines = %d, want 2 (one per corrupt overlay), got:\n%s", n, out)
692 }
693 // cart never had a file before this pass, and a Get failure must leave
694 // that exactly as it is: no env file materializes from a resolved group
695 // merge that an unresolved overlay was supposed to sit on top of.
696 if _, err := os.Stat(filepath.Join(envDir, "cart.example.com.env")); !os.IsNotExist(err) {
697 t.Fatalf("cart.example.com.env exists after an unresolved overlay: %v", err)
698 }
699 // book already had a delivered file, and its Get failure must leave that
700 // file exactly as it was: an overlay going unreadable is not licence to
701 // touch a route that already converged on it.
702 cur, err := os.ReadFile(filepath.Join(envDir, "book.example.com.env"))
703 if err != nil || !bytes.Equal(cur, delivered) {
704 t.Fatalf("book's already-delivered file changed after its overlay became unreadable: %q, want %q (err %v)", cur, delivered, err)
705 }
706}
707
708// A listing that fails is not a determination. The route keeps its file and
709// the pass says so, the same contract a bundle fetch failure already has.
710func TestSyncLeavesTheFileAloneWhenTheOverlayListingFails(t *testing.T) {
711 ctx, base, reg, envDir, priv := syncInstanceKit(t)
712 if err := SetEnv(ctx, base, "bab", "titogo", map[string]string{"APP_WIDE": "yes"}); err != nil {
713 t.Fatalf("SetEnv: %v", err)
714 }
715 unitRoute(t, reg, "book.example.com", "titogo", "bab", "")
716 path := filepath.Join(envDir, "book.example.com.env")
717 if err := os.WriteFile(path, []byte("APP_WIDE=stale\n"), 0o600); err != nil {
718 t.Fatalf("stage file: %v", err)
719 }
720 failing := &failingListStore{Store: base, failPrefix: InstancesConfigPrefix("bab", "titogo"),
721 err: errors.New("s3: transient 503 SlowDown")}
722 out := captureStdout(t, func() {
723 if err := Sync(ctx, reg, failing, priv, envDir, func(string) error { return nil }, nil); err != nil {
724 t.Fatalf("Sync: %v", err)
725 }
726 })
727 cur, err := os.ReadFile(path)
728 if err != nil || string(cur) != "APP_WIDE=stale\n" {
729 t.Fatalf("the file moved on a listing failure: %q %v", cur, err)
730 }
731 if !strings.Contains(out, "book.example.com") || !strings.Contains(out, "left as-is") {
732 t.Fatalf("the pass was silent about an unresolved instance layer:\n%s", out)
733 }
734}
735
736// The dedup sentinel itself, pinned across TWO routes of one app sharing
737// one failed LISTING (review finding, plan 4 task 7 fix round 1): every
738// other test naming a listing failure uses a single route, so a mutation
739// that stopped telling a listing failure apart from a per-host one —
740// errors.As(...) always false, or instanceOverlayListErr's wrapper dropped
741// entirely — would still leave every one of them green. This asserts the
742// dedup's actual effect: one line, not two, with one host anchor naming
743// whichever route the pass happened to reach first.
744func TestSyncDedupsTheOverlayListingFailureAcrossTwoRoutesOfOneApp(t *testing.T) {
745 ctx, base, reg, envDir, priv := syncInstanceKit(t)
746 if err := SetEnv(ctx, base, "bab", "titogo", map[string]string{"APP_WIDE": "yes"}); err != nil {
747 t.Fatalf("SetEnv: %v", err)
748 }
749 unitRoute(t, reg, "book.example.com", "titogo", "bab", "")
750 unitRoute(t, reg, "cart.example.com", "titogo", "bab", "")
751 for _, host := range []string{"book.example.com", "cart.example.com"} {
752 if err := os.WriteFile(filepath.Join(envDir, host+".env"), []byte("APP_WIDE=stale\n"), 0o600); err != nil {
753 t.Fatalf("stage %s: %v", host, err)
754 }
755 }
756 failing := &failingListStore{Store: base, failPrefix: InstancesConfigPrefix("bab", "titogo"),
757 err: errors.New("s3: transient 503 SlowDown")}
758 out := captureStdout(t, func() {
759 if err := Sync(ctx, reg, failing, priv, envDir, func(string) error { return nil }, nil); err != nil {
760 t.Fatalf("Sync: %v", err)
761 }
762 })
763 if n := strings.Count(out, "instance config unresolved"); n != 1 {
764 t.Fatalf("instance config unresolved lines = %d, want exactly 1 for one shared LISTING failure across two routes, got:\n%s", n, out)
765 }
766 if n := strings.Count(out, "(seen at "); n != 1 {
767 t.Fatalf("(seen at ...) host anchors = %d, want exactly 1, got:\n%s", n, out)
768 }
769 for _, host := range []string{"book.example.com", "cart.example.com"} {
770 cur, err := os.ReadFile(filepath.Join(envDir, host+".env"))
771 if err != nil || string(cur) != "APP_WIDE=stale\n" {
772 t.Fatalf("%s moved on a listing failure: %q %v", host, cur, err)
773 }
774 }
775}
776
777// The exec mirror: SyncExec had NO listing-failure test at all before this
778// (its said-once-per-app contract, stated in the doc comment above the
779// overlayFor loop, was pinned nowhere). Same two-route shape as the Sync
780// test above, same three mutations it exists to catch.
781func TestSyncExecDedupsTheOverlayListingFailureAcrossTwoRoutesOfOneApp(t *testing.T) {
782 ctx, base, reg, dd, privB64, _ := execKit(t)
783 priv, err := ParsePrivateKey(privB64, "test")
784 if err != nil {
785 t.Fatalf("ParsePrivateKey: %v", err)
786 }
787 if err := SetEnv(ctx, base, "bab", "titogo", map[string]string{"APP_WIDE": "yes"}); err != nil {
788 t.Fatalf("SetEnv: %v", err)
789 }
790 for _, host := range []string{"book.example.com", "cart.example.com"} {
791 if err := reg.Upsert(registry.Route{
792 Host: host, Kind: "instance", App: "titogo", Account: "bab", Status: "ready",
793 Socket: "/run/carlos/" + host + ".sock",
794 }); err != nil {
795 t.Fatalf("Upsert %s: %v", host, err)
796 }
797 if err := os.WriteFile(filepath.Join(dd, host+".config.env"), []byte("OLD=1\n"), 0o600); err != nil {
798 t.Fatalf("stage %s: %v", host, err)
799 }
800 }
801 failing := &failingListStore{Store: base, failPrefix: InstancesConfigPrefix("bab", "titogo"),
802 err: errors.New("s3: transient 503 SlowDown")}
803 out := captureStdout(t, func() {
804 if err := SyncExec(ctx, reg, failing, priv, dd, func(string) error { return nil }, "", nil); err != nil {
805 t.Fatalf("SyncExec: %v", err)
806 }
807 })
808 if n := strings.Count(out, "instance config unresolved"); n != 1 {
809 t.Fatalf("instance config unresolved lines = %d, want exactly 1 for one shared LISTING failure across two routes, got:\n%s", n, out)
810 }
811 if n := strings.Count(out, "(seen at "); n != 1 {
812 t.Fatalf("(seen at ...) host anchors = %d, want exactly 1, got:\n%s", n, out)
813 }
814 for _, host := range []string{"book.example.com", "cart.example.com"} {
815 cur, err := os.ReadFile(filepath.Join(dd, host+".config.env"))
816 if err != nil || string(cur) != "OLD=1\n" {
817 t.Fatalf("%s moved on a listing failure: %q %v", host, cur, err)
818 }
819 }
820}
821
822// failingListStore makes List fail for exactly one prefix, the listing
823// shape of failingKeyStore (syncexec_test.go).
824type failingListStore struct {
825 release.Store
826 failPrefix string
827 err error
828}
829
830func (f *failingListStore) List(ctx context.Context, prefix string) ([]string, error) {
831 if prefix == f.failPrefix {
832 return nil, f.err
833 }
834 return f.Store.List(ctx, prefix)
835}
836
837// The CRITICAL 1 guard's second door. An app with no config.json, a route
838// bound to nothing, and an overlay: the merged view is that overlay ALONE,
839// which is a wholesale replacement of whatever a hand-staged file holds.
840// Refuse it, loudly, every pass, and name the way out.
841func TestSyncRefusesToDropHandStagedKeysForAnOverlayAlone(t *testing.T) {
842 ctx, s, reg, envDir, priv := syncInstanceKit(t)
843 if _, _, err := ApplyInstance(ctx, s, "bab", "titogo", "book.example.com",
844 map[string]Var{"TENANT": {Kind: "env", Value: "book"}}, nil); err != nil {
845 t.Fatalf("ApplyInstance: %v", err)
846 }
847 unitRoute(t, reg, "book.example.com", "titogo", "bab", "")
848 path := filepath.Join(envDir, "book.example.com.env")
849 staged := "HAND_STAGED=1\nTENANT=old\n"
850 if err := os.WriteFile(path, []byte(staged), 0o600); err != nil {
851 t.Fatalf("stage file: %v", err)
852 }
853
854 var restarted []string
855 out := captureStdout(t, func() {
856 if err := Sync(ctx, reg, s, priv, envDir, func(h string) error {
857 restarted = append(restarted, h)
858 return nil
859 }, nil); err != nil {
860 t.Fatalf("Sync: %v", err)
861 }
862 })
863 cur, err := os.ReadFile(path)
864 if err != nil || string(cur) != staged {
865 t.Fatalf("a hand-staged file was replaced by an overlay-alone render: %q %v", cur, err)
866 }
867 if len(restarted) != 0 {
868 t.Fatalf("restarted %v for a route this pass refused", restarted)
869 }
870 if !strings.Contains(out, "HAND_STAGED") || !strings.Contains(out, "book.example.com") {
871 t.Fatalf("the refusal does not name the route and the key it would have dropped:\n%s", out)
872 }
873}
874
875// The same shape, self-healing: nothing is lost, so the route converges.
876// An app with no default bundle and an instance overlay is a legitimate
877// state (step 5 creates exactly that for a fresh instance), and this guard
878// must not freeze it forever.
879func TestSyncConvergesAnOverlayAloneWhenNothingIsLost(t *testing.T) {
880 ctx, s, reg, envDir, priv := syncInstanceKit(t)
881 if _, _, err := ApplyInstance(ctx, s, "bab", "titogo", "book.example.com",
882 map[string]Var{"TENANT": {Kind: "env", Value: "book"}}, nil); err != nil {
883 t.Fatalf("ApplyInstance: %v", err)
884 }
885 unitRoute(t, reg, "book.example.com", "titogo", "bab", "")
886 if err := Sync(ctx, reg, s, priv, envDir, func(string) error { return nil }, nil); err != nil {
887 t.Fatalf("Sync: %v", err)
888 }
889 cur, err := os.ReadFile(filepath.Join(envDir, "book.example.com.env"))
890 if err != nil {
891 t.Fatalf("read env file: %v", err)
892 }
893 if !bytes.Contains(cur, []byte("TENANT=book\n")) {
894 t.Fatalf("the overlay-alone render did not land:\n%s", cur)
895 }
896}
897
898// And the case that must NOT change: no default bundle, no overlay, no
899// binding is still "never configured at all", and a never-configured app's
900// hand-staged file is not this pass's to touch.
901func TestSyncStillLeavesUnconfiguredAppsAlone(t *testing.T) {
902 ctx, s, reg, envDir, priv := syncInstanceKit(t)
903 unitRoute(t, reg, "book.example.com", "titogo", "bab", "")
904 path := filepath.Join(envDir, "book.example.com.env")
905 staged := "HAND_STAGED=1\n"
906 if err := os.WriteFile(path, []byte(staged), 0o600); err != nil {
907 t.Fatalf("stage file: %v", err)
908 }
909 if err := Sync(ctx, reg, s, priv, envDir, func(string) error { return nil }, nil); err != nil {
910 t.Fatalf("Sync: %v", err)
911 }
912 cur, err := os.ReadFile(path)
913 if err != nil || string(cur) != staged {
914 t.Fatalf("an unconfigured app's file moved: %q %v", cur, err)
915 }
916}
917
918// The exec half of the same delivery: one activator-spawned instance gets
919// its own value, its sibling does not, and the listing is paid for once.
920func TestSyncExecRendersTheInstanceOverlayForItsOwnRouteOnly(t *testing.T) {
921 ctx, base, reg, dd, privB64, _ := execKit(t)
922 priv, err := ParsePrivateKey(privB64, "test")
923 if err != nil {
924 t.Fatalf("ParsePrivateKey: %v", err)
925 }
926 if err := SetEnv(ctx, base, "bab", "titogo", map[string]string{"APP_WIDE": "yes", "TENANT": "none"}); err != nil {
927 t.Fatalf("SetEnv: %v", err)
928 }
929 if _, _, err := ApplyInstance(ctx, base, "bab", "titogo", "book.example.com",
930 map[string]Var{"TENANT": {Kind: "env", Value: "book"}}, nil); err != nil {
931 t.Fatalf("ApplyInstance: %v", err)
932 }
933 for _, host := range []string{"book.example.com", "cart.example.com"} {
934 if err := reg.Upsert(registry.Route{
935 Host: host, Kind: "instance", App: "titogo", Account: "bab", Status: "ready",
936 Socket: "/run/carlos/" + host + ".sock",
937 }); err != nil {
938 t.Fatalf("Upsert %s: %v", host, err)
939 }
940 }
941
942 s := newCountingStore(base)
943 if err := SyncExec(ctx, reg, s, priv, dd, func(string) error { return nil }, "", nil); err != nil {
944 t.Fatalf("SyncExec: %v", err)
945 }
946 book, err := os.ReadFile(filepath.Join(dd, "book.example.com.config.env"))
947 if err != nil {
948 t.Fatalf("read book config: %v", err)
949 }
950 if !bytes.Contains(book, []byte("TENANT=book\n")) || !bytes.Contains(book, []byte("APP_WIDE=yes\n")) {
951 t.Fatalf("the overlaid route's config file is wrong:\n%s", book)
952 }
953 cart, err := os.ReadFile(filepath.Join(dd, "cart.example.com.config.env"))
954 if err != nil {
955 t.Fatalf("read cart config: %v", err)
956 }
957 if bytes.Contains(cart, []byte("book")) || !bytes.Contains(cart, []byte("TENANT=none\n")) {
958 t.Fatalf("a sibling saw another instance's config:\n%s", cart)
959 }
960 if got := s.n(s.lists, InstancesConfigPrefix("bab", "titogo")); got != 1 {
961 t.Fatalf("instance overlay listings = %d, want exactly 1 for two routes of one app", got)
962 }
963 if got := s.n(s.gets, InstanceConfigKey("bab", "titogo", "cart.example.com")); got != 0 {
964 t.Fatalf("read an overlay for a host the listing did not name (%d times)", got)
965 }
966}
967
968// exec_delivery is a BUNDLE-level flag and the instance layer is not one of
969// the bundles it is read from (the ruling at appconfig.go:66-71). A TCP-addr
970// route whose app has never opted in gets no file, whatever its overlay
971// says — a per-host delivery flag is exactly the granularity that ruling
972// refused, and honouring one here would be the config-that-lies hazard it
973// names.
974func TestSyncExecNeverReadsExecDeliveryFromTheInstanceLayer(t *testing.T) {
975 ctx, s, reg, dd, privB64, _ := execKit(t)
976 priv, err := ParsePrivateKey(privB64, "test")
977 if err != nil {
978 t.Fatalf("ParsePrivateKey: %v", err)
979 }
980 if err := SetEnv(ctx, s, "bab", "titogo", map[string]string{"APP_WIDE": "yes"}); err != nil {
981 t.Fatalf("SetEnv: %v", err)
982 }
983 putInstanceRaw(t, ctx, s, "bab", "titogo", "router.example.com", &Manifest{
984 Schema: 1, App: "titogo", Vars: map[string]Var{"TENANT": {Kind: "env", Value: "book"}},
985 ExecDelivery: ExecDeliveryFile})
986 if err := reg.Upsert(registry.Route{
987 Host: "router.example.com", Kind: "instance", App: "titogo", Account: "bab",
988 Status: "ready", Addr: "127.0.0.1:8080",
989 }); err != nil {
990 t.Fatalf("Upsert: %v", err)
991 }
992 if err := SyncExec(ctx, reg, s, priv, dd, func(string) error { return nil }, "", nil); err != nil {
993 t.Fatalf("SyncExec: %v", err)
994 }
995 if _, err := os.Stat(filepath.Join(dd, "router.example.com.config.env")); !os.IsNotExist(err) {
996 t.Fatalf("an instance overlay opted a route into file delivery: %v", err)
997 }
998
999 // Opt the APP in, and the same route gets the same overlay merged in the
1000 // ordinary way — the flag's door is the bundle, the overlay's payload is
1001 // vars.
1002 if err := SetExecDelivery(ctx, s, "bab", "titogo", ExecDeliveryFile); err != nil {
1003 t.Fatalf("SetExecDelivery: %v", err)
1004 }
1005 if err := SyncExec(ctx, reg, s, priv, dd, func(string) error { return nil }, "", nil); err != nil {
1006 t.Fatalf("SyncExec (opted in): %v", err)
1007 }
1008 cur, err := os.ReadFile(filepath.Join(dd, "router.example.com.config.env"))
1009 if err != nil {
1010 t.Fatalf("read router config: %v", err)
1011 }
1012 if !bytes.Contains(cur, []byte("TENANT=book\n")) {
1013 t.Fatalf("the overlay's vars did not land once the app opted in:\n%s", cur)
1014 }
1015}
1016
1017// The exec mirror of TestSyncReportsEachRoutesOverlayGetFailureSeparately:
1018// two independently corrupt overlays are two independent failures, not one.
1019// The listing itself succeeds (both hosts are named), so the "once per app"
1020// dedup for a failed LISTING must not swallow the second route's Get
1021// failure — that would leave one of two stale config files invisible,
1022// which is exactly what this layer exists to surface.
1023func TestSyncExecReportsEachRoutesOverlayGetFailureSeparately(t *testing.T) {
1024 ctx, s, reg, dd, privB64, _ := execKit(t)
1025 priv, err := ParsePrivateKey(privB64, "test")
1026 if err != nil {
1027 t.Fatalf("ParsePrivateKey: %v", err)
1028 }
1029 if err := SetEnv(ctx, s, "bab", "titogo", map[string]string{"APP_WIDE": "yes"}); err != nil {
1030 t.Fatalf("SetEnv: %v", err)
1031 }
1032 // Schema 2 is refused by getIfExistsAtETag ("newer than this binary
1033 // understands"), which is a Get-time failure specific to the object
1034 // read, not a listing failure — instanceOverlays only reads keys, never
1035 // their bodies, so the listing itself succeeds for both hosts.
1036 putInstanceRaw(t, ctx, s, "bab", "titogo", "book.example.com", &Manifest{Schema: 2, App: "titogo"})
1037 putInstanceRaw(t, ctx, s, "bab", "titogo", "cart.example.com", &Manifest{Schema: 2, App: "titogo"})
1038 for _, host := range []string{"book.example.com", "cart.example.com"} {
1039 if err := reg.Upsert(registry.Route{
1040 Host: host, Kind: "instance", App: "titogo", Account: "bab", Status: "ready",
1041 Socket: "/run/carlos/" + host + ".sock",
1042 }); err != nil {
1043 t.Fatalf("Upsert %s: %v", host, err)
1044 }
1045 }
1046
1047 out := captureStdout(t, func() {
1048 if err := SyncExec(ctx, reg, s, priv, dd, func(string) error { return nil }, "", nil); err != nil {
1049 t.Fatalf("SyncExec: %v", err)
1050 }
1051 })
1052 for _, host := range []string{"book.example.com", "cart.example.com"} {
1053 if !strings.Contains(out, host) {
1054 t.Fatalf("no line for %s's overlay failure, one route's stale file went unreported:\n%s", host, out)
1055 }
1056 }
1057 if n := strings.Count(out, "instance config unresolved"); n != 2 {
1058 t.Fatalf("instance config unresolved lines = %d, want 2 (one per corrupt overlay), got:\n%s", n, out)
1059 }
1060 // Neither route had a file before this pass, and a Get failure must
1061 // leave that exactly as it is: no config file materializes from a
1062 // resolved group merge that an unresolved overlay was supposed to sit
1063 // on top of.
1064 for _, host := range []string{"book.example.com", "cart.example.com"} {
1065 if _, err := os.Stat(filepath.Join(dd, host+".config.env")); !os.IsNotExist(err) {
1066 t.Fatalf("%s.config.env exists after an unresolved overlay: %v", host, err)
1067 }
1068 }
1069}
1070
1071// An app with no default bundle at all, and one instance with an overlay.
1072// The overlay is the whole configuration of that route, so its file is
1073// written, not removed — while its sibling, which has nothing anywhere, is
1074// reaped exactly as it is today.
1075func TestSyncExecConvergesAnOverlayAloneInsteadOfRemoving(t *testing.T) {
1076 ctx, s, reg, dd, privB64, _ := execKit(t)
1077 priv, err := ParsePrivateKey(privB64, "test")
1078 if err != nil {
1079 t.Fatalf("ParsePrivateKey: %v", err)
1080 }
1081 if _, _, err := ApplyInstance(ctx, s, "bab", "titogo", "book.example.com",
1082 map[string]Var{"TENANT": {Kind: "env", Value: "book"}}, nil); err != nil {
1083 t.Fatalf("ApplyInstance: %v", err)
1084 }
1085 for _, host := range []string{"book.example.com", "cart.example.com"} {
1086 if err := reg.Upsert(registry.Route{
1087 Host: host, Kind: "instance", App: "titogo", Account: "bab", Status: "ready",
1088 Socket: "/run/carlos/" + host + ".sock",
1089 }); err != nil {
1090 t.Fatalf("Upsert %s: %v", host, err)
1091 }
1092 if err := os.WriteFile(filepath.Join(dd, host+".config.env"), []byte("OLD=1\n"), 0o600); err != nil {
1093 t.Fatalf("stage %s: %v", host, err)
1094 }
1095 }
1096 if err := SyncExec(ctx, reg, s, priv, dd, func(string) error { return nil }, "", nil); err != nil {
1097 t.Fatalf("SyncExec: %v", err)
1098 }
1099 book, err := os.ReadFile(filepath.Join(dd, "book.example.com.config.env"))
1100 if err != nil {
1101 t.Fatalf("the overlaid route's file was removed: %v", err)
1102 }
1103 if !bytes.Contains(book, []byte("TENANT=book\n")) || bytes.Contains(book, []byte("OLD=1")) {
1104 t.Fatalf("the overlay-alone render did not converge:\n%s", book)
1105 }
1106 if _, err := os.Stat(filepath.Join(dd, "cart.example.com.config.env")); !os.IsNotExist(err) {
1107 t.Fatalf("a route with no config anywhere kept its file: %v", err)
1108 }
1109}
1110
1111// The mass-removal guard, with an overlay in the middle of it. Two apps
1112// whose manifests all read absent still freeze every removal on the box,
1113// and the one route that has an overlay still converges: writing a file a
1114// bundle asked for is not a removal, and the guard exists to stop
1115// deletions, not delivery.
1116func TestSyncExecMassRemovalGuardHoldsRemovalsButNotOverlays(t *testing.T) {
1117 ctx, s, reg, dd, privB64, _ := execKit(t)
1118 priv, err := ParsePrivateKey(privB64, "test")
1119 if err != nil {
1120 t.Fatalf("ParsePrivateKey: %v", err)
1121 }
1122 if _, _, err := ApplyInstance(ctx, s, "bab", "titogo", "book.example.com",
1123 map[string]Var{"TENANT": {Kind: "env", Value: "book"}}, nil); err != nil {
1124 t.Fatalf("ApplyInstance: %v", err)
1125 }
1126 routes := map[string]string{
1127 "book.example.com": "titogo",
1128 "cart.example.com": "titogo",
1129 "desk.example.com": "eleven",
1130 }
1131 for host, app := range routes {
1132 if err := reg.Upsert(registry.Route{
1133 Host: host, Kind: "instance", App: app, Account: "bab", Status: "ready",
1134 Socket: "/run/carlos/" + host + ".sock",
1135 }); err != nil {
1136 t.Fatalf("Upsert %s: %v", host, err)
1137 }
1138 if err := os.WriteFile(filepath.Join(dd, host+".config.env"), []byte("OLD=1\n"), 0o600); err != nil {
1139 t.Fatalf("stage %s: %v", host, err)
1140 }
1141 }
1142 out := captureStdout(t, func() {
1143 if err := SyncExec(ctx, reg, s, priv, dd, func(string) error { return nil }, "", nil); err != nil {
1144 t.Fatalf("SyncExec: %v", err)
1145 }
1146 })
1147 if !strings.Contains(out, "refusing to remove any config files this pass") {
1148 t.Fatalf("the mass-removal guard did not fire:\n%s", out)
1149 }
1150 for _, host := range []string{"cart.example.com", "desk.example.com"} {
1151 cur, err := os.ReadFile(filepath.Join(dd, host+".config.env"))
1152 if err != nil || string(cur) != "OLD=1\n" {
1153 t.Fatalf("%s lost its file while the guard was up: %q %v", host, cur, err)
1154 }
1155 }
1156 book, err := os.ReadFile(filepath.Join(dd, "book.example.com.config.env"))
1157 if err != nil {
1158 t.Fatalf("the overlaid route was frozen by a guard about removals: %v", err)
1159 }
1160 if !bytes.Contains(book, []byte("TENANT=book\n")) {
1161 t.Fatalf("the overlay did not converge under the guard:\n%s", book)
1162 }
1163}
1164
1165// The layer's only convergence signal is the generation object. The route
1166// fingerprint cannot see a bucket write and must not start paying to try:
1167// this pins both halves of that at once.
1168func TestWritingAnOverlayMovesTheGenerationAndNotTheFingerprint(t *testing.T) {
1169 ctx := context.Background()
1170 s := release.NewDirStore(t.TempDir())
1171 if err := SetEnv(ctx, s, "bab", "titogo", map[string]string{"APP_WIDE": "yes"}); err != nil {
1172 t.Fatalf("SetEnv: %v", err)
1173 }
1174 routes := []registry.Route{{
1175 Host: "book.example.com", Kind: "instance", App: "titogo", Account: "bab",
1176 Status: "ready", Socket: "/run/carlos/book.sock",
1177 }}
1178 execBefore, unitBefore := ConfigRouteFingerprints(routes)
1179 _, genBefore, err := s.Get(ctx, GenKey("bab", "titogo"))
1180 if err != nil {
1181 t.Fatalf("read generation: %v", err)
1182 }
1183
1184 if _, _, err := ApplyInstance(ctx, s, "bab", "titogo", "book.example.com",
1185 map[string]Var{"TENANT": {Kind: "env", Value: "book"}}, nil); err != nil {
1186 t.Fatalf("ApplyInstance: %v", err)
1187 }
1188
1189 execAfter, unitAfter := ConfigRouteFingerprints(routes)
1190 if execAfter != execBefore || unitAfter != unitBefore {
1191 t.Fatal("the route fingerprint moved for a bucket-side write, which would cost a read per route per tick")
1192 }
1193 _, genAfter, err := s.Get(ctx, GenKey("bab", "titogo"))
1194 if err != nil {
1195 t.Fatalf("read generation: %v", err)
1196 }
1197 if genAfter == genBefore {
1198 t.Fatal("the generation did not move, so the fast loop has nothing to notice and delivery waits out the hourly floor")
1199 }
1200}
1201
1202// One pass over a box holding every shape this layer comes in: an instance
1203// with an overlay over its app's bundle, a sibling with none, and an
1204// instance whose app has no bundle at all so its overlay stands alone.
1205// Then the overlay is removed and the route converges back, with the ack
1206// saying so.
1207func TestOnePassOverEveryInstanceLayerShape(t *testing.T) {
1208 ctx, s, reg, dd, privB64, _ := execKit(t)
1209 priv, err := ParsePrivateKey(privB64, "test")
1210 if err != nil {
1211 t.Fatalf("ParsePrivateKey: %v", err)
1212 }
1213 if err := SetEnv(ctx, s, "bab", "titogo", map[string]string{"APP_WIDE": "yes", "TENANT": "none"}); err != nil {
1214 t.Fatalf("SetEnv: %v", err)
1215 }
1216 _, bookETag, err := ApplyInstance(ctx, s, "bab", "titogo", "book.example.com",
1217 map[string]Var{"TENANT": {Kind: "env", Value: "book"}}, nil)
1218 if err != nil {
1219 t.Fatalf("ApplyInstance (book): %v", err)
1220 }
1221 _, deskETag, err := ApplyInstance(ctx, s, "bab", "solo", "desk.example.com",
1222 map[string]Var{"TENANT": {Kind: "env", Value: "desk"}}, nil)
1223 if err != nil {
1224 t.Fatalf("ApplyInstance (desk): %v", err)
1225 }
1226 for host, app := range map[string]string{
1227 "book.example.com": "titogo", "cart.example.com": "titogo", "desk.example.com": "solo"} {
1228 if err := reg.Upsert(registry.Route{
1229 Host: host, Kind: "instance", App: app, Account: "bab", Status: "ready",
1230 Socket: "/run/carlos/" + host + ".sock",
1231 }); err != nil {
1232 t.Fatalf("Upsert %s: %v", host, err)
1233 }
1234 }
1235
1236 acks := map[string]ConfigAck{}
1237 collect := func(a ConfigAck) { acks[a.Host] = a }
1238 if err := SyncExec(ctx, reg, s, priv, dd, func(string) error { return nil }, "", collect); err != nil {
1239 t.Fatalf("SyncExec: %v", err)
1240 }
1241 read := func(host string) string {
1242 t.Helper()
1243 body, err := os.ReadFile(filepath.Join(dd, host+".config.env"))
1244 if err != nil {
1245 t.Fatalf("read %s: %v", host, err)
1246 }
1247 return string(body)
1248 }
1249 if got := read("book.example.com"); !strings.Contains(got, "TENANT=book\n") || !strings.Contains(got, "APP_WIDE=yes\n") {
1250 t.Fatalf("book: %q", got)
1251 }
1252 if got := read("cart.example.com"); !strings.Contains(got, "TENANT=none\n") {
1253 t.Fatalf("cart saw another instance's config: %q", got)
1254 }
1255 if got := read("desk.example.com"); got != "TENANT=desk\n" {
1256 t.Fatalf("desk, whose app has no bundle, should render its overlay alone: %q", got)
1257 }
1258 for host, want := range map[string]string{
1259 "book.example.com": bookETag, "cart.example.com": "", "desk.example.com": deskETag} {
1260 a, ok := acks[host]
1261 if !ok {
1262 t.Fatalf("no ack for %s", host)
1263 }
1264 if a.Schema != ConfigAckSchema || a.InstanceConfigETag != want {
1265 t.Fatalf("%s ack = %+v, want schema %d and etag %q", host, a, ConfigAckSchema, want)
1266 }
1267 if v := InstanceConfigDelivery(&a, want); v != InstanceConfigDelivered {
1268 t.Fatalf("%s delivery = %q, want %q", host, v, InstanceConfigDelivered)
1269 }
1270 }
1271
1272 // Remove one overlay: that route converges back to its app's merge, and
1273 // its ack stops naming an overlay, which is what tells a reader the
1274 // removal actually reached the box.
1275 if err := DeleteInstanceConfig(ctx, s, "bab", "titogo", "book.example.com"); err != nil {
1276 t.Fatalf("DeleteInstanceConfig: %v", err)
1277 }
1278 acks = map[string]ConfigAck{}
1279 if err := SyncExec(ctx, reg, s, priv, dd, func(string) error { return nil }, "", collect); err != nil {
1280 t.Fatalf("SyncExec (after delete): %v", err)
1281 }
1282 if got := read("book.example.com"); !strings.Contains(got, "TENANT=none\n") {
1283 t.Fatalf("book did not converge back to its app's merge: %q", got)
1284 }
1285 if a := acks["book.example.com"]; a.InstanceConfigETag != "" || a.Schema != ConfigAckSchema {
1286 t.Fatalf("book ack after the delete = %+v, want the schema and no overlay", a)
1287 }
1288 if got := read("desk.example.com"); got != "TENANT=desk\n" {
1289 t.Fatalf("desk was disturbed by a sibling app's delete: %q", got)
1290 }
1291}
1292
1293// The unit-path half of the removal claim. TestOnePassOverEveryInstanceLayerShape
1294// pins the exec sweep's removal convergence; nothing else in the package
1295// runs Sync (root's unit path) after a delete, so without this the claim
1296// that a route "goes back to what the bundles alone say" once its overlay
1297// is removed is only ever true for SyncExec.
1298func TestSyncConvergesBackToTheBundleAfterAnOverlayIsDeleted(t *testing.T) {
1299 ctx, s, reg, envDir, priv := syncInstanceKit(t)
1300 if err := SetEnv(ctx, s, "bab", "titogo", map[string]string{"APP_WIDE": "yes", "TENANT": "none"}); err != nil {
1301 t.Fatalf("SetEnv: %v", err)
1302 }
1303 if _, _, err := ApplyInstance(ctx, s, "bab", "titogo", "book.example.com",
1304 map[string]Var{"TENANT": {Kind: "env", Value: "book"}}, nil); err != nil {
1305 t.Fatalf("ApplyInstance: %v", err)
1306 }
1307 unitRoute(t, reg, "book.example.com", "titogo", "bab", "")
1308
1309 if err := Sync(ctx, reg, s, priv, envDir, func(string) error { return nil }, nil); err != nil {
1310 t.Fatalf("Sync: %v", err)
1311 }
1312 before, err := os.ReadFile(filepath.Join(envDir, "book.example.com.env"))
1313 if err != nil {
1314 t.Fatalf("read env file before the delete: %v", err)
1315 }
1316 if !bytes.Contains(before, []byte("TENANT=book\n")) {
1317 t.Fatalf("the overlay did not land before the delete: %q", before)
1318 }
1319
1320 if err := DeleteInstanceConfig(ctx, s, "bab", "titogo", "book.example.com"); err != nil {
1321 t.Fatalf("DeleteInstanceConfig: %v", err)
1322 }
1323 if err := Sync(ctx, reg, s, priv, envDir, func(string) error { return nil }, nil); err != nil {
1324 t.Fatalf("Sync (after delete): %v", err)
1325 }
1326 after, err := os.ReadFile(filepath.Join(envDir, "book.example.com.env"))
1327 if err != nil {
1328 t.Fatalf("read env file after the delete: %v", err)
1329 }
1330 if bytes.Contains(after, []byte("TENANT=book\n")) || !bytes.Contains(after, []byte("TENANT=none\n")) {
1331 t.Fatalf("the route did not converge back to the bundle alone after the overlay was deleted: %q", after)
1332 }
1333}
1334
1335// Every other test in this file passes a no-op restart callback, so a
1336// regression that made resolving one route's overlay spuriously mark a
1337// different route as changed would still pass the whole suite. This one
1338// records which hosts restart is actually called for. The first pass
1339// converges both routes from nothing and restarts both once, which is
1340// ordinary first-convergence behaviour and not the claim under test; that
1341// recording is discarded before the pass that matters, where only book's
1342// overlay changes and cart's rendered bytes do not move at all.
1343func TestSyncDoesNotRestartAnUnchangedSiblingWhileAnOverlayIsResolved(t *testing.T) {
1344 ctx, s, reg, envDir, priv := syncInstanceKit(t)
1345 if err := SetEnv(ctx, s, "bab", "titogo", map[string]string{"APP_WIDE": "yes", "TENANT": "none"}); err != nil {
1346 t.Fatalf("SetEnv: %v", err)
1347 }
1348 unitRoute(t, reg, "book.example.com", "titogo", "bab", "")
1349 unitRoute(t, reg, "cart.example.com", "titogo", "bab", "")
1350
1351 var restarted []string
1352 record := func(host string) error {
1353 restarted = append(restarted, host)
1354 return nil
1355 }
1356 if err := Sync(ctx, reg, s, priv, envDir, record, nil); err != nil {
1357 t.Fatalf("Sync (baseline): %v", err)
1358 }
1359 restarted = nil
1360
1361 if _, _, err := ApplyInstance(ctx, s, "bab", "titogo", "book.example.com",
1362 map[string]Var{"TENANT": {Kind: "env", Value: "book"}}, nil); err != nil {
1363 t.Fatalf("ApplyInstance: %v", err)
1364 }
1365 if err := Sync(ctx, reg, s, priv, envDir, record, nil); err != nil {
1366 t.Fatalf("Sync (overlay pass): %v", err)
1367 }
1368 if len(restarted) != 1 || restarted[0] != "book.example.com" {
1369 t.Fatalf("restarted = %v, want exactly [book.example.com]: cart has no overlay and its rendered file did not change, so it must not restart", restarted)
1370 }
1371}
1372
1373// The exec mirror of the test above, over bounce rather than restart.
1374func TestSyncExecDoesNotBounceAnUnchangedSiblingWhileAnOverlayIsResolved(t *testing.T) {
1375 ctx, s, reg, dd, privB64, _ := execKit(t)
1376 priv, err := ParsePrivateKey(privB64, "test")
1377 if err != nil {
1378 t.Fatalf("ParsePrivateKey: %v", err)
1379 }
1380 if err := SetEnv(ctx, s, "bab", "titogo", map[string]string{"APP_WIDE": "yes", "TENANT": "none"}); err != nil {
1381 t.Fatalf("SetEnv: %v", err)
1382 }
1383 for _, host := range []string{"book.example.com", "cart.example.com"} {
1384 if err := reg.Upsert(registry.Route{
1385 Host: host, Kind: "instance", App: "titogo", Account: "bab", Status: "ready",
1386 Socket: "/run/carlos/" + host + ".sock",
1387 }); err != nil {
1388 t.Fatalf("Upsert %s: %v", host, err)
1389 }
1390 }
1391
1392 var bounced []string
1393 record := func(host string) error {
1394 bounced = append(bounced, host)
1395 return nil
1396 }
1397 if err := SyncExec(ctx, reg, s, priv, dd, record, "", nil); err != nil {
1398 t.Fatalf("SyncExec (baseline): %v", err)
1399 }
1400 bounced = nil
1401
1402 if _, _, err := ApplyInstance(ctx, s, "bab", "titogo", "book.example.com",
1403 map[string]Var{"TENANT": {Kind: "env", Value: "book"}}, nil); err != nil {
1404 t.Fatalf("ApplyInstance: %v", err)
1405 }
1406 if err := SyncExec(ctx, reg, s, priv, dd, record, "", nil); err != nil {
1407 t.Fatalf("SyncExec (overlay pass): %v", err)
1408 }
1409 if len(bounced) != 1 || bounced[0] != "book.example.com" {
1410 t.Fatalf("bounced = %v, want exactly [book.example.com]: cart has no overlay and its rendered file did not change, so it must not bounce", bounced)
1411 }
1412}
1413