carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package hostactors
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "github.com/carlosframework/platform/internal/hostclaim"
8 "github.com/carlosframework/platform/internal/hostfloor"
9 "github.com/carlosframework/platform/internal/release"
10 "strings"
11 "testing"
12 "time"
13)
14
15// writeFence records ref's termination as observed by a separate regional
16// writer box. The writer sits in another pool, so no census scope requires
17// it; a box never witnesses its own termination (M1-5).
18func writeFence(t *testing.T, d Directory, o *Observed, ref string, now time.Time, invalid bool) {
19 t.Helper()
20 b := o.Roster.Boxes[ref]
21 writer := b
22 writer.BoxID, writer.Label, writer.Pool = "fence-writer", "fence-writer", "workers"
23 writer.Enrollment = strings.Repeat("9", 32)
24 writer.Cloud.InstanceID = "i-fence-writer"
25 o.Roster.Boxes["fence-writer"] = writer
26 state := "terminated"
27 if invalid {
28 state = "stopped"
29 }
30 f := map[string]any{"v": 1, "box": ref, "enrollment": b.Enrollment, "cloud": b.Cloud, "state": state, "observed_at": now, "via": "describe", "request_id": "request", "written_by": map[string]string{"box": "fence-writer", "enrollment": writer.Enrollment}}
31 raw, _ := json.Marshal(f)
32 if err := release.CreateExact(context.Background(), d.Store, "control/box-fences/"+b.Enrollment+".json", raw); err != nil {
33 t.Fatal(err)
34 }
35}
36func TestTerminationFenceExcludesActiveBeforeRosterUpdate(t *testing.T) {
37 for _, invalid := range []bool{false, true} {
38 t.Run(map[bool]string{false: "valid", true: "invalid"}[invalid], func(t *testing.T) {
39 d, o, e, f, scope, now := censusFixture(t)
40 writeFence(t, d, &o, "box", now, invalid)
41 reports, err := Census(context.Background(), o, e, d.PublicKey, f, scope, now)
42 if invalid {
43 if err == nil {
44 t.Fatal("invalid fence treated as absence or exclusion")
45 }
46 } else if err != nil || len(reports) != 1 {
47 t.Fatal(reports, err)
48 }
49 if _, err := d.Lookup(context.Background(), "box"); err == nil {
50 t.Fatal("fenced or invalid actor eligible")
51 }
52 if _, err := d.CheckBoxStart(context.Background(), "box"); err == nil {
53 t.Fatal("fenced or invalid start allowed")
54 }
55 scope.Participants = []string{"box"}
56 if _, err := Census(context.Background(), o, e, d.PublicKey, f, scope, now); err == nil {
57 t.Fatal("fenced participant allowed")
58 }
59 })
60 }
61}
62func boundConsole(o *Observed, e *reportsFixture) {
63 c := o.Roster.Consoles["console-1"]
64 c.Compute = &ComputeBinding{Box: "box", Enrollment: e.box.Enrollment}
65 o.Roster.Consoles["console-1"] = c
66 e.console.Compute = c.Compute
67 e.box.ConsoleProcesses = []ConsoleProcess{{ConsoleID: "console-1", Enrollment: c.Enrollment, ProcessID: e.console.ProcessID, BinarySHA256: e.console.BinarySHA256, GuardFloorSHA256: e.box.FloorSHA256, AllLaunchPathsGuarded: true}}
68}
69func TestConsoleFenceRequiresExactProcessMachineAndBoot(t *testing.T) {
70 for _, fault := range []string{"good", "missing", "boot", "enrollment", "process", "ambiguous", "console-enrollment", "console-instance"} {
71 t.Run(fault, func(t *testing.T) {
72 d, o, e, f, scope, now := censusFixture(t)
73 boundConsole(&o, &e)
74 // The starter lives elsewhere; only the bound console can be excluded.
75 scope.ConsoleID = "starter"
76 c := o.Roster.Consoles["console-1"]
77 c.Compute = nil
78 c.InstanceID = "starter"
79 c.Enrollment = "dddddddddddddddddddddddddddddddd"
80 o.Roster.Consoles["starter"] = c
81 switch fault {
82 case "missing":
83 c = o.Roster.Consoles["console-1"]
84 c.Compute = nil
85 o.Roster.Consoles["console-1"] = c
86 case "console-enrollment":
87 e.console.Enrollment = "wrong"
88 case "console-instance":
89 e.console.InstanceID = "wrong"
90 case "boot":
91 e.console.BootID = "other"
92 case "enrollment":
93 e.console.Compute = &ComputeBinding{Box: "box", Enrollment: "other"}
94 case "process":
95 e.box.ConsoleProcesses[0].ProcessID = "other"
96 case "ambiguous":
97 e.box.ConsoleProcesses = append(e.box.ConsoleProcesses, e.box.ConsoleProcesses[0])
98 }
99 // Validate the binding independently, then exercise full census below with exact objects.
100 _, err := verifyCompute(context.Background(), o, e, "console-1", o.Roster.Consoles["console-1"], e.console)
101 if (err == nil) != (fault == "good") {
102 t.Fatal(fault, err)
103 }
104 if fault != "good" {
105 return
106 }
107 writeFence(t, d, &o, "box", now, false)
108 for id, cc := range o.Roster.Consoles {
109 r := e.console
110 if id == "starter" {
111 r.Actor = id
112 r.Enrollment = cc.Enrollment
113 r.InstanceID = id
114 r.Compute = nil
115 }
116 raw, _ := json.Marshal(r)
117 if err := release.CreateExact(context.Background(), d.Store, "status/consoles/"+id+".json", raw); err != nil {
118 t.Fatal(err)
119 }
120 }
121 local := e.console
122 local.Actor = "starter"
123 local.Enrollment = c.Enrollment
124 local.InstanceID = "starter"
125 local.Compute = nil
126 exact := ExactEvidence{Store: d.Store, PublicKey: d.PublicKey, Boxes: e.BoxReport, Self: func(context.Context) (Report, error) { return local, nil }}
127 reports, err := Census(context.Background(), o, exact, d.PublicKey, f, scope, now)
128 if err != nil || len(reports) != 1 || reports["console:starter"].Actor != "starter" {
129 t.Fatal(reports, err)
130 }
131 })
132 }
133}
134func TestRelocationOnlyNewestGuardedConsoleMayBeStale(t *testing.T) {
135 for _, fault := range []string{"good", "ordinary", "guard", "floor", "binding", "future", "newer-malformed", "newer-image", "other-operation"} {
136 t.Run(fault, func(t *testing.T) {
137 d, o, e, f, scope, now := censusFixture(t)
138 boundConsole(&o, &e)
139 e.console.GeneratedAt = now.Add(-time.Hour)
140 scope.boxRelocation = true
141 scope.StarterBox = "box"
142 scope.ConsoleID = ""
143 switch fault {
144 case "ordinary", "other-operation":
145 scope.boxRelocation = false
146 scope.ConsoleID = "console-1"
147 case "guard":
148 e.box.ConsoleProcesses[0].AllLaunchPathsGuarded = false
149 case "floor":
150 e.box.ConsoleProcesses[0].GuardFloorSHA256 = "wrong"
151 case "binding":
152 e.console.Compute = nil
153 case "future":
154 e.console.GeneratedAt = now.Add(time.Minute)
155 case "newer-image":
156 e.console.BinarySHA256 = "wrong"
157 }
158 raw, _ := json.Marshal(e.console)
159 if fault == "newer-malformed" {
160 raw = []byte("{")
161 }
162 if err := release.CreateExact(context.Background(), d.Store, "status/consoles/console-1.json", raw); err != nil {
163 t.Fatal(err)
164 }
165 exact := ExactEvidence{Store: d.Store, PublicKey: d.PublicKey, Boxes: e.BoxReport, Self: func(context.Context) (Report, error) { return e.box, nil }}
166 _, err := Census(context.Background(), o, exact, d.PublicKey, f, scope, now)
167 if (err == nil) != (fault == "good") {
168 t.Fatal(fault, err)
169 }
170 })
171 }
172}
173
174type brokenFenceStore struct{ release.Store }
175
176func (s brokenFenceStore) Get(ctx context.Context, key string) ([]byte, string, error) {
177 if strings.HasPrefix(key, "control/box-fences/") {
178 return nil, "", errors.New("fence read unavailable")
179 }
180 return s.Store.Get(ctx, key)
181}
182func TestFenceAuthorityErrorsAndMismatchesRefuse(t *testing.T) {
183 for _, fault := range []string{"read", "malformed", "enrollment", "cloud", "writer", "future"} {
184 t.Run(fault, func(t *testing.T) {
185 d, o, e, f, scope, now := censusFixture(t)
186 if fault == "read" {
187 d.Store = brokenFenceStore{d.Store}
188 e.d = d
189 } else {
190 writeFence(t, d, &o, "box", now, false)
191 key := "control/box-fences/" + o.Roster.Boxes["box"].Enrollment + ".json"
192 raw, etag, err := d.Store.Get(context.Background(), key)
193 if err != nil {
194 t.Fatal(err)
195 }
196 var record map[string]any
197 if err := json.Unmarshal(raw, &record); err != nil {
198 t.Fatal(err)
199 }
200 switch fault {
201 case "enrollment":
202 record["enrollment"] = "wrong"
203 case "cloud":
204 record["cloud"] = Cloud{Provider: "aws", Account: "123", Region: "eu-west-1", InstanceID: "wrong"}
205 case "writer":
206 record["written_by"] = map[string]string{"box": "unknown", "enrollment": "wrong"}
207 case "future":
208 record["observed_at"] = now.Add(time.Minute)
209 }
210 raw, _ = json.Marshal(record)
211 if fault == "malformed" {
212 raw = []byte("{")
213 }
214 // Test-only mutation creates corrupt authority without allowing production overwrite.
215 if _, err = d.Store.Put(context.Background(), key, raw, etag); err != nil {
216 t.Fatal(err)
217 }
218 }
219 if _, err := Census(context.Background(), o, e, d.PublicKey, f, scope, now); err == nil {
220 t.Fatal("bad authority admitted census")
221 }
222 if _, err := d.Lookup(context.Background(), "box"); err == nil {
223 t.Fatal("bad authority granted eligibility")
224 }
225 if _, err := d.CheckBoxStart(context.Background(), "box"); err == nil {
226 t.Fatal("bad authority granted start")
227 }
228 })
229 }
230}
231
232func TestStartRevalidatesReservedActorEligibility(t *testing.T) {
233 for _, fault := range []string{"good", "quarantined", "retired", "fenced", "malformed", "unreadable", "console-quarantined", "console-fenced"} {
234 t.Run(fault, func(t *testing.T) {
235 ctx := t.Context()
236 d, sg, o := fixture(t)
237 floor := hostfloor.Floor{Min: 1, MinRelease: "v1.2.3"}
238 if err := hostfloor.Publish(ctx, d.Store, sg, d.Bucket, floor); err != nil {
239 t.Fatal(err)
240 }
241 f, err := hostfloor.Read(ctx, d.Store, d.PublicKey, d.Bucket)
242 if err != nil {
243 t.Fatal(err)
244 }
245 r := Clone(o.Roster)
246 r.Publication, r.FloorSHA256 = "published", f.Digest
247 actor, enrollment := "box:box", r.Boxes["box"].Enrollment
248 if strings.HasPrefix(fault, "console-") {
249 actor, enrollment = "console:console-1", strings.Repeat("c", 32)
250 r.Consoles["console-1"] = Console{Enrollment: enrollment, InstanceID: "console-1", Namespaces: []string{"platform"}, DeploymentBucket: d.Bucket, Compute: &ComputeBinding{Box: "box", Enrollment: r.Boxes["box"].Enrollment}}
251 }
252 o, err = d.Replace(ctx, sg, o, r)
253 if err != nil {
254 t.Fatal(err)
255 }
256 before, err := hostclaim.Read(ctx, d.Store, "shop.example.com")
257 if err != nil {
258 t.Fatal(err)
259 }
260 h := Reservation{ID: "hold", Host: before.Host, OpID: "operation", ExpectedClaimETag: before.ETag, FloorSHA256: f.Digest, Actors: map[string]string{actor: enrollment}, Releases: map[string]Report{actor: {Enrollment: enrollment, GeneratedAt: time.Now()}}}
261 o, err = d.Reserve(ctx, sg, o, h, false)
262 if err != nil {
263 t.Fatal(err)
264 }
265 switch fault {
266 case "quarantined", "console-quarantined":
267 _, err = d.Quarantine(ctx, sg, o, "box", o.Roster.Boxes["box"].Enrollment, "loss")
268 case "retired":
269 r = Clone(o.Roster)
270 b := r.Boxes["box"]
271 b.State = "retired"
272 r.Boxes["box"] = b
273 _, err = d.Replace(ctx, sg, o, r)
274 case "fenced", "console-fenced", "malformed":
275 writeFence(t, d, &o, "box", time.Now(), fault == "malformed")
276 case "unreadable":
277 d.Store = brokenFenceStore{d.Store}
278 }
279 if err != nil {
280 t.Fatal(err)
281 }
282 called := false
283 _, err = d.Start(ctx, before, h.ID, func(*hostclaim.Claim) error { called = true; return errors.New("stop before writing") })
284 if err == nil || called != (fault == "good") {
285 t.Fatalf("transition called=%v, error=%v", called, err)
286 }
287 after, err := hostclaim.Read(ctx, d.Store, before.Host)
288 if err != nil || before.ETag != after.ETag || before.Absent != after.Absent {
289 t.Fatal("refusal changed claim", err)
290 }
291 })
292 }
293}
294