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 "fmt"
6
7 "github.com/carlosframework/platform/internal/hostclaim"
8)
9
10// RelocationReleaseObligation is the stop request's obligation component
11// for a relocation's release (§5.8 release step 4). A release retires
12// nothing, so it names no park-local chore; root's FenceAndStop requires
13// the component, and the receipt carries it so a retirement stop can never
14// be offered as a release, nor a release as a retirement.
15const RelocationReleaseObligation = "relocation-release"
16
17// StopReceiptReleases is hostclaim.ReleaseAuthority over main's
18// relocation-release discharge (claim Plan 11): a source's journaled
19// release counts only once RelocationReleaseReceipts.Complete has verified
20// root's exact receipt, journaled its key and digest, and discharged the
21// obligation. The release must cite exactly that key and digest, the claim
22// must owe this operation no relocation release any more, and the receipt
23// is verified again here, at the pin, by the region worker (Plan 11 Task
24// E: the adapter consumes the same signed receipt, never an agent-written
25// digest alone). That check is main's own: Complete's replay form, which
26// re-reads the bytes and checks the digest, the canonical form, every
27// bound field, the roster entry and root's signature under the releasing
28// box's census key, and writes nothing once the journal holds the key and
29// digest and no obligation is owed. A verify-only export from main
30// (RelocationReleaseReceipts.Verify) would replace the replay call.
31//
32// Main discharges a relocation-release only for the source in releasing.
33// A failed target's release in probe-failed has no obligation root will
34// stop for, so it is refused here: the nested relocation waits, and its
35// release deadline makes it stuck.
36type StopReceiptReleases struct {
37 Receipts RelocationReleaseReceipts
38}
39
40var _ hostclaim.ReleaseAuthority = StopReceiptReleases{}
41
42func (a StopReceiptReleases) VerifyRelease(ctx context.Context, c hostclaim.Claim, r hostclaim.Release) error {
43 if c.Holder == nil || a.Receipts.Store == nil {
44 return ErrClaimStopUnavailable
45 }
46 if err := releaseDischarged(&c, r.Receipt, r.ReceiptSHA256); err != nil {
47 return err
48 }
49 return verifyDischargedReceipt(ctx, a.Receipts, &c, r.Receipt, r.ReceiptSHA256)
50}
51
52// releaseDischarged checks c journals key and digest as its relocating
53// operation's discharged release receipt, and owes that operation no
54// relocation release.
55func releaseDischarged(c *hostclaim.Claim, key, digest string) error {
56 if c.Op.Kind != hostclaim.KindRelocating || c.Op.Phase != hostclaim.PhaseReleasing || c.Op.ID == "" {
57 return fmt.Errorf("%w: only a source's release in releasing is discharged by root's receipt", ErrClaimStopReceipt)
58 }
59 if key == "" || c.Op.Journal["release_stop_receipt"] != key || c.Op.Journal["release_stop_sha256"] != digest {
60 return fmt.Errorf("%w: the release does not cite the receipt that discharged it", ErrClaimStopReceipt)
61 }
62 for _, ob := range c.Obligations {
63 if ob.Kind == RelocationReleaseObligation && ob.Op == c.Op.ID {
64 return fmt.Errorf("%w: the relocation release is still owed", ErrClaimStopReceipt)
65 }
66 }
67 return nil
68}
69
70// verifyDischargedReceipt re-verifies a discharged receipt with main's
71// RelocationReleaseReceipts.Complete in its replay form. The caller has
72// checked releaseDischarged, so Complete takes its replay branch and
73// writes nothing; the observation's ETag is never compared there, and an
74// unexpected write could only fail its CAS against it.
75func verifyDischargedReceipt(ctx context.Context, receipts RelocationReleaseReceipts, c *hostclaim.Claim, key, digest string) error {
76 _, err := receipts.Complete(ctx, hostclaim.Observed{Host: c.Host, Claim: c, ETag: "verify-only"}, key, digest)
77 return err
78}
79
80// RelocationReleaseDischarge is the relocation-release obligation's two
81// claim CASes around root's stop (claim Plan 11): Queue before the stop is
82// requested, Complete with root's exact receipt after it.
83type RelocationReleaseDischarge interface {
84 Queue(context.Context, hostclaim.Observed) (hostclaim.Observed, error)
85 Complete(ctx context.Context, observed hostclaim.Observed, key, digest string) (hostclaim.Observed, error)
86 // Verify re-verifies a receipt observed has already discharged, and
87 // writes nothing.
88 Verify(ctx context.Context, observed hostclaim.Observed, key, digest string) error
89}
90
91// ReleaseObligation is production's RelocationReleaseDischarge: main's
92// QueueRelocationRelease and RelocationReleaseReceipts.Complete, over the
93// receipts' store (the claims' store).
94type ReleaseObligation struct {
95 Receipts RelocationReleaseReceipts
96}
97
98var _ RelocationReleaseDischarge = ReleaseObligation{}
99
100// relocationReleaseRetirement is the obligation's stable retirement
101// identity: one release of one writer session of one installation, under
102// one operation.
103func relocationReleaseRetirement(c *hostclaim.Claim) string {
104 return fmt.Sprintf("%s/%d/%d", c.Op.ID, c.Holder.Stamp, c.Holder.Session)
105}
106
107// Queue records the obligation for obs's release, once. An identical
108// obligation already owed, or one already discharged by this operation's
109// receipt, leaves the claim as it is; any other refuses.
110func (o ReleaseObligation) Queue(ctx context.Context, obs hostclaim.Observed) (hostclaim.Observed, error) {
111 if o.Receipts.Store == nil || obs.Absent || obs.Claim == nil || obs.Claim.Holder == nil {
112 return hostclaim.Observed{}, ErrClaimStopUnavailable
113 }
114 c := obs.Claim
115 if c.Op.Journal["release_stop_receipt"] != "" {
116 return obs, nil // discharged: root replays the receipt, Complete finds it done
117 }
118 queue := hostclaim.QueueRelocationRelease(c.Op.ID, c.Op.Exec, c.Op.Intent["region"], relocationReleaseRetirement(c))
119 for _, ob := range c.Obligations {
120 if ob.ID == RelocationReleaseObligation {
121 // Already owed: the same obligation passes unchanged (Queue
122 // returns before writing), any other refuses.
123 return obs, queue(c)
124 }
125 }
126 return hostclaim.Update(ctx, o.Receipts.Store, obs, queue)
127}
128
129// Complete discharges the obligation with root's receipt on a fresh read.
130func (o ReleaseObligation) Complete(ctx context.Context, obs hostclaim.Observed, key, digest string) (hostclaim.Observed, error) {
131 if o.Receipts.Store == nil {
132 return hostclaim.Observed{}, ErrClaimStopUnavailable
133 }
134 fresh, err := hostclaim.Read(ctx, o.Receipts.Store, obs.Host)
135 if err != nil {
136 return hostclaim.Observed{}, err
137 }
138 return o.Receipts.Complete(ctx, fresh, key, digest)
139}
140
141// Verify re-verifies the discharged receipt key and digest name on
142// observed's claim, with main's Complete in its replay form.
143func (o ReleaseObligation) Verify(ctx context.Context, observed hostclaim.Observed, key, digest string) error {
144 if o.Receipts.Store == nil || observed.Claim == nil || observed.Claim.Holder == nil {
145 return ErrClaimStopUnavailable
146 }
147 if err := releaseDischarged(observed.Claim, key, digest); err != nil {
148 return err
149 }
150 return verifyDischargedReceipt(ctx, o.Receipts, observed.Claim, key, digest)
151}
152