carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "context"
5 "os"
6 "path/filepath"
7 "runtime"
8 "strings"
9 "testing"
10)
11
12func hostexec(t *testing.T, f *applyFixture) string {
13 t.Helper()
14 dir := filepath.Join(f.a.dataDir, hostexecDir)
15 if err := os.MkdirAll(dir, 0o755); err != nil {
16 t.Fatal(err)
17 }
18 return dir
19}
20
21func assertNotUpdated(t *testing.T, f *applyFixture, why string) {
22 t.Helper()
23 r := f.result()
24 if r == nil || r.Outcome != systemOutcomeDeferred || !strings.Contains(r.Reason, why) {
25 t.Fatalf("result = %+v, want deferred naming %q", r, why)
26 }
27 if r.terminal() {
28 t.Fatal("the agent would never retry this promote")
29 }
30 if bin, _ := os.ReadFile(systemBinaryPath(f.a.optDir)); !strings.Contains(string(bin), "carlos v1") {
31 t.Fatal("the binary changed under a refusal")
32 }
33 if j, _ := readSystemJournal(f.a.optDir); j != nil {
34 t.Fatalf("a refusal opened a journal: %+v", j)
35 }
36 if _, err := os.Stat(updateBegunPath(f.a.optDir)); !os.IsNotExist(err) {
37 t.Fatalf("the update-begun marker outlived the refusal: %v", err)
38 }
39 if f.called("systemctl stop") || f.called("systemctl restart") {
40 t.Fatal("a refused update touched a unit")
41 }
42}
43
44// Spec §5.10: any controller record, in any state, refuses the update.
45func TestApplyRefusesWhileThisBoxTakesPartInAClaimOperation(t *testing.T) {
46 t.Parallel()
47 for _, state := range []string{"pending", "active", "lost"} {
48 t.Run(state, func(t *testing.T) {
49 f, _ := applyHappy(t)
50 f.a.regionHA = true
51 os.WriteFile(filepath.Join(hostexec(t, f), "shop.bab.test.json"), []byte(`{"state":"`+state+`"}`), 0o600)
52 if err := f.a.Run(context.Background(), false); err != nil {
53 t.Fatal(err)
54 }
55 assertNotUpdated(t, f, "takes part in a running claim operation")
56 })
57 }
58}
59
60// Review Focus 4: what root cannot read is a refusal, never a pass.
61func TestApplyRefusesWhenItCannotTellWhetherThisBoxTakesPart(t *testing.T) {
62 t.Parallel()
63 cases := map[string]func(t *testing.T, f *applyFixture){
64 "a temporary file": func(t *testing.T, f *applyFixture) {
65 os.WriteFile(filepath.Join(hostexec(t, f), ".shop.bab.test.json.tmp"), nil, 0o600)
66 },
67 "garbage": func(t *testing.T, f *applyFixture) {
68 os.WriteFile(filepath.Join(hostexec(t, f), "shop.bab.test.json"), []byte("{half"), 0o600)
69 },
70 "a symlink in place of the directory": func(t *testing.T, f *applyFixture) {
71 elsewhere := t.TempDir()
72 if err := os.Symlink(elsewhere, filepath.Join(f.a.dataDir, hostexecDir)); err != nil {
73 t.Skip("no symlinks here:", err)
74 }
75 },
76 "a file in place of the directory": func(t *testing.T, f *applyFixture) {
77 os.WriteFile(filepath.Join(f.a.dataDir, hostexecDir), nil, 0o600)
78 },
79 }
80 for name, setup := range cases {
81 t.Run(name, func(t *testing.T) {
82 f, _ := applyHappy(t)
83 f.a.regionHA = true
84 setup(t, f)
85 if err := f.a.Run(context.Background(), false); err != nil {
86 t.Fatal(err)
87 }
88 assertNotUpdated(t, f, "hostexec")
89 })
90 }
91}
92
93func TestApplyRefusesWhenItCannotListTheControllerDirectory(t *testing.T) {
94 if runtime.GOOS == "windows" || os.Geteuid() == 0 {
95 t.Skip("permission bits do not stop this user")
96 }
97 f, _ := applyHappy(t)
98 f.a.regionHA = true
99 dir := hostexec(t, f)
100 os.Chmod(dir, 0)
101 t.Cleanup(func() { os.Chmod(dir, 0o755) })
102 if err := f.a.Run(context.Background(), false); err != nil {
103 t.Fatal(err)
104 }
105 assertNotUpdated(t, f, "cannot be listed")
106}
107
108// The marker is written before the box changes and is gone once the
109// update is done; an empty controller directory is no participation.
110func TestApplyHoldsTheUpdateBegunMarkerAcrossTheInstall(t *testing.T) {
111 f, req := applyHappy(t)
112 f.a.regionHA = true
113 hostexec(t, f) // present and empty
114 seen := false
115 orig := f.a.ops.systemctl
116 f.a.ops.systemctl = func(args ...string) (string, error) {
117 if args[0] == "restart" && args[len(args)-1] == systemEdgeUnit {
118 _, err := os.Stat(updateBegunPath(f.a.optDir))
119 seen = err == nil
120 }
121 return orig(args...)
122 }
123 if err := f.a.Run(context.Background(), false); err != nil {
124 t.Fatal(err)
125 }
126 if r := f.result(); r == nil || r.Outcome != systemOutcomeApplied || !r.matches("stable", "v2", req.PromotedAt) {
127 t.Fatalf("result = %+v", r)
128 }
129 if !seen {
130 t.Fatal("no update-begun marker while the edge restarted onto the new binary")
131 }
132 if _, err := os.Stat(updateBegunPath(f.a.optDir)); !os.IsNotExist(err) {
133 t.Fatalf("marker left after the update finished: %v", err)
134 }
135}
136
137func TestApplyWithoutRegionHAWritesNoMarkerAndIgnoresControllerRecords(t *testing.T) {
138 f, _ := applyHappy(t)
139 os.WriteFile(filepath.Join(hostexec(t, f), "shop.bab.test.json"), []byte(`{}`), 0o600)
140 if err := f.a.Run(context.Background(), false); err != nil {
141 t.Fatal(err)
142 }
143 if r := f.result(); r == nil || r.Outcome != systemOutcomeApplied {
144 t.Fatalf("result = %+v: the flag is off, so nothing changes", r)
145 }
146 if _, err := os.Stat(systemGuardDir(f.a.optDir)); !os.IsNotExist(err) {
147 t.Fatalf("the guard directory was created with the flag off: %v", err)
148 }
149}
150
151// A crash between the marker and the journal leaves a marker with no
152// update behind it; the next run, --recover included, clears it.
153func TestAStaleUpdateBegunMarkerIsClearedByTheNextRun(t *testing.T) {
154 f := newApplyFixture(t, "stable", "v1")
155 os.MkdirAll(systemGuardDir(f.a.optDir), 0o755)
156 os.WriteFile(updateBegunPath(f.a.optDir), []byte(`{"target":"v2"}`), 0o644)
157 if err := f.a.Run(context.Background(), true); err != nil {
158 t.Fatal(err)
159 }
160 if _, err := os.Stat(updateBegunPath(f.a.optDir)); !os.IsNotExist(err) {
161 t.Fatalf("stale marker kept: %v", err)
162 }
163}
164
165// A rollback that could not finish keeps its journal for the next run, and
166// the marker with it: the update is not over.
167func TestTheMarkerStaysWhileAJournalIsOpen(t *testing.T) {
168 f := newApplyFixture(t, "stable", "v1")
169 f.a.regionHA = true
170 os.MkdirAll(systemGuardDir(f.a.optDir), 0o755)
171 os.WriteFile(updateBegunPath(f.a.optDir), []byte(`{"target":"v2"}`), 0o644)
172 // A journal recovery cannot act on: no carlos.prev to restore.
173 if err := writeJSONDurable(f.a.file("journal.json"), systemJournal{Phase: systemPhaseInstalled, Target: "v2"}, 0o644); err != nil {
174 t.Fatal(err)
175 }
176 _ = f.a.Run(context.Background(), true)
177 if j, _ := readSystemJournal(f.a.optDir); j == nil {
178 t.Fatal("recovery closed the journal; this test needs one it cannot finish")
179 }
180 if _, err := os.Stat(updateBegunPath(f.a.optDir)); err != nil {
181 t.Fatalf("marker cleared while the journal is still open: %v", err)
182 }
183}
184
185// Astra plan R1-3: participation defers, it does not refuse. Once the
186// operation ends and its record is cleared, the same promote applies. The
187// agent's side, re-requesting a deferred apply, is
188// TestSystemLegReusesAVerifiedCacheOnARetry.
189func TestApplyDeferredForParticipationAppliesOnceItEnds(t *testing.T) {
190 f, req := applyHappy(t)
191 f.a.regionHA = true
192 rec := filepath.Join(hostexec(t, f), "shop.bab.test.json")
193 os.WriteFile(rec, []byte(`{"state":"active"}`), 0o600)
194 if err := f.a.Run(context.Background(), false); err != nil {
195 t.Fatal(err)
196 }
197 if r := f.result(); r == nil || r.Outcome != systemOutcomeDeferred || r.terminal() {
198 t.Fatalf("result = %+v, want deferred, which the agent retries", r)
199 }
200 os.Remove(rec)
201 if err := f.a.Run(context.Background(), false); err != nil {
202 t.Fatal(err)
203 }
204 if r := f.result(); r == nil || r.Outcome != systemOutcomeApplied || !r.matches("stable", "v2", req.PromotedAt) {
205 t.Fatalf("result after the operation ended = %+v, want applied", r)
206 }
207}
208