carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package caps
2
3import (
4 "bytes"
5 "errors"
6 "fmt"
7 "os"
8 "path/filepath"
9 "strings"
10 "testing"
11)
12
13// fakeSystemd is a tiny in-memory systemd: it RECORDS every state-changing
14// verb and answers the query seam from the state those verbs left behind. The
15// state tracking is what lets "converge twice, the second pass changes
16// nothing" be asserted against a DAEMON route — the interesting case — rather
17// than only against a route with no unit state to repair.
18type fakeSystemd struct {
19 calls []string
20 active map[string]bool
21 enabled map[string]bool
22 fail map[string]error // "verb unit" -> error
23}
24
25func newFakeSystemd() *fakeSystemd {
26 return &fakeSystemd{active: map[string]bool{}, enabled: map[string]bool{}, fail: map[string]error{}}
27}
28
29func (f *fakeSystemd) run(args ...string) error {
30 call := strings.Join(args, " ")
31 f.calls = append(f.calls, call)
32 if err := f.fail[call]; err != nil {
33 return err
34 }
35 if len(args) < 2 {
36 return nil
37 }
38 switch args[0] {
39 case "start", "restart":
40 f.active[args[1]] = true
41 case "stop":
42 f.active[args[1]] = false
43 case "enable":
44 f.enabled[args[1]] = true
45 case "disable":
46 f.enabled[args[1]] = false
47 }
48 return nil
49}
50
51func (f *fakeSystemd) query(args ...string) bool {
52 if len(args) < 2 {
53 return false
54 }
55 switch args[0] {
56 case "is-active":
57 return f.active[args[1]]
58 case "is-enabled":
59 return f.enabled[args[1]]
60 }
61 return false
62}
63
64func (f *fakeSystemd) reset() { f.calls = nil }
65
66// testOps wires every seam at a temp dir so nothing here touches systemd,
67// /etc, /run or /var/lib.
68func testOps(t *testing.T) (Ops, *fakeSystemd, *[]string, *[]string) {
69 t.Helper()
70 f := newFakeSystemd()
71 var logs []string
72 var users []string
73 o := Ops{
74 EtcDir: t.TempDir(),
75 StateRoot: t.TempDir(),
76 RunDir: t.TempDir(),
77 Systemctl: f.run,
78 Query: f.query,
79 EnsureUser: func(name string) error {
80 users = append(users, name)
81 return nil
82 },
83 Logf: func(format string, a ...any) { logs = append(logs, fmt.Sprintf(format, a...)) },
84 }
85 return o, f, &logs, &users
86}
87
88func keymailPlan(t *testing.T, g Grants) Plan {
89 t.Helper()
90 return Plan{Host: "keymail.dev", App: "keymail", Set: keymailSet(t), Grants: g}
91}
92
93func readFile(t *testing.T, path string) []byte {
94 t.Helper()
95 b, err := os.ReadFile(path)
96 if err != nil {
97 t.Fatalf("read %s: %v", path, err)
98 }
99 return b
100}
101
102func mustNotExist(t *testing.T, path string) {
103 t.Helper()
104 if _, err := os.Lstat(path); !os.IsNotExist(err) {
105 t.Fatalf("%s: want absent, got err=%v", path, err)
106 }
107}
108
109func hasLog(logs []string, substr string) bool {
110 for _, l := range logs {
111 if strings.Contains(l, substr) {
112 return true
113 }
114 }
115 return false
116}
117
118// indexOf returns the position of the first call equal to want, or -1.
119func indexOf(calls []string, want string) int {
120 for i, c := range calls {
121 if c == want {
122 return i
123 }
124 }
125 return -1
126}
127
128// TestGrantAddedWritesTheDropInAndRestarts pins the whole happy path: both
129// drop-ins are the golden bytes, exactly one daemon-reload, the socket
130// sequence in its pinned order (a socket restart under a running service
131// races into EADDRINUSE), and `enable` for BOTH units — bootstrap enables no
132// tenant socket unit, so without it the ListenStream binds nothing after a
133// reboot.
134func TestGrantAddedWritesTheDropInAndRestarts(t *testing.T) {
135 o, f, _, users := testOps(t)
136 p := keymailPlan(t, keymailGrants())
137
138 res, err := Converge(o, p)
139 if err != nil {
140 t.Fatalf("Converge: %v", err)
141 }
142 if !res.Changed {
143 t.Fatal("Changed = false, want true on a first converge")
144 }
145 svcPath, sockPath := DropInPaths(o.EtcDir, p.Host)
146 if got, want := readFile(t, svcPath), readGolden(t, "keymail.service.conf"); !bytes.Equal(got, want) {
147 t.Fatalf("service drop-in:\n got: %q\n want: %q", got, want)
148 }
149 if got, want := readFile(t, sockPath), readGolden(t, "keymail.socket.conf"); !bytes.Equal(got, want) {
150 t.Fatalf("socket drop-in:\n got: %q\n want: %q", got, want)
151 }
152 if len(*users) != 1 || (*users)[0] != "carlos-keymail" {
153 t.Fatalf("EnsureUser calls = %v, want [carlos-keymail]", *users)
154 }
155
156 reloads := 0
157 for _, c := range f.calls {
158 if c == "daemon-reload" {
159 reloads++
160 }
161 }
162 if reloads != 1 {
163 t.Fatalf("daemon-reload x%d, want exactly 1 (calls: %v)", reloads, f.calls)
164 }
165
166 stop := indexOf(f.calls, "stop carlos-app@keymail.dev.service")
167 sock := indexOf(f.calls, "restart carlos-app@keymail.dev.socket")
168 start := indexOf(f.calls, "start carlos-app@keymail.dev.service")
169 if stop < 0 || sock < 0 || start < 0 || !(stop < sock && sock < start) {
170 t.Fatalf("socket ordering: stop=%d socket=%d start=%d, want stop < socket restart < start (calls: %v)", stop, sock, start, f.calls)
171 }
172 if indexOf(f.calls, "enable carlos-app@keymail.dev.service") < 0 {
173 t.Fatalf("no enable for the service unit (calls: %v)", f.calls)
174 }
175 if indexOf(f.calls, "enable carlos-app@keymail.dev.socket") < 0 {
176 t.Fatalf("no enable for the socket unit — the ListenStream would bind nothing after a reboot (calls: %v)", f.calls)
177 }
178 // The reload precedes EVERY unit verb: a restart against configuration
179 // systemd has not re-read is a pass that looks like it worked and changed
180 // nothing.
181 reload := indexOf(f.calls, "daemon-reload")
182 for i, c := range f.calls {
183 verb, _, _ := strings.Cut(c, " ")
184 switch verb {
185 case "restart", "start", "stop", "enable":
186 if i < reload {
187 t.Fatalf("%q ran before the daemon-reload: %v", c, f.calls)
188 }
189 }
190 }
191}
192
193// A failed daemon-reload must not leave the new bytes on disk: the next pass
194// would see no diff, conclude there is nothing to do, and the box would run
195// the old configuration behind a drop-in that reads "converged" forever.
196func TestAFailedReloadRollsTheBytesBackSoTheNextPassRetries(t *testing.T) {
197 o, f, _, _ := testOps(t)
198 p := keymailPlan(t, keymailGrants())
199 f.fail["daemon-reload"] = errors.New("systemd is not running")
200
201 res, err := Converge(o, p)
202 if err == nil {
203 t.Fatal("Converge swallowed a failed daemon-reload")
204 }
205 if res.Changed {
206 t.Fatal("Changed = true after the pass was rolled back")
207 }
208 svcPath, sockPath := DropInPaths(o.EtcDir, p.Host)
209 mustNotExist(t, svcPath)
210 mustNotExist(t, sockPath)
211 for _, c := range f.calls {
212 verb, _, _ := strings.Cut(c, " ")
213 if verb == "restart" || verb == "start" || verb == "stop" {
214 t.Fatalf("unit state changed after a failed reload: %v", f.calls)
215 }
216 }
217
218 // Pass N+1, with a working systemd: the whole change is retried.
219 delete(f.fail, "daemon-reload")
220 f.reset()
221 res, err = Converge(o, p)
222 if err != nil {
223 t.Fatalf("Converge (retry): %v", err)
224 }
225 if !res.Changed {
226 t.Fatal("the retry pass found nothing to do — the rollback did not restore the diff")
227 }
228 if got, want := readFile(t, svcPath), readGolden(t, "keymail.service.conf"); !bytes.Equal(got, want) {
229 t.Fatalf("retry did not converge:\n got: %q\n want: %q", got, want)
230 }
231 if got, want := readFile(t, sockPath), readGolden(t, "keymail.socket.conf"); !bytes.Equal(got, want) {
232 t.Fatalf("retry did not converge the socket half:\n got: %q\n want: %q", got, want)
233 }
234}
235
236// A half-written pass is the same divergence in miniature: one unit
237// converged, the other not, and the disk claiming both are done.
238func TestAPartialWriteRollsBothHalvesBackToPrev(t *testing.T) {
239 o, f, _, _ := testOps(t)
240 p := keymailPlan(t, keymailGrants())
241 if _, err := Converge(o, p); err != nil {
242 t.Fatalf("Converge (first): %v", err)
243 }
244 svcPath, sockPath := DropInPaths(o.EtcDir, p.Host)
245 svcA, sockA := readFile(t, svcPath), readFile(t, sockPath)
246 f.reset()
247
248 // Version B changes both halves, but the socket half cannot be written:
249 // its temp file lands in a directory that has been replaced by... itself,
250 // made unwritable.
251 if err := os.Chmod(filepath.Dir(sockPath), 0o500); err != nil {
252 t.Fatal(err)
253 }
254 t.Cleanup(func() { os.Chmod(filepath.Dir(sockPath), 0o755) })
255 var b Set
256 if err := b.UnmarshalJSON([]byte(`{"v":1,"command":"forward","daemon":true,"runtime_dir":"keymail","host_role":true,"ports":[{"port":587,"name":"submission"}]}`)); err != nil {
257 t.Fatal(err)
258 }
259 p.Set = &b
260 p.Grants = Grants{Ports: map[int]bool{587: true}, HostRole: true}
261
262 res, err := Converge(o, p)
263 if err == nil {
264 t.Skip("this environment allows writing into a 0500 directory (running as root?)")
265 }
266 if res.Changed {
267 t.Fatal("Changed = true after both halves were rolled back")
268 }
269 if got := readFile(t, svcPath); !bytes.Equal(got, svcA) {
270 t.Fatalf("the service half was left mid-change:\n got: %q\n want: %q", got, svcA)
271 }
272 if got := readFile(t, sockPath); !bytes.Equal(got, sockA) {
273 t.Fatalf("the socket half moved:\n got: %q\n want: %q", got, sockA)
274 }
275 for _, c := range f.calls {
276 verb, _, _ := strings.Cut(c, " ")
277 if verb == "restart" || verb == "start" || verb == "stop" {
278 t.Fatalf("unit state changed on a half-written pass: %v", f.calls)
279 }
280 }
281}
282
283// Deviation #4's consequence, made visible: the socket unit is enabled for
284// hibernating routes too (or the ListenStream binds nothing), and a granted
285// public port is then a wake path that does not go through the activator's
286// health gate.
287func TestAGrantedPortOnAHibernatingRouteIsAdvised(t *testing.T) {
288 o, _, logs, _ := testOps(t)
289 var s Set
290 if err := s.UnmarshalJSON([]byte(`{"v":1,"command":"relay","ports":[{"port":25,"name":"mx"}]}`)); err != nil {
291 t.Fatal(err)
292 }
293 p := Plan{Host: "mx.example.com", App: "keymail", Set: &s,
294 Grants: Grants{Ports: map[int]bool{25: true}}, Hibernate: true}
295
296 if _, err := Converge(o, p); err != nil {
297 t.Fatalf("Converge: %v", err)
298 }
299 if !hasLog(*logs, "port:25 is granted on a hibernating route") {
300 t.Fatalf("logs = %v, want the hibernating-port advisory", *logs)
301 }
302 if !hasLog(*logs, "bypassing the activator's health-gated wake") {
303 t.Fatalf("logs = %v, want the advisory to say what the risk is", *logs)
304 }
305
306 // Not advised when the port is merely declared: nothing binds, so there is
307 // no wake path to warn about. Advice that fires on a healthy fleet is
308 // advice nobody reads.
309 o2, _, logs2, _ := testOps(t)
310 q := p
311 q.Host = "mx2.example.com"
312 q.Grants = Grants{}
313 if _, err := Converge(o2, q); err != nil {
314 t.Fatalf("Converge (ungranted): %v", err)
315 }
316 if hasLog(*logs2, "hibernating route") {
317 t.Fatalf("logs = %v, want no advisory for an ungranted port", *logs2)
318 }
319}
320
321// An address-form ListenStream is the same claim: a hand-written drop-in
322// cannot dodge the box-wide port check by spelling :25 differently.
323func TestPortClaimsSeeAddressFormListenStreams(t *testing.T) {
324 for _, form := range []string{"25", "0.0.0.0:25", "[::]:25", "10.0.0.4:25"} {
325 o, _, logs, _ := testOps(t)
326 sockPath := filepath.Join(o.EtcDir, SocketUnit("hand.example.com")+".d", DropInName)
327 if err := os.MkdirAll(filepath.Dir(sockPath), 0o755); err != nil {
328 t.Fatal(err)
329 }
330 if err := os.WriteFile(sockPath, []byte("[Socket]\nListenStream="+form+"\n"), 0o644); err != nil {
331 t.Fatal(err)
332 }
333 if _, err := Converge(o, keymailPlan(t, keymailGrants())); err == nil {
334 t.Fatalf("ListenStream=%s did not count as a claim on :25", form)
335 }
336 if !hasLog(*logs, "port 25 is already claimed by hand.example.com") {
337 t.Fatalf("ListenStream=%s: logs = %v", form, *logs)
338 }
339 }
340 // The template's own unix listener claims no TCP port at all.
341 o, _, _, _ := testOps(t)
342 sockPath := filepath.Join(o.EtcDir, SocketUnit("hand.example.com")+".d", DropInName)
343 if err := os.MkdirAll(filepath.Dir(sockPath), 0o755); err != nil {
344 t.Fatal(err)
345 }
346 if err := os.WriteFile(sockPath, []byte("[Socket]\nListenStream=/run/carlos/apps/hand.example.com.sock\n"), 0o644); err != nil {
347 t.Fatal(err)
348 }
349 if _, err := Converge(o, keymailPlan(t, keymailGrants())); err != nil {
350 t.Fatalf("a unix ListenStream was read as a TCP claim: %v", err)
351 }
352}
353
354// TestGrantRevokedRewritesTheDropIn: a grant is not a promise. Revoking
355// port:25 and host-role removes the socket file entirely and rewrites the
356// service file without its User= lines — and the removal alone is enough to
357// count as Changed.
358func TestGrantRevokedRewritesTheDropIn(t *testing.T) {
359 o, f, _, _ := testOps(t)
360 p := keymailPlan(t, keymailGrants())
361 if _, err := Converge(o, p); err != nil {
362 t.Fatalf("Converge (granted): %v", err)
363 }
364 f.reset()
365
366 p.Grants = Grants{}
367 res, err := Converge(o, p)
368 if err != nil {
369 t.Fatalf("Converge (revoked): %v", err)
370 }
371 if !res.Changed {
372 t.Fatal("Changed = false after a revoke, want true")
373 }
374 svcPath, sockPath := DropInPaths(o.EtcDir, p.Host)
375 mustNotExist(t, sockPath)
376 svc := string(readFile(t, svcPath))
377 for _, gone := range []string{"DynamicUser=no", "User=carlos-keymail", "Group=carlos-keymail", "IPAddressAllow"} {
378 if strings.Contains(svc, gone) {
379 t.Fatalf("service drop-in still holds %q after the revoke:\n%s", gone, svc)
380 }
381 }
382 for _, kept := range []string{"ExecStart=/opt/carlos/live/%i relay", "RuntimeDirectory=keymail", "TasksMax=1024"} {
383 if !strings.Contains(svc, kept) {
384 t.Fatalf("service drop-in lost the self-service %q:\n%s", kept, svc)
385 }
386 }
387 reloads := 0
388 for _, c := range f.calls {
389 if c == "daemon-reload" {
390 reloads++
391 }
392 }
393 if reloads != 1 {
394 t.Fatalf("daemon-reload x%d, want exactly 1 (calls: %v)", reloads, f.calls)
395 }
396 if res.Prev.Socket == nil || !res.Prev.SocketHad {
397 t.Fatal("Prev did not capture the socket drop-in that was removed")
398 }
399 // A grant is not a promise (spec §7): the socket unit must be restarted
400 // so systemd actually drops :25. Removing the file and reloading is not
401 // enough — the running socket unit is still holding the listener.
402 stop := indexOf(f.calls, "stop carlos-app@keymail.dev.service")
403 sock := indexOf(f.calls, "restart carlos-app@keymail.dev.socket")
404 start := indexOf(f.calls, "start carlos-app@keymail.dev.service")
405 if stop < 0 || sock < 0 || start < 0 || !(stop < sock && sock < start) {
406 t.Fatalf("a revoked port did not go through the socket sequence: %v", f.calls)
407 }
408}
409
410// TestUnreadableManifestLeavesDropInsAlone is the three-state rule, and the
411// reason it exists: a bucket hiccup must not strip :25 off a running mail
412// server. Nil Set = leave-alone, not removal.
413func TestUnreadableManifestLeavesDropInsAlone(t *testing.T) {
414 o, f, logs, _ := testOps(t)
415 p := keymailPlan(t, keymailGrants())
416 if _, err := Converge(o, p); err != nil {
417 t.Fatalf("Converge (staged): %v", err)
418 }
419 svcPath, sockPath := DropInPaths(o.EtcDir, p.Host)
420 svcBefore, sockBefore := readFile(t, svcPath), readFile(t, sockPath)
421 f.reset()
422 *logs = nil
423
424 p.Set = nil
425 res, err := Converge(o, p)
426 if err != nil {
427 t.Fatalf("Converge (nil Set) returned an error: %v — leave-alone is not a failure", err)
428 }
429 if res.Changed {
430 t.Fatal("Changed = true on a nil Set")
431 }
432 if len(f.calls) != 0 {
433 t.Fatalf("systemctl calls on a nil Set: %v", f.calls)
434 }
435 if !bytes.Equal(readFile(t, svcPath), svcBefore) || !bytes.Equal(readFile(t, sockPath), sockBefore) {
436 t.Fatal("a nil Set changed the drop-ins")
437 }
438 if len(*logs) != 1 || !strings.Contains((*logs)[0], "leaving its drop-ins exactly as they are") {
439 t.Fatalf("logs = %v, want exactly one leave-alone line", *logs)
440 }
441}
442
443// TestInvalidStagedManifestRefusesTheRouteAndLeavesDropInsAlone: a
444// platform-binary rollback can meet an already-staged version whose
445// capability block this binary does not understand (ensureStaged short-
446// circuits on a staged artifact and never re-reads its manifest). That falls
447// the same way as an unreadable manifest — nothing removed — but it also
448// returns an error, so a version-change caller refuses the version rather
449// than pointing live at something it cannot materialize.
450func TestInvalidStagedManifestRefusesTheRouteAndLeavesDropInsAlone(t *testing.T) {
451 o, f, logs, users := testOps(t)
452 p := keymailPlan(t, keymailGrants())
453 if _, err := Converge(o, p); err != nil {
454 t.Fatalf("Converge (valid): %v", err)
455 }
456 svcPath, sockPath := DropInPaths(o.EtcDir, p.Host)
457 svcBefore, sockBefore := readFile(t, svcPath), readFile(t, sockPath)
458 f.reset()
459 *logs = nil
460
461 // A Set from a manifest a NEWER binary wrote: v2 is outside 1..Version.
462 // It declares host_role, which is granted — so if the re-validation is
463 // not the FIRST thing after the leave-alone branch, this refusal happens
464 // only at render time, after the pass has already minted a system account
465 // for a version it cannot materialize.
466 p.Set = &Set{V: Version + 1, Command: "relay", Daemon: true, HostRole: true}
467 res, err := Converge(o, p)
468 if err == nil {
469 t.Fatal("Converge accepted a capability block this binary does not speak")
470 }
471 if res.Changed {
472 t.Fatal("Changed = true on a refused route")
473 }
474 if len(f.calls) != 0 {
475 t.Fatalf("systemctl calls on a refused route: %v", f.calls)
476 }
477 if !bytes.Equal(readFile(t, svcPath), svcBefore) || !bytes.Equal(readFile(t, sockPath), sockBefore) {
478 t.Fatal("a refused route's drop-ins were modified")
479 }
480 if len(*users) != 1 { // the first, valid converge — and nothing since
481 t.Fatalf("EnsureUser calls = %v, want only the one from the valid pass", *users)
482 }
483 if !hasLog(*logs, "update the platform binary") {
484 t.Fatalf("logs = %v, want the update-the-platform-binary line", *logs)
485 }
486}
487
488// TestHandWrittenDropInIsNeverOverwritten: a file at a generated path without
489// the marker is never overwritten and never deleted — reported and skipped —
490// while the OTHER unit's half still converges.
491func TestHandWrittenDropInIsNeverOverwritten(t *testing.T) {
492 o, _, logs, _ := testOps(t)
493 p := keymailPlan(t, keymailGrants())
494 svcPath, sockPath := DropInPaths(o.EtcDir, p.Host)
495 hand := []byte("[Service]\nEnvironment=HAND=written\n")
496 if err := os.MkdirAll(filepath.Dir(svcPath), 0o755); err != nil {
497 t.Fatal(err)
498 }
499 if err := os.WriteFile(svcPath, hand, 0o644); err != nil {
500 t.Fatal(err)
501 }
502
503 if _, err := Converge(o, p); err != nil {
504 t.Fatalf("Converge: %v", err)
505 }
506 if got := readFile(t, svcPath); !bytes.Equal(got, hand) {
507 t.Fatalf("hand-written drop-in was modified:\n got: %q\nwant: %q", got, hand)
508 }
509 if !hasLog(*logs, "never overwritten, never removed") {
510 t.Fatalf("logs = %v, want the hand-edited report", *logs)
511 }
512 if got, want := readFile(t, sockPath), readGolden(t, "keymail.socket.conf"); !bytes.Equal(got, want) {
513 t.Fatalf("the other unit's half did not converge:\n got: %q\nwant: %q", got, want)
514 }
515
516 // And it is not restorable either: this pass never wrote it, so it is not
517 // this pass's to put back — or to delete on a rollback.
518 res, _ := Converge(o, p)
519 if res.Prev.ServiceHad {
520 t.Fatal("Snapshot claimed a hand-written file as its own")
521 }
522 if err := Restore(o, res.Prev); err != nil {
523 t.Fatalf("Restore: %v", err)
524 }
525 if got := readFile(t, svcPath); !bytes.Equal(got, hand) {
526 t.Fatal("Restore deleted a hand-written drop-in")
527 }
528}
529
530// TestUnchangedPassWritesNothing is the determinism promise, asserted against
531// a DAEMON route with a socket — the case with the most unit state to
532// repair. Once the box is converged, the hourly pass changes nothing at all.
533func TestUnchangedPassWritesNothing(t *testing.T) {
534 o, f, logs, _ := testOps(t)
535 p := keymailPlan(t, keymailGrants())
536 if _, err := Converge(o, p); err != nil {
537 t.Fatalf("Converge (first): %v", err)
538 }
539 svcPath, sockPath := DropInPaths(o.EtcDir, p.Host)
540 svcInfo, err := os.Stat(svcPath)
541 if err != nil {
542 t.Fatal(err)
543 }
544 f.reset()
545 *logs = nil
546
547 res, err := Converge(o, p)
548 if err != nil {
549 t.Fatalf("Converge (second): %v", err)
550 }
551 if res.Changed {
552 t.Fatal("Changed = true on an unchanged pass")
553 }
554 if len(f.calls) != 0 {
555 t.Fatalf("an unchanged pass made systemctl calls: %v", f.calls)
556 }
557 if len(*logs) != 0 {
558 t.Fatalf("an unchanged pass logged: %v", *logs)
559 }
560 // Not even a rewrite with identical bytes: the mtime is untouched.
561 after, err := os.Stat(svcPath)
562 if err != nil {
563 t.Fatal(err)
564 }
565 if !after.ModTime().Equal(svcInfo.ModTime()) {
566 t.Fatal("the service drop-in was rewritten with identical bytes")
567 }
568 if got, want := readFile(t, sockPath), readGolden(t, "keymail.socket.conf"); !bytes.Equal(got, want) {
569 t.Fatalf("socket drop-in drifted on a no-op pass:\n got: %q\nwant: %q", got, want)
570 }
571 // The mode is part of the contract systemd reads.
572 if perm := svcInfo.Mode().Perm(); perm != 0o644 {
573 t.Fatalf("service drop-in mode = %o, want 0644", perm)
574 }
575}
576
577// TestDaemonPlusHibernateIsRefused: a daemon the platform is asked to keep
578// running and to stop when idle is a contradiction, and for these apps
579// hibernation means an MX that sleeps. Refused authoritatively here, with the
580// operator's exact fix in the line.
581func TestDaemonPlusHibernateIsRefused(t *testing.T) {
582 o, f, logs, users := testOps(t)
583 p := keymailPlan(t, keymailGrants())
584 p.Hibernate = true
585
586 res, err := Converge(o, p)
587 if err == nil {
588 t.Fatal("Converge accepted daemon + hibernate")
589 }
590 if res.Changed {
591 t.Fatal("Changed = true on a refusal")
592 }
593 if len(f.calls) != 0 {
594 t.Fatalf("systemctl calls on a refusal: %v", f.calls)
595 }
596 if len(*users) != 0 {
597 t.Fatalf("a refused plan created users: %v", *users)
598 }
599 svcPath, sockPath := DropInPaths(o.EtcDir, p.Host)
600 mustNotExist(t, svcPath)
601 mustNotExist(t, sockPath)
602 if !hasLog(*logs, "carlos route --host keymail.dev --hibernate=false") {
603 t.Fatalf("logs = %v, want the --hibernate=false fix", *logs)
604 }
605}
606
607// TestHostRoleFlipRefusedWhenTheStateDirIsTheOtherShape is spec §4b's pinned
608// sharp edge: with DynamicUser=yes the state dir is a SYMLINK into
609// /var/lib/private, and a static User= needs a real directory. The platform
610// refuses in place; the operator does the stop → move → start.
611func TestHostRoleFlipRefusedWhenTheStateDirIsTheOtherShape(t *testing.T) {
612 o, f, logs, users := testOps(t)
613 p := keymailPlan(t, keymailGrants())
614 if err := os.Symlink(filepath.Join(o.StateRoot, "private", "carlos-app", p.Host), filepath.Join(o.StateRoot, p.Host)); err != nil {
615 t.Fatal(err)
616 }
617
618 res, err := Converge(o, p)
619 if err == nil {
620 t.Fatal("Converge flipped host_role on over a DynamicUser state dir")
621 }
622 if res.Changed {
623 t.Fatal("Changed = true on a refusal")
624 }
625 if len(f.calls) != 0 {
626 t.Fatalf("systemctl calls on a refusal: %v", f.calls)
627 }
628 if len(*users) != 0 {
629 t.Fatalf("a refused plan created users: %v", *users)
630 }
631 svcPath, _ := DropInPaths(o.EtcDir, p.Host)
632 mustNotExist(t, svcPath)
633 if !hasLog(*logs, "refused in place") {
634 t.Fatalf("logs = %v, want a refused-in-place line naming the operator's move", *logs)
635 }
636}
637
638// The OFF direction of the same flip: once THIS package's drop-in has put the
639// unit on a static user, dropping host_role would hand the template's
640// DynamicUser=yes an existing real directory it cannot use.
641func TestHostRoleFlipOffRefusedWhenTheStateDirIsAlreadyStatic(t *testing.T) {
642 o, _, logs, _ := testOps(t)
643 p := keymailPlan(t, keymailGrants())
644 if _, err := Converge(o, p); err != nil {
645 t.Fatalf("Converge (host_role on): %v", err)
646 }
647 if err := os.Mkdir(filepath.Join(o.StateRoot, p.Host), 0o700); err != nil {
648 t.Fatal(err)
649 }
650 svcPath, _ := DropInPaths(o.EtcDir, p.Host)
651 before := readFile(t, svcPath)
652 *logs = nil
653
654 p.Grants = Grants{Ports: map[int]bool{25: true}} // host-role revoked
655 if _, err := Converge(o, p); err == nil {
656 t.Fatal("Converge flipped host_role off over a static state dir")
657 }
658 if got := readFile(t, svcPath); !bytes.Equal(got, before) {
659 t.Fatal("a refused flip rewrote the service drop-in")
660 }
661 if !hasLog(*logs, "refused in place") {
662 t.Fatalf("logs = %v, want a refused-in-place line", *logs)
663 }
664}
665
666// A real state directory that no drop-in of OURS created is not a flip — it
667// is a legacy hand-provisioned app — and must not freeze an unrelated route's
668// capabilities forever.
669func TestPreExistingStateDirDoesNotBlockARouteThatNeverHadHostRole(t *testing.T) {
670 o, _, _, _ := testOps(t)
671 var s Set
672 if err := s.UnmarshalJSON([]byte(`{"v":1,"command":"relay","daemon":true}`)); err != nil {
673 t.Fatal(err)
674 }
675 p := Plan{Host: "legacy.example.com", App: "legacy", Set: &s}
676 if err := os.Mkdir(filepath.Join(o.StateRoot, p.Host), 0o700); err != nil {
677 t.Fatal(err)
678 }
679 if _, err := Converge(o, p); err != nil {
680 t.Fatalf("Converge refused a route over a state dir it never created: %v", err)
681 }
682 svcPath, _ := DropInPaths(o.EtcDir, p.Host)
683 if !strings.Contains(string(readFile(t, svcPath)), "ExecStart=/opt/carlos/live/%i relay") {
684 t.Fatal("the route did not converge")
685 }
686}
687
688// TestRestorePutsThePreviousDropInsBack is the drop-in half of adoptHost's
689// restart-failure rollback (the symlink half is release.PointLive's).
690func TestRestorePutsThePreviousDropInsBack(t *testing.T) {
691 o, f, _, _ := testOps(t)
692 p := keymailPlan(t, keymailGrants())
693 if _, err := Converge(o, p); err != nil {
694 t.Fatalf("Converge A: %v", err)
695 }
696 svcPath, sockPath := DropInPaths(o.EtcDir, p.Host)
697 svcA, sockA := readFile(t, svcPath), readFile(t, sockPath)
698
699 // Version B: the app dropped `ports` and `children` from its manifest.
700 var b Set
701 if err := b.UnmarshalJSON([]byte(`{"v":1,"command":"relay","daemon":true,"runtime_dir":"keymail","host_role":true}`)); err != nil {
702 t.Fatal(err)
703 }
704 p.Set = &b
705 resB, err := Converge(o, p)
706 if err != nil {
707 t.Fatalf("Converge B: %v", err)
708 }
709 if !resB.Changed {
710 t.Fatal("B did not change anything")
711 }
712 mustNotExist(t, sockPath)
713 f.reset()
714
715 if err := Restore(o, resB.Prev); err != nil {
716 t.Fatalf("Restore: %v", err)
717 }
718 if got := readFile(t, svcPath); !bytes.Equal(got, svcA) {
719 t.Fatalf("service drop-in after Restore:\n got: %q\n want: %q", got, svcA)
720 }
721 if got := readFile(t, sockPath); !bytes.Equal(got, sockA) {
722 t.Fatalf("socket drop-in after Restore:\n got: %q\n want: %q", got, sockA)
723 }
724 reload := indexOf(f.calls, "daemon-reload")
725 if reload < 0 {
726 t.Fatalf("Restore did not daemon-reload (calls: %v)", f.calls)
727 }
728 // Bytes are not a binding. A restored ListenStream systemd never re-binds
729 // restores nothing — the production MX would stay off :25 after a rollback
730 // — so the socket half goes back through the pinned sequence, after the
731 // reload.
732 stop := indexOf(f.calls, "stop carlos-app@keymail.dev.service")
733 sock := indexOf(f.calls, "restart carlos-app@keymail.dev.socket")
734 start := indexOf(f.calls, "start carlos-app@keymail.dev.service")
735 if stop < 0 || sock < 0 || start < 0 || !(reload < stop && stop < sock && sock < start) {
736 t.Fatalf("Restore did not re-bind the restored listener in order: %v", f.calls)
737 }
738}
739
740// The mirror case, and the one a bytes-only Restore gets exactly as wrong:
741// version A had NO socket drop-in, B added one and bound :25, the restart
742// failed, and the rollback must both delete the file and give the port back.
743// Left bound, it is a public listener held by a binary that never asked for
744// it and a claim that blocks the next route.
745func TestRestoreReleasesAPortThatTheRolledBackVersionAdded(t *testing.T) {
746 o, f, _, _ := testOps(t)
747 var a Set
748 if err := a.UnmarshalJSON([]byte(`{"v":1,"command":"relay","daemon":true}`)); err != nil {
749 t.Fatal(err)
750 }
751 p := Plan{Host: "keymail.dev", App: "keymail", Set: &a, Grants: keymailGrants()}
752 if _, err := Converge(o, p); err != nil {
753 t.Fatalf("Converge A: %v", err)
754 }
755 _, sockPath := DropInPaths(o.EtcDir, p.Host)
756 mustNotExist(t, sockPath)
757
758 p.Set = keymailSet(t) // B declares ports, and :25 is granted
759 resB, err := Converge(o, p)
760 if err != nil {
761 t.Fatalf("Converge B: %v", err)
762 }
763 if resB.Prev.SocketHad {
764 t.Fatal("the snapshot claims a socket drop-in that never existed")
765 }
766 readFile(t, sockPath) // B really did bind it
767 f.reset()
768 // The state a rollback actually happens in: the restart that triggered it
769 // failed, so the service is DOWN. Whether it is supposed to be running is
770 // not something the disk can answer, which is why the snapshot carries it
771 // — without that, the sequence would skip the stop/start pair and leave a
772 // daemon stopped on the version it was just rolled back to.
773 f.active[ServiceUnit(p.Host)] = false
774
775 if err := Restore(o, resB.Prev); err != nil {
776 t.Fatalf("Restore: %v", err)
777 }
778 mustNotExist(t, sockPath)
779 reload := indexOf(f.calls, "daemon-reload")
780 sock := indexOf(f.calls, "restart carlos-app@keymail.dev.socket")
781 if reload < 0 || sock < 0 || reload > sock {
782 t.Fatalf("Restore removed the drop-in but never released the port: %v", f.calls)
783 }
784 if indexOf(f.calls, "start carlos-app@keymail.dev.service") < 0 || !f.active[ServiceUnit(p.Host)] {
785 t.Fatalf("Restore left the daemon stopped: %v", f.calls)
786 }
787 // And the box no longer counts it as a claim, so the next route can have it.
788 if ports, _ := scanClaims(o.EtcDir, "other.example.com"); ports[25] != "" {
789 t.Fatalf("port 25 is still claimed by %q after the rollback", ports[25])
790 }
791}
792
793// TestCheckClaimsRefusesAPortHeldByTwoRoutesAndARuntimeDirCollision: box-wide
794// exclusivity, resolved before anything is written, and deterministic on ties
795// so a conflict does not flap between passes.
796func TestCheckClaimsRefusesAPortHeldByTwoRoutesAndARuntimeDirCollision(t *testing.T) {
797 mx := func(host string) Plan {
798 var s Set
799 if err := s.UnmarshalJSON([]byte(`{"v":1,"ports":[{"port":25,"name":"mx"}]}`)); err != nil {
800 t.Fatal(err)
801 }
802 return Plan{Host: host, App: "keymail", Set: &s, Grants: Grants{Ports: map[int]bool{25: true}}}
803 }
804 rd := func(host string) Plan {
805 var s Set
806 if err := s.UnmarshalJSON([]byte(`{"v":1,"runtime_dir":"keymail"}`)); err != nil {
807 t.Fatal(err)
808 }
809 return Plan{Host: host, App: "keymail", Set: &s}
810 }
811
812 for i := 0; i < 5; i++ { // deterministic across repeated runs, both input orders
813 plans := []Plan{mx("b.example.com"), mx("a.example.com"), rd("d.example.com"), rd("c.example.com")}
814 if i%2 == 1 {
815 plans = []Plan{plans[1], plans[0], plans[3], plans[2]}
816 }
817 refused := CheckClaims(plans)
818 if len(refused) != 2 {
819 t.Fatalf("refused %d routes, want 2: %v", len(refused), refused)
820 }
821 if refused["a.example.com"] != nil || refused["c.example.com"] != nil {
822 t.Fatalf("the lexicographically-first host lost its claim: %v", refused)
823 }
824 if refused["b.example.com"] == nil || !strings.Contains(refused["b.example.com"].Error(), "a.example.com") {
825 t.Fatalf("port conflict not refused against the holder: %v", refused["b.example.com"])
826 }
827 if refused["d.example.com"] == nil || !strings.Contains(refused["d.example.com"].Error(), "c.example.com") {
828 t.Fatalf("runtime_dir conflict not refused against the holder: %v", refused["d.example.com"])
829 }
830 }
831}
832
833// An UNGRANTED port claims nothing: nothing materializes for it, so it cannot
834// lock another route out of the port it was actually granted.
835func TestCheckClaimsIgnoresUngrantedPortsAndUnresolvedManifests(t *testing.T) {
836 var s Set
837 if err := s.UnmarshalJSON([]byte(`{"v":1,"ports":[{"port":25,"name":"mx"}]}`)); err != nil {
838 t.Fatal(err)
839 }
840 plans := []Plan{
841 {Host: "a.example.com", App: "keymail", Set: &s}, // declared, not granted
842 {Host: "b.example.com", App: "keymail", Set: &s, Grants: Grants{Ports: map[int]bool{25: true}}}, // granted
843 {Host: "c.example.com", App: "keymail", Set: nil}, // leave-alone claims nothing
844 }
845 if refused := CheckClaims(plans); len(refused) != 0 {
846 t.Fatalf("refused %v, want none", refused)
847 }
848}
849
850// TestPortClaimedByAnotherRoutesDropInIsRefused covers the single-route
851// version-change path, which never sees the other plans: the claim is read
852// off what is already written on the box, so a stale drop-in for a route that
853// has left the registry still holds its port.
854func TestPortClaimedByAnotherRoutesDropInIsRefused(t *testing.T) {
855 o, f, logs, _ := testOps(t)
856 first := keymailPlan(t, keymailGrants())
857 if _, err := Converge(o, first); err != nil {
858 t.Fatalf("Converge (first): %v", err)
859 }
860 f.reset()
861 *logs = nil
862
863 // A second MX-shaped route with NO runtime_dir, so the port claim is the
864 // only thing that can refuse it.
865 var s Set
866 if err := s.UnmarshalJSON([]byte(`{"v":1,"command":"relay","daemon":true,"ports":[{"port":25,"name":"mx"}]}`)); err != nil {
867 t.Fatal(err)
868 }
869 second := Plan{Host: "mx2.example.com", App: "keymail", Set: &s, Grants: Grants{Ports: map[int]bool{25: true}}}
870 res, err := Converge(o, second)
871 if err == nil {
872 t.Fatal("two routes were allowed to claim :25 on one box")
873 }
874 if res.Changed {
875 t.Fatal("Changed = true on a refusal")
876 }
877 if len(f.calls) != 0 {
878 t.Fatalf("systemctl calls on a refusal: %v", f.calls)
879 }
880 svcPath, sockPath := DropInPaths(o.EtcDir, second.Host)
881 mustNotExist(t, svcPath)
882 mustNotExist(t, sockPath)
883 if !hasLog(*logs, "port 25 is already claimed by keymail.dev") {
884 t.Fatalf("logs = %v, want the port-conflict line naming the port and the holder", *logs)
885 }
886
887 // The same route reconverging keeps its own claim: it is not in conflict
888 // with itself.
889 if _, err := Converge(o, first); err != nil {
890 t.Fatalf("the holder refused its own claim: %v", err)
891 }
892}
893
894// The same disk-read claim check for runtime_dir: the second app to claim
895// `keymail` is refused rather than handed a directory another app's sockets
896// live in (spec §7).
897func TestRuntimeDirClaimedByAnotherRoutesDropInIsRefused(t *testing.T) {
898 o, f, logs, _ := testOps(t)
899 if _, err := Converge(o, keymailPlan(t, keymailGrants())); err != nil {
900 t.Fatalf("Converge (first): %v", err)
901 }
902 f.reset()
903 *logs = nil
904
905 var s Set
906 if err := s.UnmarshalJSON([]byte(`{"v":1,"command":"serve","runtime_dir":"keymail"}`)); err != nil {
907 t.Fatal(err)
908 }
909 second := Plan{Host: "other.example.com", App: "other", Set: &s}
910 if _, err := Converge(o, second); err == nil {
911 t.Fatal("two routes were allowed to claim /run/keymail on one box")
912 }
913 if len(f.calls) != 0 {
914 t.Fatalf("systemctl calls on a refusal: %v", f.calls)
915 }
916 svcPath, _ := DropInPaths(o.EtcDir, second.Host)
917 mustNotExist(t, svcPath)
918 if !hasLog(*logs, `runtime_dir "keymail" is already claimed by keymail.dev`) {
919 t.Fatalf("logs = %v, want the runtime_dir-conflict line naming the holder", *logs)
920 }
921}
922
923// A hand-written drop-in at another host's generated path still holds its
924// port: this pass will never rewrite it, so systemd applies it, so the claim
925// is real. Fail closed — the alternative is two units racing for :25.
926func TestAHandWrittenDropInStillHoldsItsPortAgainstAnotherRoute(t *testing.T) {
927 o, _, logs, _ := testOps(t)
928 svcPath, _ := DropInPaths(o.EtcDir, "hand.example.com")
929 if err := os.MkdirAll(filepath.Dir(svcPath), 0o755); err != nil {
930 t.Fatal(err)
931 }
932 sockPath := filepath.Join(o.EtcDir, SocketUnit("hand.example.com")+".d", DropInName)
933 if err := os.MkdirAll(filepath.Dir(sockPath), 0o755); err != nil {
934 t.Fatal(err)
935 }
936 if err := os.WriteFile(sockPath, []byte("[Socket]\nListenStream=25\n"), 0o644); err != nil {
937 t.Fatal(err)
938 }
939
940 if _, err := Converge(o, keymailPlan(t, keymailGrants())); err == nil {
941 t.Fatal("a route was granted a port a hand-written drop-in already binds")
942 }
943 if !hasLog(*logs, "port 25 is already claimed by hand.example.com") {
944 t.Fatalf("logs = %v, want the port-conflict line", *logs)
945 }
946}
947
948// Changed is true when only a REMOVAL happened: the caller's rollback and the
949// pass's own daemon-reload both hang off it, and "the box changed" is as true
950// of a deletion as of a write.
951func TestChangedIsTrueWhenOnlyARemovalHappened(t *testing.T) {
952 o, f, _, _ := testOps(t)
953 p := keymailPlan(t, keymailGrants())
954 if _, err := Converge(o, p); err != nil {
955 t.Fatalf("Converge (first): %v", err)
956 }
957 svcPath, sockPath := DropInPaths(o.EtcDir, p.Host)
958 f.reset()
959
960 // A new version declaring only self-service, directive-free capabilities:
961 // both halves render nil, so both generated files go away.
962 var s Set
963 if err := s.UnmarshalJSON([]byte(`{"v":1,"daemon":true,"data":{"replicate":"platform","dbs":["registry.db"]}}`)); err != nil {
964 t.Fatal(err)
965 }
966 p.Set = &s
967 res, err := Converge(o, p)
968 if err != nil {
969 t.Fatalf("Converge (removal): %v", err)
970 }
971 if !res.Changed {
972 t.Fatal("Changed = false when both drop-ins were removed")
973 }
974 mustNotExist(t, svcPath)
975 mustNotExist(t, sockPath)
976 if indexOf(f.calls, "daemon-reload") < 0 {
977 t.Fatalf("a removal did not daemon-reload: %v", f.calls)
978 }
979}
980
981// The /run existence check is the SECOND runtime_dir guard: the static
982// reserved list is finite, and a box can have anything under /run that no
983// list will ever enumerate.
984func TestRuntimeDirRefusedWhenRunPathAlreadyExists(t *testing.T) {
985 o, f, logs, _ := testOps(t)
986 p := keymailPlan(t, keymailGrants())
987 if err := os.Mkdir(filepath.Join(o.RunDir, "keymail"), 0o755); err != nil {
988 t.Fatal(err)
989 }
990
991 if _, err := Converge(o, p); err == nil {
992 t.Fatal("Converge accepted a runtime_dir whose /run path already exists")
993 }
994 if len(f.calls) != 0 {
995 t.Fatalf("systemctl calls on a refusal: %v", f.calls)
996 }
997 svcPath, _ := DropInPaths(o.EtcDir, p.Host)
998 mustNotExist(t, svcPath)
999 if !hasLog(*logs, "is not this route's own claim") {
1000 t.Fatalf("logs = %v, want the /run existence refusal", *logs)
1001 }
1002}
1003
1004// ...and it must not refuse a route its OWN previous pass created the
1005// directory for, or a converged box would refuse itself every hour.
1006func TestRuntimeDirAcceptsThisRoutesOwnPriorClaim(t *testing.T) {
1007 o, _, _, _ := testOps(t)
1008 p := keymailPlan(t, keymailGrants())
1009 if _, err := Converge(o, p); err != nil {
1010 t.Fatalf("Converge (first): %v", err)
1011 }
1012 // systemd creates /run/keymail once the unit starts.
1013 if err := os.Mkdir(filepath.Join(o.RunDir, "keymail"), 0o755); err != nil {
1014 t.Fatal(err)
1015 }
1016 if _, err := Converge(o, p); err != nil {
1017 t.Fatalf("Converge refused the route its own RuntimeDirectory belongs to: %v", err)
1018 }
1019}
1020
1021// TestHostWithASeparatorIsRefused: a registry row is trusted at the same
1022// level as live/<host>, but this one becomes a filename under
1023// /etc/systemd/system, written as root (appconfig.syncHost's guard, same
1024// direction).
1025func TestHostWithASeparatorIsRefused(t *testing.T) {
1026 o, f, _, _ := testOps(t)
1027 for _, host := range []string{"", ".", "..", "../../etc/passwd", "a/b", "sub/../keymail.dev"} {
1028 p := keymailPlan(t, keymailGrants())
1029 p.Host = host
1030 if _, err := Converge(o, p); err == nil {
1031 t.Fatalf("Converge accepted host %q", host)
1032 }
1033 if err := Restore(o, Snapshot{Host: host}); err == nil {
1034 t.Fatalf("Restore accepted host %q", host)
1035 }
1036 if err := Restart(o, p); err == nil {
1037 t.Fatalf("Restart accepted host %q", host)
1038 }
1039 }
1040 if len(f.calls) != 0 {
1041 t.Fatalf("a refused host reached systemctl: %v", f.calls)
1042 }
1043 ents, err := os.ReadDir(o.EtcDir)
1044 if err != nil {
1045 t.Fatal(err)
1046 }
1047 if len(ents) != 0 {
1048 t.Fatalf("a refused host wrote into EtcDir: %v", ents)
1049 }
1050}
1051
1052// TestSocketRestartFailureStillStartsTheService: the service must never be
1053// left stopped because the socket failed to come back — and the error must
1054// still be returned, so the caller's rollback fires.
1055func TestSocketRestartFailureStillStartsTheService(t *testing.T) {
1056 o, f, _, _ := testOps(t)
1057 p := keymailPlan(t, keymailGrants())
1058 f.fail["restart carlos-app@keymail.dev.socket"] = errors.New("Address already in use")
1059
1060 res, err := Converge(o, p)
1061 if err == nil {
1062 t.Fatal("Converge swallowed a failed socket restart")
1063 }
1064 if !strings.Contains(err.Error(), "Address already in use") {
1065 t.Fatalf("error = %v, want the socket failure", err)
1066 }
1067 // systemd's listener no longer matches the bytes, so the bytes go back:
1068 // otherwise the next pass finds no diff and never retries, and the box
1069 // sits on a drop-in that reads "converged" forever.
1070 if res.Changed {
1071 t.Fatal("Changed = true, but the failed pass was rolled back")
1072 }
1073 svcPath, sockPath := DropInPaths(o.EtcDir, p.Host)
1074 mustNotExist(t, svcPath)
1075 mustNotExist(t, sockPath)
1076 stop := indexOf(f.calls, "stop carlos-app@keymail.dev.service")
1077 start := indexOf(f.calls, "start carlos-app@keymail.dev.service")
1078 if stop < 0 || start < 0 || start < stop {
1079 t.Fatalf("the service was left stopped after a failed socket restart: %v", f.calls)
1080 }
1081 if !f.active[ServiceUnit(p.Host)] {
1082 t.Fatalf("the service is not running after the sequence: %v", f.calls)
1083 }
1084
1085 // The same failure on a NON-daemon route, where no enable/start repair
1086 // exists to paper over it: the start can only have come from the sequence
1087 // itself. This is the assertion that pins "the start is attempted even
1088 // when the socket verb failed" rather than merely observing a start.
1089 o2, f2, _, _ := testOps(t)
1090 var s Set
1091 if err := s.UnmarshalJSON([]byte(`{"v":1,"command":"relay","ports":[{"port":25,"name":"mx"}]}`)); err != nil {
1092 t.Fatal(err)
1093 }
1094 q := Plan{Host: "mx.example.com", App: "keymail", Set: &s, Grants: Grants{Ports: map[int]bool{25: true}}}
1095 f2.active[ServiceUnit(q.Host)] = true // running, so the sequence stops it
1096 f2.fail["restart carlos-app@mx.example.com.socket"] = errors.New("Address already in use")
1097
1098 if _, err := Converge(o2, q); err == nil {
1099 t.Fatal("Converge swallowed a failed socket restart on a non-daemon route")
1100 }
1101 stop = indexOf(f2.calls, "stop carlos-app@mx.example.com.service")
1102 start = indexOf(f2.calls, "start carlos-app@mx.example.com.service")
1103 if stop < 0 || start < 0 || start < stop {
1104 t.Fatalf("a non-daemon service was left stopped after a failed socket restart: %v", f2.calls)
1105 }
1106 if !f2.active[ServiceUnit(q.Host)] {
1107 t.Fatalf("the service is not running after the sequence: %v", f2.calls)
1108 }
1109}
1110
1111// EnsureUser failing refuses the WHOLE plan: writing User=carlos-keymail for
1112// a user that does not exist gives systemd a unit that fails to start, which
1113// Restart=always then spins.
1114func TestEnsureUserFailureRefusesThePlan(t *testing.T) {
1115 o, f, logs, _ := testOps(t)
1116 o.EnsureUser = func(string) error { return errors.New("useradd: no free uid") }
1117 p := keymailPlan(t, keymailGrants())
1118
1119 if _, err := Converge(o, p); err == nil {
1120 t.Fatal("Converge wrote a User= line for a user it could not create")
1121 }
1122 svcPath, sockPath := DropInPaths(o.EtcDir, p.Host)
1123 mustNotExist(t, svcPath)
1124 mustNotExist(t, sockPath)
1125 if len(f.calls) != 0 {
1126 t.Fatalf("systemctl calls after a user-creation failure: %v", f.calls)
1127 }
1128 if !hasLog(*logs, "refusing the whole plan") {
1129 t.Fatalf("logs = %v, want the refusal", *logs)
1130 }
1131}
1132
1133// The user is created only when host_role is BOTH declared and granted — an
1134// ungranted declaration must not mint a system account.
1135func TestUngrantedHostRoleCreatesNoUser(t *testing.T) {
1136 o, _, _, users := testOps(t)
1137 p := keymailPlan(t, Grants{})
1138 if _, err := Converge(o, p); err != nil {
1139 t.Fatalf("Converge: %v", err)
1140 }
1141 if len(*users) != 0 {
1142 t.Fatalf("EnsureUser called for an ungranted host_role: %v", *users)
1143 }
1144}
1145
1146// A .d path that is a FILE rather than a directory is an honest error, not a
1147// panic and not a silent skip.
1148func TestDropInDirectoryThatIsAFileIsAnError(t *testing.T) {
1149 o, _, _, _ := testOps(t)
1150 p := keymailPlan(t, keymailGrants())
1151 svcPath, _ := DropInPaths(o.EtcDir, p.Host)
1152 if err := os.WriteFile(filepath.Dir(svcPath), []byte("not a directory"), 0o644); err != nil {
1153 t.Fatal(err)
1154 }
1155 if _, err := Converge(o, p); err == nil {
1156 t.Fatal("Converge succeeded with a file where the .d directory belongs")
1157 }
1158}
1159
1160// A non-daemon route's restart stays the caller's try-restart, byte-
1161// identically to today: converge writes the unit text and starts nothing.
1162func TestNonDaemonRouteIsNeitherEnabledNorStarted(t *testing.T) {
1163 o, f, _, _ := testOps(t)
1164 var s Set
1165 if err := s.UnmarshalJSON([]byte(`{"v":1,"command":"serve","children":true}`)); err != nil {
1166 t.Fatal(err)
1167 }
1168 p := Plan{Host: "app.example.com", App: "app", Set: &s, Hibernate: true}
1169
1170 if _, err := Converge(o, p); err != nil {
1171 t.Fatalf("Converge: %v", err)
1172 }
1173 for _, c := range f.calls {
1174 if c != "daemon-reload" {
1175 t.Fatalf("a hibernating non-daemon route had its unit state changed: %v", f.calls)
1176 }
1177 }
1178 svcPath, _ := DropInPaths(o.EtcDir, p.Host)
1179 if !strings.Contains(string(readFile(t, svcPath)), "ExecStart=/opt/carlos/live/%i serve") {
1180 t.Fatal("the drop-in was not written for a hibernating route")
1181 }
1182}
1183
1184// Drift repair, the other half of the determinism promise: a converged box
1185// whose daemon has been stopped and whose units have been disabled by hand is
1186// put back — this is what the hourly pass is for (spec §4d).
1187func TestStoppedDaemonAndDisabledUnitsAreRepaired(t *testing.T) {
1188 o, f, _, _ := testOps(t)
1189 p := keymailPlan(t, keymailGrants())
1190 if _, err := Converge(o, p); err != nil {
1191 t.Fatalf("Converge (first): %v", err)
1192 }
1193 f.active[ServiceUnit(p.Host)] = false
1194 f.enabled[ServiceUnit(p.Host)] = false
1195 f.enabled[SocketUnit(p.Host)] = false
1196 f.reset()
1197
1198 res, err := Converge(o, p)
1199 if err != nil {
1200 t.Fatalf("Converge (repair): %v", err)
1201 }
1202 if res.Changed {
1203 t.Fatal("Changed = true, but no byte moved — Changed is about the FILES")
1204 }
1205 if indexOf(f.calls, "daemon-reload") >= 0 {
1206 t.Fatalf("a unit-state repair reloaded the daemon: %v", f.calls)
1207 }
1208 for _, want := range []string{
1209 "enable carlos-app@keymail.dev.socket",
1210 "enable carlos-app@keymail.dev.service",
1211 "start carlos-app@keymail.dev.service",
1212 } {
1213 if indexOf(f.calls, want) < 0 {
1214 t.Fatalf("drift not repaired: %q missing from %v", want, f.calls)
1215 }
1216 }
1217}
1218
1219// Restart's error is REAL — it is what makes adoptHost's rollback fire.
1220func TestRestartReportsItsFailure(t *testing.T) {
1221 o, f, _, _ := testOps(t)
1222 p := keymailPlan(t, keymailGrants())
1223 f.fail["restart carlos-app@keymail.dev.service"] = errors.New("job failed")
1224 if err := Restart(o, p); err == nil {
1225 t.Fatal("Restart swallowed a failed restart")
1226 }
1227 if f.calls[len(f.calls)-1] != "restart carlos-app@keymail.dev.service" {
1228 t.Fatalf("Restart ran %v", f.calls)
1229 }
1230}
1231
1232// Two plans, one box, no conflict: the second route's drop-ins are written
1233// and the first route's are untouched.
1234func TestASecondRouteOnTheBoxConvergesIndependently(t *testing.T) {
1235 o, _, _, _ := testOps(t)
1236 first := keymailPlan(t, keymailGrants())
1237 if _, err := Converge(o, first); err != nil {
1238 t.Fatalf("Converge (first): %v", err)
1239 }
1240 svcA, sockA := DropInPaths(o.EtcDir, first.Host)
1241 beforeSvc, beforeSock := readFile(t, svcA), readFile(t, sockA)
1242
1243 second := Plan{Host: "platform.carlosframework.com", App: "console", Set: consoleSet(t), Grants: consoleGrants()}
1244 if _, err := Converge(o, second); err != nil {
1245 t.Fatalf("Converge (second): %v", err)
1246 }
1247 svcB, sockB := DropInPaths(o.EtcDir, second.Host)
1248 if got, want := readFile(t, svcB), readGolden(t, "console.service.conf"); !bytes.Equal(got, want) {
1249 t.Fatalf("console service drop-in:\n got: %q\n want: %q", got, want)
1250 }
1251 mustNotExist(t, sockB) // console declares no ports
1252 if !bytes.Equal(readFile(t, svcA), beforeSvc) || !bytes.Equal(readFile(t, sockA), beforeSock) {
1253 t.Fatal("converging one route changed another's drop-ins")
1254 }
1255}
1256