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 "encoding/base64"
5 "encoding/json"
6 "errors"
7 "os"
8 "path/filepath"
9 "testing"
10
11 "github.com/carlosframework/platform/internal/hostfloor"
12)
13
14func sharedGuardFixture(t *testing.T) (*applyFixture, string) {
15 t.Helper()
16 f, id := guardRecoveryFixture(t)
17 dir := systemGuardDir(f.a.optDir)
18 if err := os.Rename(f.a.floor.Dir, dir); err != nil {
19 t.Fatal(err)
20 }
21 f.a.floor.Dir = dir
22 f.a.regionHA = true
23 return f, id
24}
25
26func TestSystemGuardRegionHAPreservesReservationThroughRecovery(t *testing.T) {
27 f, id := sharedGuardFixture(t)
28 a := f.a
29 if err := a.floor.Begin(t.Context(), id, a.dataDir); err != nil {
30 t.Fatal(err)
31 }
32 before, err := os.ReadFile(updateBegunPath(a.optDir))
33 if err != nil {
34 t.Fatal(err)
35 }
36 if err := a.beginExecutableChange(t.Context(), "new-build", f.now); err != nil {
37 t.Fatal(err)
38 }
39 a.settleUpdateBegun(t.Context())
40 after, err := os.ReadFile(updateBegunPath(a.optDir))
41 if err != nil || string(after) != string(before) {
42 t.Fatalf("region HA changed the floor reservation: %s, %v", after, err)
43 }
44 if err := a.journal(systemJournal{Phase: systemPhaseBackingUp, Target: "new-build", PrevBuild: "old-build", StartedAt: f.now}); err != nil {
45 t.Fatal(err)
46 }
47 if err := a.Run(t.Context(), true); err != nil {
48 t.Fatal(err)
49 }
50 completed, err := os.ReadFile(filepath.Join(a.floor.Dir, "completed.json"))
51 if err != nil || string(completed) != id {
52 t.Fatalf("completion = %q, %v", completed, err)
53 }
54 if _, err := os.Stat(updateBegunPath(a.optDir)); !os.IsNotExist(err) {
55 t.Fatalf("marker remains after recovery: %v", err)
56 }
57}
58
59func TestSystemGuardRegionHAKeepsFailedCompletionEvidence(t *testing.T) {
60 for _, failure := range []string{"completion", "controller", "marker"} {
61 t.Run(failure, func(t *testing.T) {
62 f, id := sharedGuardFixture(t)
63 a := f.a
64 if err := a.floor.Begin(t.Context(), id, a.dataDir); err != nil {
65 t.Fatal(err)
66 }
67 if err := os.MkdirAll(a.stagingDir(), 0755); err != nil {
68 t.Fatal(err)
69 }
70 evidence := filepath.Join(a.stagingDir(), "evidence")
71 if err := os.WriteFile(evidence, []byte("retain"), 0600); err != nil {
72 t.Fatal(err)
73 }
74 switch failure {
75 case "completion":
76 if err := os.Mkdir(filepath.Join(a.floor.Dir, "completed.json"), 0755); err != nil {
77 t.Fatal(err)
78 }
79 case "controller":
80 if err := os.WriteFile(filepath.Join(a.dataDir, hostexecDir, "pending.json"), []byte(`{"state":"pending"}`), 0600); err != nil {
81 t.Fatal(err)
82 }
83 if err := a.beginExecutableChange(t.Context(), "new-build", f.now); !errors.Is(err, errSystemDeferred) {
84 t.Fatalf("late participation = %v", err)
85 }
86 case "marker":
87 if err := os.WriteFile(updateBegunPath(a.optDir), []byte(`{"target":"new-build"}`), 0600); err != nil {
88 t.Fatal(err)
89 }
90 if err := a.beginExecutableChange(t.Context(), "new-build", f.now); err == nil {
91 t.Fatal("region marker accepted for an armed transaction")
92 }
93 }
94 a.settleUpdateBegun(t.Context())
95 if err := a.Run(t.Context(), true); err == nil {
96 t.Fatal("unsafe recovery succeeded")
97 }
98 for _, path := range []string{updateBegunPath(a.optDir), evidence} {
99 if _, err := os.Stat(path); err != nil {
100 t.Fatalf("recovery discarded %s: %v", path, err)
101 }
102 }
103 })
104 }
105}
106
107func TestSystemGuardUnpublishedKeepsRegionHAParticipation(t *testing.T) {
108 f, _ := applyHappy(t)
109 a := f.a
110 a.regionHA = true
111 a.floor = &hostfloor.Disk{Dir: systemGuardDir(a.optDir), Bucket: "bucket", PublicKey: f.pub, Participation: a.participation}
112 if err := os.MkdirAll(a.floor.Dir, 0755); err != nil {
113 t.Fatal(err)
114 }
115 raw, _ := json.Marshal(hostfloor.Policy{Schema: 1, DeploymentBucket: "bucket", Mode: "unpublished"})
116 if err := hostfloor.Durable(filepath.Join(a.floor.Dir, "policy.json"), raw, 0644); err != nil {
117 t.Fatal(err)
118 }
119 if err := os.WriteFile(filepath.Join(hostexec(t, f), "pending.json"), []byte(`{}`), 0600); err != nil {
120 t.Fatal(err)
121 }
122 if err := a.beginExecutableChange(t.Context(), "v2", f.now); !errors.Is(err, errSystemDeferred) {
123 t.Fatalf("unpublished floor bypassed participation: %v", err)
124 }
125 if _, err := os.Stat(updateBegunPath(a.optDir)); !os.IsNotExist(err) {
126 t.Fatalf("refused unarmed update retained marker: %v", err)
127 }
128 if err := os.Remove(filepath.Join(a.dataDir, hostexecDir, "pending.json")); err != nil {
129 t.Fatal(err)
130 }
131 if err := a.beginExecutableChange(t.Context(), "v2", f.now); err != nil {
132 t.Fatal(err)
133 }
134 raw, err := os.ReadFile(updateBegunPath(a.optDir))
135 var begun updateBegun
136 if err != nil || json.Unmarshal(raw, &begun) != nil || begun.Target != "v2" {
137 t.Fatalf("unpublished marker = %s, %v", raw, err)
138 }
139 a.settleUpdateBegun(t.Context())
140 if _, err := os.Stat(updateBegunPath(a.optDir)); !os.IsNotExist(err) {
141 t.Fatalf("completed unarmed update retained marker: %v", err)
142 }
143 if err := os.MkdirAll(a.stagingDir(), 0755); err != nil {
144 t.Fatal(err)
145 }
146 if err := os.WriteFile(a.file("journal.json"), []byte("{"), 0600); err != nil {
147 t.Fatal(err)
148 }
149 if err := a.Run(t.Context(), true); err == nil {
150 t.Fatal("malformed journal recovered")
151 }
152 if _, err := os.Stat(a.stagingDir()); err != nil {
153 t.Fatalf("unresolved root recovery removed staging: %v", err)
154 }
155 if _, err := os.Stat(a.file("journal.json")); err != nil {
156 t.Fatalf("malformed journal discarded: %v", err)
157 }
158}
159
160func TestSystemGuardHookRecognizesOnlyLegacyRegionState(t *testing.T) {
161 for _, state := range []string{"absent", "empty", "region-marker", "floor-marker", "malformed-marker", "tmp-only", "marker+tmp", "floor-marker+tmp", "checker", "identities", "activation", "completed", "policy", "symlink"} {
162 t.Run(state, func(t *testing.T) {
163 f := newApplyFixture(t, "stable", "v1")
164 t.Setenv("CARLOS_RELEASE_PUBKEY", base64.StdEncoding.EncodeToString(f.pub))
165 t.Setenv("CARLOS_DEPLOYMENT_BUCKET", "bucket")
166 dir := systemGuardDir(f.a.optDir)
167 if state == "symlink" {
168 if err := os.Symlink(t.TempDir(), dir); err != nil {
169 t.Skip(err)
170 }
171 } else if state != "absent" {
172 if err := os.MkdirAll(dir, 0755); err != nil {
173 t.Fatal(err)
174 }
175 }
176 name, body := "", ""
177 switch state {
178 case "region-marker":
179 name, body = updateBegunFile, `{"target":"v2"}`
180 case "floor-marker":
181 name, body = updateBegunFile, `{"id":"activation"}`
182 case "malformed-marker":
183 name, body = updateBegunFile, `{`
184 case "tmp-only":
185 name, body = updateBegunFile+".tmp", `{"tar`
186 case "marker+tmp", "floor-marker+tmp":
187 name, body = updateBegunFile, `{"target":"v2"}`
188 if state == "floor-marker+tmp" {
189 body = `{"id":"activation"}`
190 }
191 if err := os.WriteFile(filepath.Join(dir, updateBegunFile+".tmp"), []byte(`{"tar`), 0644); err != nil {
192 t.Fatal(err)
193 }
194 case "checker":
195 name = "carlos"
196 case "identities":
197 if err := os.Mkdir(filepath.Join(dir, "identities"), 0755); err != nil {
198 t.Fatal(err)
199 }
200 case "activation", "completed", "policy":
201 name = state + ".json"
202 }
203 if name != "" {
204 if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0644); err != nil {
205 t.Fatal(err)
206 }
207 }
208 err := installFloorHook(f.a)
209 // A crash inside main's durable marker write leaves the .tmp or a
210 // torn marker; neither is floor state (Opus merge review M1-1).
211 legacy := state == "absent" || state == "empty" || state == "region-marker" || state == "malformed-marker" || state == "tmp-only" || state == "marker+tmp"
212 if legacy {
213 if err != nil || f.a.guarded() {
214 t.Fatalf("legacy state armed floor: %v", err)
215 }
216 if err := f.a.Run(t.Context(), true); err != nil {
217 t.Fatal(err)
218 }
219 if _, err := os.Stat(updateBegunPath(f.a.optDir)); !os.IsNotExist(err) {
220 t.Fatalf("legacy recovery kept marker: %v", err)
221 }
222 } else if err == nil {
223 t.Fatal("missing or invalid floor policy accepted")
224 }
225 })
226 }
227}
228
229// The floor decision is read under the apply lock: a locked-out run never
230// looks at a guard directory another run may be writing (Opus merge M1-1).
231func TestSystemFloorHookRunsOnlyUnderTheApplyLock(t *testing.T) {
232 f := newApplyFixture(t, "stable", "v1")
233 f.promote("v2", []byte("x"), false)
234 called := 0
235 f.a.floorHook = func(*systemApplier) error { called++; return nil }
236 held, err := os.OpenFile(f.a.lockPath(), os.O_CREATE|os.O_RDWR, 0o600)
237 if err != nil {
238 t.Fatal(err)
239 }
240 if err := flockExclusiveNonblock(held); err != nil {
241 t.Fatal(err)
242 }
243 if err := f.a.Run(t.Context(), false); err != nil || called != 0 {
244 t.Fatalf("locked-out run: err=%v hook calls=%d", err, called)
245 }
246 held.Close()
247 if err := f.a.Run(t.Context(), false); err != nil || called != 1 {
248 t.Fatalf("lock-holding run: err=%v hook calls=%d", err, called)
249 }
250}
251