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 "encoding/json"
6 "errors"
7 "fmt"
8 "net"
9 "net/http"
10 "os"
11 "sort"
12 "strconv"
13 "time"
14
15 "github.com/carlosframework/platform/internal/activator"
16 "github.com/carlosframework/platform/internal/cgroup"
17 "github.com/carlosframework/platform/internal/hostbox"
18 "github.com/carlosframework/platform/internal/hostclaim"
19 execlease "github.com/carlosframework/platform/internal/hostexec"
20 "github.com/carlosframework/platform/internal/hostguard"
21 "github.com/carlosframework/platform/internal/instancectl"
22 "github.com/carlosframework/platform/internal/release"
23 "github.com/carlosframework/platform/internal/rootstate"
24)
25
26// ErrClaimLaunchUnavailable refuses every launch while a root provider of
27// the isolated launch protocol is missing.
28var ErrClaimLaunchUnavailable = fmt.Errorf("%w: claim launch root provider missing", activator.ErrClaimIsolationUnavailable)
29
30// ClaimLaunchUnits starts, stops and observes the host's isolated run unit.
31// Production is instancectl.ClaimUnits.
32type ClaimLaunchUnits interface {
33 StartReserved(context.Context, instancectl.ClaimUnit, instancectl.Launch, instancectl.ClaimExecPermit, func() error) (instancectl.ClaimUnitStarted, error)
34 Same(context.Context, instancectl.ClaimUnitStarted) error
35 Stop(context.Context, string) error
36 Gone(context.Context, string, string) error
37}
38
39// ClaimLaunchKernel is root's own view of the build and of the started unit.
40type ClaimLaunchKernel interface {
41 // Verify accepts only a root-owned, immutable build under the release
42 // tree with exactly the pinned identity.
43 Verify(string, hostbox.LaunchFile) error
44 VerifyDigest(context.Context, string, hostbox.LaunchFile, string) error
45 // Observe reads the main process lifetime and cgroup, and its tenant
46 // child's lifetime, namespace-bound build and running executable.
47 Observe(instancectl.ClaimUnitStarted) (ClaimLaunchObserved, error)
48}
49
50type ClaimLaunchObserved struct {
51 ProcessStart string
52 TenantPID int
53 TenantStart string
54 Group, Executable hostbox.LaunchFile
55 Running hostbox.LaunchFile
56 Launcher hostbox.LaunchFile
57}
58
59// ClaimLaunchRoot is root's side of the reservation-bearing isolated launch
60// protocol (claim Plan 11, R10). Every authority it checks comes from root's
61// own providers: the authenticated peer, its guard and claim reader, its
62// roster identity, its local launch journal and its own view of the kernel.
63// A request only names which reservation to run.
64type ClaimLaunchRoot struct {
65 boundReady func(context.Context, string, instancectl.ClaimUnitStarted) error
66 unitProgram func(claimUnitShape) (claimUnitProgram, error)
67 unitBindings *claimUnitBindings
68 history rootHistoryAccess
69 generations *rootGenerationSource
70 installedProgram func(context.Context, hostbox.RoleLaunchRequest, instancectl.ClaimUnitStarted, ClaimLaunchObserved) (string, error)
71 SeedController func(context.Context, StopPeer) error
72 executionState rootstate.Store
73 ValidateExecutable func(host, path, digest string) error
74 Local hostbox.LocalAuthority
75 Clock execlease.Clock
76 DataRoot, ExecLockDir string
77 runRoot string
78 Attempts map[string]claimLaunchAttemptReader
79 ReadyBudget, ReadyPoll time.Duration
80 Authenticate func(context.Context, net.Conn) (StopPeer, error)
81 Guard hostguard.Guard
82 Actor func(context.Context) (hostbox.Actor, error)
83 Journal *hostbox.LaunchJournal
84 Units ClaimLaunchUnits
85 Kernel ClaimLaunchKernel
86}
87
88func (h *ClaimLaunchRoot) complete() bool {
89 return h != nil && h.Guard.DataDir != "" && h.Guard.Claims != nil && h.Actor != nil && h.Local != nil && h.Clock != nil && h.DataRoot != "" && h.Journal != nil && h.Units != nil && h.Kernel != nil && h.ValidateExecutable != nil
90}
91
92// Serve answers one validated request for an authenticated peer.
93func (h *ClaimLaunchRoot) Serve(ctx context.Context, peer StopPeer, q hostbox.RoleLaunchRequest) (hostbox.RoleLaunchResponse, error) {
94 if !h.complete() {
95 return hostbox.RoleLaunchResponse{}, ErrClaimLaunchUnavailable
96 }
97 if err := q.Validate(); err != nil {
98 return hostbox.RoleLaunchResponse{}, err
99 }
100 if peer.UID == 0 || peer.PID <= 0 || peer.ProcessStart == "" || peer.BootID == "" || peer.ControllerID == "" {
101 return hostbox.RoleLaunchResponse{}, ErrClaimStopIdentity
102 }
103 out := hostbox.RoleLaunchResponse{V: hostbox.RoleLaunchV, Protocol: hostbox.RoleLaunchProtocol}
104 switch q.Op {
105 case hostbox.RoleLaunchInputs:
106 inputs, err := h.boundInputs(ctx, *q.Route)
107 if err != nil {
108 return hostbox.RoleLaunchResponse{}, err
109 }
110 out.Inputs = inputs
111 case hostbox.RoleLaunchCapability:
112 if q.Kind == "files-seed" {
113 if h.SeedController == nil {
114 return hostbox.RoleLaunchResponse{}, hostbox.ErrUnsupported
115 }
116 if err := h.SeedController(ctx, peer); err != nil {
117 return hostbox.RoleLaunchResponse{}, err
118 }
119 return out, nil
120 }
121 if q.Kind != "" && q.Kind != hostclaim.KindServing {
122 if (q.Kind != hostclaim.KindLanding && q.Kind != hostclaim.KindRelocating) || h.Attempts[q.Kind] == nil {
123 return hostbox.RoleLaunchResponse{}, hostbox.ErrUnsupported
124 }
125 }
126 case hostbox.RoleLaunchStart:
127 grant, err := h.start(ctx, peer, q)
128 if err != nil {
129 return hostbox.RoleLaunchResponse{}, err
130 }
131 out.Grant = &grant
132 case hostbox.RoleLaunchStop:
133 if err := h.stop(ctx, peer, *q.Reservation); err != nil {
134 return hostbox.RoleLaunchResponse{}, err
135 }
136 }
137 return out, nil
138}
139
140// claimLaunchAuthority is the fresh claim check root makes under the host
141// guard: the reservation's exact holder session and operation, this box as
142// the executor of a phase that admits a writer (main's wake policy), this
143// box as a holder's admitted writer, and an install stamp pin equal to the
144// reservation's stamp. A relocation must carry that pin.
145func claimLaunchAuthority(c *hostclaim.Claim, t hostguard.FenceTarget, box string) error {
146 if c == nil || c.Holder == nil || c.Holder.Incarnation != t.Incarnation || c.Holder.Stamp != t.Stamp || c.Holder.Session != t.Session || c.Op.ID != t.Op || c.Op.Phase != t.Phase {
147 return hostbox.ErrChanged
148 }
149 probe := c.Op.Kind != hostclaim.KindServing && c.Op.Phase == hostclaim.PhaseLandingProbing
150 if err := hostbox.WakePhase(c, box, probe); err != nil {
151 return err
152 }
153 if c.Holder.WriterBox != box {
154 return hostbox.ErrChanged
155 }
156 pin, pinned := c.Op.Journal["install_stamp"]
157 if (c.Op.Kind == hostclaim.KindRelocating && !pinned) || (pinned && pin != strconv.FormatUint(t.Stamp, 10)) {
158 return hostbox.ErrChanged
159 }
160 return nil
161}
162
163func claimLaunchTier(n int64) bool {
164 return n == cgroup.TierDefault.MemoryMax || n == cgroup.TierChildren.MemoryMax
165}
166
167// launchRecord is the prelaunch record of exactly t, written for the live
168// controller that is asking.
169func (h *ClaimLaunchRoot) launchRecord(ctx context.Context, peer StopPeer, t hostguard.FenceTarget) error {
170 rec, err := h.Journal.Read(ctx, t)
171 if err != nil {
172 return err
173 }
174 if err := validatePersistedUnitContainment(ctx, rec, h.Units); err != nil {
175 return err
176 }
177 if rec.ControllerPID != peer.PID || rec.ProcessStart != peer.ProcessStart {
178 return ErrClaimStopIdentity
179 }
180 if rec.Target.Region != t.Region || rec.Target.Op != t.Op || rec.Target.Phase != t.Phase || rec.Target.DirectoryRevision != t.DirectoryRevision {
181 return hostbox.ErrChanged
182 }
183 return nil
184}
185
186func (h *ClaimLaunchRoot) start(ctx context.Context, peer StopPeer, q hostbox.RoleLaunchRequest) (hostbox.RoleLaunchGrant, error) {
187 if h.history == nil {
188 if h.generations != nil {
189 return hostbox.RoleLaunchGrant{}, os.ErrPermission
190 }
191 return h.startRecorded(ctx, peer, q, nil)
192 }
193 var out hostbox.RoleLaunchGrant
194 err := h.history(ctx, func(history *release.RootHistory) error {
195 var err error
196 out, err = h.startRecorded(ctx, peer, q, history)
197 return err
198 })
199 return out, err
200}
201
202func (h *ClaimLaunchRoot) startRecorded(ctx context.Context, peer StopPeer, q hostbox.RoleLaunchRequest, history *release.RootHistory) (hostbox.RoleLaunchGrant, error) {
203 r := *q.Reservation
204 t := r.Target
205 if t.BootID != peer.BootID || t.ControllerID != peer.ControllerID {
206 return hostbox.RoleLaunchGrant{}, ErrClaimStopIdentity
207 }
208 _, group, placementErr := claimLaunchPlacement(h.Units, t.Host)
209 if placementErr != nil || r.Group.RelativePath != group || !claimLaunchTier(q.MemoryMax) {
210 return hostbox.RoleLaunchGrant{}, hostbox.ErrRoleLaunchShape
211 }
212 actor, err := h.Actor(ctx)
213 if err != nil {
214 return hostbox.RoleLaunchGrant{}, err
215 }
216 if actor.Quarantined || actor.Box != t.Box || actor.Enrollment != t.Enrollment || actor.Region != t.Region || actor.Binding == "" || actor.Binding != t.DirectoryRevision {
217 return hostbox.RoleLaunchGrant{}, hostbox.ErrChanged
218 }
219 if err = h.Kernel.Verify(q.Executable, r.Executable); err != nil {
220 return hostbox.RoleLaunchGrant{}, err
221 }
222 var grant hostbox.RoleLaunchGrant
223 var binding release.HistoryBinding
224 if history != nil {
225 if h.Guard.Root == nil || h.installedProgram == nil {
226 return grant, ErrClaimLaunchUnavailable
227 }
228 view, err := h.Guard.Root.View(ctx, t.Host, hostguard.Captured{Incarnation: t.Incarnation, Stamp: t.Stamp})
229 if err != nil {
230 return grant, err
231 }
232 in := view.Installation
233 if in.Box != t.Box || in.Enrollment != t.Enrollment || in.InventoryRevision == 0 {
234 return grant, hostbox.ErrChanged
235 }
236 binding = release.HistoryBinding{Host: in.Host, Account: in.Account, App: in.App}
237 }
238 check := func(c *hostclaim.Claim) error { return h.authorize(ctx, peer, q, c) }
239 // The fence, the claim check, the journal intention, the unit start and
240 // the started record all happen under the host guard a root fence also
241 // takes, so no fence or second launch interleaves with them.
242 err = h.Guard.With(ctx, t.Host, hostguard.Captured{Incarnation: t.Incarnation, Stamp: t.Stamp}, check, func() (outcome error) {
243 if history != nil {
244 if err := history.ConsumeCreation(binding); err != nil {
245 return err
246 }
247 }
248 var pin *generationLaunchPermit
249 if h.generations != nil {
250 var err error
251 pin, err = h.generations.reserve(ctx, h, peer, q)
252 if err != nil {
253 return err
254 }
255 defer pin.ledger.Store.Close()
256 if pin.closeUpdate != nil {
257 defer pin.closeUpdate()
258 }
259 if pin.finishUpdate != nil {
260 defer func() {
261 finished := pin.finishUpdate(grant.UnitInvocationID, outcome, func() error {
262 started := instancectl.ClaimUnitStarted{Unit: grant.Unit, InvocationID: grant.UnitInvocationID, MainPID: grant.PID, ControlGroup: grant.Group.RelativePath}
263 observed, err := h.waitReady(ctx, r.Target.Host, started, r.Executable)
264 if err != nil {
265 return err
266 }
267 if observed.ProcessStart != grant.ProcessStart || observed.Group.Device != grant.Group.Device || observed.Group.Inode != grant.Group.Inode {
268 return hostbox.ErrChanged
269 }
270 return h.Units.Same(ctx, started)
271 })
272 if finished != nil && outcome == nil && grant.UnitInvocationID != "" {
273 cleanup, cancel := context.WithTimeout(context.WithoutCancel(ctx), 2*time.Minute)
274 defer cancel()
275 stopped := claimStopInvocation(cleanup, h.Units, t.Host, grant.UnitInvocationID)
276 if stopped == nil {
277 stopped = h.settleGone(cleanup, t, r.ID, grant.UnitInvocationID)
278 }
279 finished = errors.Join(finished, stopped)
280 }
281 outcome = errors.Join(outcome, finished)
282 }()
283 }
284 }
285 var e error
286 grant, e = h.launchLocked(ctx, peer, q, r, pin)
287 if e == nil && history != nil {
288 started := instancectl.ClaimUnitStarted{Unit: grant.Unit, InvocationID: grant.UnitInvocationID, MainPID: grant.PID, ControlGroup: grant.Group.RelativePath}
289 observed, err := h.waitReady(ctx, r.Target.Host, started, r.Executable)
290 if err != nil {
291 return err
292 }
293 if observed.ProcessStart != grant.ProcessStart {
294 return hostbox.ErrChanged
295 }
296 program, err := h.installedProgram(ctx, q, started, observed)
297 if err != nil {
298 return err
299 }
300 fresh, err := h.Guard.Claims.Read(ctx, t.Host)
301 if err != nil {
302 return err
303 }
304 if fresh.Absent {
305 return hostbox.ErrChanged
306 }
307 if err := h.authorize(ctx, peer, q, fresh.Claim); err != nil {
308 return err
309 }
310 if err := history.RecordInstalledProgram(binding, program); err != nil {
311 return err
312 }
313 }
314 return e
315 })
316 return grant, err
317}
318
319func (h *ClaimLaunchRoot) launchLocked(ctx context.Context, peer StopPeer, q hostbox.RoleLaunchRequest, r hostbox.LaunchReservation, pin *generationLaunchPermit) (hostbox.RoleLaunchGrant, error) {
320 t := r.Target
321 if err := h.launchRecord(ctx, peer, t); err != nil {
322 return hostbox.RoleLaunchGrant{}, err
323 }
324 // Root has proved there is no run unit, while holding the host guard.
325 // Spend old intentions now, before a replacement can acquire the unit name.
326 // Otherwise a late stop of an intention with no invocation ID could stop it.
327 intentions, err := h.Journal.UnitIntentions(ctx, t.Host)
328 if err != nil {
329 return hostbox.RoleLaunchGrant{}, err
330 }
331 for _, old := range intentions {
332 record, err := h.Journal.Read(ctx, old.Target)
333 if err != nil {
334 return hostbox.RoleLaunchGrant{}, err
335 }
336 if err := validatePersistedUnitContainment(ctx, record, h.Units); err != nil {
337 return hostbox.RoleLaunchGrant{}, err
338 }
339 if old.Group.RelativePath != r.Group.RelativePath {
340 return hostbox.RoleLaunchGrant{}, hostbox.ErrChanged
341 }
342 }
343 // Refuse a busy unit before journaling, so a refusal leaves nothing
344 // pending in the session's stop record.
345 if err := h.Units.Gone(ctx, t.Host, ""); err != nil {
346 return hostbox.RoleLaunchGrant{}, errors.Join(hostbox.ErrChanged, err)
347 }
348 for _, old := range intentions {
349 if old.Group.RelativePath != r.Group.RelativePath {
350 return hostbox.RoleLaunchGrant{}, hostbox.ErrChanged
351 }
352 if err := h.Journal.SettleUnit(ctx, old.Target, old.ID); err != nil {
353 return hostbox.RoleLaunchGrant{}, err
354 }
355 }
356 if err := h.Journal.IntendUnit(ctx, r); err != nil {
357 return hostbox.RoleLaunchGrant{}, err
358 }
359 cleanup, cancel := context.WithTimeout(context.WithoutCancel(ctx), 2*time.Minute)
360 defer cancel()
361 launch, err := h.launchSpec(q)
362 if err != nil {
363 return hostbox.RoleLaunchGrant{}, errors.Join(err, h.settleGone(cleanup, t, r.ID, ""))
364 }
365 permit, err := h.execPermit(ctx, q)
366 if err != nil {
367 return hostbox.RoleLaunchGrant{}, errors.Join(err, h.settleGone(cleanup, t, r.ID, ""))
368 }
369 started, err := h.Units.StartReserved(ctx, instancectl.ClaimUnit{Host: t.Host, Executable: q.Executable, MemoryMax: q.MemoryMax}, launch, permit, func() error {
370 // Check before starting the unit and again when its authenticated
371 // trampoline reaches ForkExec, under this same host guard.
372 fresh, err := h.Guard.Claims.Read(ctx, t.Host)
373 if err != nil {
374 return err
375 }
376 if fresh.Absent || fresh.Host != t.Host {
377 return hostbox.ErrChanged
378 }
379 in, err := h.Journal.Read(ctx, t)
380 if err != nil {
381 return err
382 }
383 if in.WritersClosed {
384 return hostbox.ErrChanged
385 }
386 rr, err := h.Journal.UnitReservation(ctx, t, r.ID)
387 if err != nil {
388 return err
389 }
390 if rr.State != hostbox.UnitIntended || !sameUnitRequest(rr, r) {
391 return hostbox.ErrChanged
392 }
393 if err := h.authorize(ctx, peer, q, fresh.Claim); err != nil {
394 return err
395 }
396 if err := pin.check(); err != nil {
397 return err
398 }
399 _, boot, err := h.Clock.Now()
400 if err != nil {
401 return err
402 }
403 if boot >= permit.SafeUntil {
404 return execlease.ErrLost
405 }
406 return nil
407 })
408 if err != nil {
409 return hostbox.RoleLaunchGrant{}, errors.Join(err, h.settleGone(cleanup, t, r.ID, ""))
410 }
411 observed, err := h.waitReady(ctx, r.Target.Host, started, r.Executable)
412 if err == nil && (started.ControlGroup != r.Group.RelativePath || observed.Executable != r.Executable || observed.ProcessStart == "" || observed.Group.Device == 0 || observed.Group.Inode == 0) {
413 err = hostbox.ErrChanged
414 }
415 group := hostbox.LaunchGroup{RelativePath: r.Group.RelativePath, Device: observed.Group.Device, Inode: observed.Group.Inode, InvocationID: started.InvocationID}
416 if err == nil {
417 err = h.Journal.StartedUnit(ctx, r, group, started.MainPID, observed.ProcessStart)
418 }
419 if err == nil {
420 err = pin.started(hostbox.RoleLaunchGrant{PID: started.MainPID, ProcessStart: observed.ProcessStart, Group: group, UnitInvocationID: started.InvocationID}, observed)
421 }
422 if err != nil {
423 // A replacement detected while waiting is never stopped by the old
424 // reservation. Settle only if root can prove the old invocation gone.
425 var stopErr error
426 if sameErr := h.Units.Same(cleanup, started); sameErr != nil {
427 stopErr = h.settleGone(cleanup, t, r.ID, started.InvocationID)
428 } else {
429 stopErr = claimStopInvocation(cleanup, h.Units, t.Host, started.InvocationID)
430 if stopErr == nil {
431 stopErr = h.settleGone(cleanup, t, r.ID, started.InvocationID)
432 }
433 }
434 return hostbox.RoleLaunchGrant{}, errors.Join(err, stopErr)
435 }
436 return hostbox.RoleLaunchGrant{ReservationID: r.ID, Unit: started.Unit, UnitInvocationID: started.InvocationID, PID: started.MainPID, ProcessStart: observed.ProcessStart, Group: group}, nil
437}
438
439// settleGone settles a unit reservation only once root proved its
440// invocation ended. Otherwise it stays pending and the stop record keeps
441// refusing a receipt.
442func (h *ClaimLaunchRoot) settleGone(ctx context.Context, t hostguard.FenceTarget, id, invocation string) error {
443 if err := h.Units.Gone(ctx, t.Host, invocation); err != nil {
444 return err
445 }
446 return h.Journal.SettleUnit(ctx, t, id)
447}
448
449func sameUnitRequest(old, r hostbox.LaunchReservation) bool {
450 return old.Role == r.Role && old.InvocationID == r.InvocationID && old.Inputs == r.Inputs && old.Executable == r.Executable &&
451 old.Group.RelativePath == r.Group.RelativePath && hostbox.SameLaunchOwner(old.Target, r.Target)
452}
453
454// stop ends one reservation for the live controller that owns it. Stopping
455// is always allowed, fenced or not; it needs only the host lock, so it
456// serializes with a launch. A reservation root never saw is spent.
457func (h *ClaimLaunchRoot) stop(ctx context.Context, peer StopPeer, r hostbox.LaunchReservation) error {
458 t := r.Target
459 if err := h.checkUnitBinding(ctx, t, true); err != nil {
460 return err
461 }
462 _, group, err := claimLaunchPlacement(h.Units, t.Host)
463 if err != nil || r.Group.RelativePath != group {
464 return hostbox.ErrRoleLaunchShape
465 }
466 if t.BootID != peer.BootID || t.ControllerID != peer.ControllerID {
467 return ErrClaimStopIdentity
468 }
469 return h.Guard.WithLocal(ctx, t.Host, func() error {
470 rec, err := h.Journal.Read(ctx, t)
471 if err != nil {
472 return err
473 }
474 if err := validatePersistedUnitContainment(ctx, rec, h.Units); err != nil {
475 return err
476 }
477 if rec.ControllerPID != peer.PID || rec.ProcessStart != peer.ProcessStart {
478 return ErrClaimStopIdentity
479 }
480 for _, old := range rec.Reservations {
481 if old.ID != r.ID {
482 continue
483 }
484 if !hostbox.IsUnitReservation(old) || !sameUnitRequest(old, r) {
485 return hostbox.ErrChanged
486 }
487 if old.State == hostbox.UnitSettled {
488 return nil
489 }
490 if err := h.Units.Gone(ctx, t.Host, old.Group.InvocationID); err == nil {
491 return h.Journal.SettleUnit(ctx, t, r.ID)
492 } else if !errors.Is(err, instancectl.ErrClaimUnitBusy) {
493 return err
494 }
495 if err := claimStopInvocation(ctx, h.Units, t.Host, old.Group.InvocationID); err != nil {
496 return err
497 }
498 return h.settleGone(ctx, t, r.ID, old.Group.InvocationID)
499 }
500 return h.Journal.TombstoneUnit(ctx, r)
501 })
502}
503
504func claimLaunchStatus(err error) int {
505 switch {
506 case errors.Is(err, activator.ErrClaimIsolationUnavailable):
507 return http.StatusServiceUnavailable
508 case errors.Is(err, hostbox.ErrRoleLaunchShape):
509 return http.StatusBadRequest
510 case errors.Is(err, ErrClaimStopIdentity):
511 return http.StatusForbidden
512 }
513 return http.StatusConflict
514}
515
516// ServeHTTP is POST /claim/launch on the root instance-control socket,
517// mounted only in claim mode. The peer is authenticated before a byte of the
518// body is read.
519func (h *ClaimLaunchRoot) ServeHTTP(w http.ResponseWriter, r *http.Request) {
520 if h == nil || h.Authenticate == nil || r.Method != http.MethodPost || r.URL.Path != "/claim/launch" {
521 http.Error(w, "refused", http.StatusServiceUnavailable)
522 return
523 }
524 conn := instancectl.RequestConn(r.Context())
525 if conn == nil {
526 http.Error(w, "refused", http.StatusForbidden)
527 return
528 }
529 peer, err := h.Authenticate(r.Context(), conn)
530 if err != nil {
531 http.Error(w, "refused", http.StatusForbidden)
532 return
533 }
534 q, err := hostbox.DecodeRoleLaunchRequest(r.Body)
535 if err != nil {
536 http.Error(w, "refused", http.StatusBadRequest)
537 return
538 }
539 out, err := h.Serve(r.Context(), peer, q)
540 if err != nil {
541 http.Error(w, "refused", claimLaunchStatus(err))
542 return
543 }
544 w.Header().Set("Content-Type", "application/json")
545 w.WriteHeader(http.StatusOK)
546 _ = json.NewEncoder(w).Encode(out)
547}
548
549// claimUnitStopKernel puts the isolated units in root's stop evidence. It
550// settles a unit reservation only once root proves its invocation ended,
551// before the wrapped verifier scans the session, and reports the settled
552// invocations. A unit it cannot prove gone refuses the receipt.
553type claimUnitStopKernel struct {
554 Inner StopKernelVerifier
555 Journal *hostbox.LaunchJournal
556 Units ClaimLaunchUnits
557}
558
559func (k claimUnitStopKernel) settleUnits(ctx context.Context, t hostguard.FenceTarget) ([]string, error) {
560 if k.Inner == nil || k.Journal == nil || k.Units == nil {
561 return nil, ErrClaimStopUnavailable
562 }
563 in, err := k.Journal.Read(ctx, t)
564 if err != nil {
565 return nil, err
566 }
567 if err := validatePersistedUnitContainment(ctx, in, k.Units); err != nil {
568 return nil, err
569 }
570 ids := []string{}
571 for _, r := range in.Reservations {
572 if !hostbox.IsUnitReservation(r) {
573 continue
574 }
575 if r.State != hostbox.UnitSettled {
576 if err := k.Units.Gone(ctx, t.Host, r.Group.InvocationID); err != nil {
577 return nil, err
578 }
579 if err := k.Journal.SettleUnit(ctx, t, r.ID); err != nil {
580 return nil, err
581 }
582 }
583 if r.Group.InvocationID != "" {
584 ids = append(ids, r.Group.InvocationID)
585 }
586 }
587 sort.Strings(ids)
588 return ids, nil
589}
590
591func (k claimUnitStopKernel) VerifyStopped(ctx context.Context, t hostguard.FenceTarget, inv StopInventory, ack DrainAcknowledgement) (StopKernelEvidence, error) {
592 ids, err := k.settleUnits(ctx, t)
593 if err != nil {
594 return StopKernelEvidence{}, err
595 }
596 evidence, err := k.Inner.VerifyStopped(ctx, t, inv, ack)
597 if err != nil {
598 return StopKernelEvidence{}, err
599 }
600 evidence.UnitInvocationIDs = append(evidence.UnitInvocationIDs, ids...)
601 return evidence, nil
602}
603
604func (k claimUnitStopKernel) VerifyUnjournaled(ctx context.Context, t hostguard.FenceTarget, inv StopInventory) (StopKernelEvidence, error) {
605 kernel, ok := k.Inner.(interface {
606 VerifyUnjournaled(context.Context, hostguard.FenceTarget, StopInventory) (StopKernelEvidence, error)
607 })
608 if !ok {
609 return StopKernelEvidence{}, ErrClaimStopUnavailable
610 }
611 return kernel.VerifyUnjournaled(ctx, t, inv)
612}
613
614func (k claimUnitStopKernel) RecoverStopped(ctx context.Context, t hostguard.FenceTarget, in StopInventory) (StopKernelEvidence, DrainAcknowledgement, error) {
615 recovery, ok := k.Inner.(stopRecoveryKernel)
616 if !ok {
617 return StopKernelEvidence{}, DrainAcknowledgement{}, ErrClaimStopUnavailable
618 }
619 if controller, ok := k.Inner.(interface {
620 CheckRecoveryController(context.Context, hostguard.FenceTarget) error
621 }); ok {
622 if err := controller.CheckRecoveryController(ctx, t); err != nil {
623 return StopKernelEvidence{}, DrainAcknowledgement{}, err
624 }
625 }
626 ids, err := k.settleUnits(ctx, t)
627 if err != nil {
628 return StopKernelEvidence{}, DrainAcknowledgement{}, err
629 }
630 evidence, ack, err := recovery.RecoverStopped(ctx, t, in)
631 if err != nil {
632 return StopKernelEvidence{}, DrainAcknowledgement{}, err
633 }
634 evidence.UnitInvocationIDs = append(evidence.UnitInvocationIDs, ids...)
635 return evidence, ack, nil
636}
637