carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package hostops
2
3import (
4 "bytes"
5 "context"
6 "encoding/json"
7
8 "github.com/carlosframework/platform/internal/hostclaim"
9 "github.com/carlosframework/platform/internal/provision"
10 "github.com/carlosframework/platform/internal/release"
11)
12
13// The console move grant (ruling J17, claim Plan 10). On a host-claim
14// console the cut, abort and retry doors write the move journal on the
15// record. None of them may run under an app permit, a lifecycle grant or a
16// callback: each is one typed ControlWriter transition, chosen here from a
17// closed action set, bound to the exact claim, registration and record the
18// door observed, and checked again at its final conditional write.
19
20// ConsoleMoveAction names one console-initiated move record transition.
21type ConsoleMoveAction string
22
23const (
24 // ConsoleMoveCut is provision.MoveGuardedCut: ready to cutting/requested.
25 ConsoleMoveCut ConsoleMoveAction = "cut"
26 // ConsoleMoveAbort is provision.MoveGuardedAbort (§7.11).
27 ConsoleMoveAbort ConsoleMoveAction = "abort"
28 // ConsoleMoveRetry is provision.MoveRetry: stuck back to its recovery.
29 ConsoleMoveRetry ConsoleMoveAction = "retry"
30)
31
32// ConsoleMoveObservation is what a door read: values only, never authority.
33// ConsoleMove rereads and compares every field.
34type ConsoleMoveObservation struct {
35 Account, App, AppID, Host, Incarnation, OpID string
36 ClaimETag, RecordETag string
37 RecordSeq uint64
38}
39
40// ObserveConsoleMove reads the claim and record a door is about to act on.
41// The claim must hold this account and app's instance under a move op whose
42// admission id is its own id, and the record must journal that same move.
43func ObserveConsoleMove(ctx context.Context, s release.Store, account, app, host string) (ConsoleMoveObservation, error) {
44 if s == nil || account == "" || app == "" || host == "" {
45 return ConsoleMoveObservation{}, ErrIdentity
46 }
47 o, err := hostclaim.Read(ctx, s, host)
48 if err != nil {
49 return ConsoleMoveObservation{}, err
50 }
51 if o.Absent || o.Claim == nil || o.Claim.Holder == nil || o.ETag == "" {
52 return ConsoleMoveObservation{}, ErrIdentity
53 }
54 c, h := o.Claim, o.Claim.Holder
55 if h.Family != "instance" || h.Key != provision.RecordKey(account, app, host) || h.Account != account || h.App != app || c.Op.ID == "" || c.Op.Intent["admission_id"] != c.Op.ID || c.Op.Intent["app_id"] == "" {
56 return ConsoleMoveObservation{}, ErrIdentity
57 }
58 r, _, seq, err := consoleMoveRecord(ctx, s, h, c.Op.ID)
59 if err != nil {
60 return ConsoleMoveObservation{}, err
61 }
62 return ConsoleMoveObservation{Account: account, App: app, AppID: c.Op.Intent["app_id"], Host: host, Incarnation: h.Incarnation, OpID: c.Op.ID,
63 ClaimETag: o.ETag, RecordETag: r.ETag, RecordSeq: seq}, nil
64}
65
66// consoleMoveRecord reads the holder's record and requires it to journal
67// move id for the holder's incarnation.
68func consoleMoveRecord(ctx context.Context, s release.Store, h *hostclaim.Holder, id string) (Record, provision.Record, uint64, error) {
69 if h == nil || h.Family != "instance" {
70 return Record{}, provision.Record{}, 0, ErrIdentity
71 }
72 r, err := ReadRecord(ctx, s, h.Key)
73 if err != nil {
74 return Record{}, provision.Record{}, 0, err
75 }
76 if !matches(r, h) {
77 return Record{}, provision.Record{}, 0, ErrIdentity
78 }
79 _, seq, err := recordSequence(r.Body)
80 if err != nil {
81 return Record{}, provision.Record{}, 0, err
82 }
83 rec, err := provision.DecodeRecord(r.Body)
84 if err != nil {
85 return Record{}, provision.Record{}, 0, ErrIdentity
86 }
87 if rec.Move == nil || rec.Move.ID != id || rec.Move.Incarnation != h.Incarnation {
88 return Record{}, provision.Record{}, 0, ErrIdentity
89 }
90 return r, rec, seq, nil
91}
92
93// consoleMoveTransition maps the closed action set to its provision
94// transition and the claim operation it is legal under: cut and abort
95// before the seal (moving, phase ""), retry under a recovery (restoring or
96// reverting, where the record's stuck lives).
97func consoleMoveTransition(a ConsoleMoveAction, g provision.MoveGuard, op hostclaim.Op) (func(*provision.Record) error, error) {
98 switch a {
99 case ConsoleMoveCut, ConsoleMoveAbort:
100 if op.Kind != hostclaim.KindMoving || op.Phase != hostclaim.PhaseMoving {
101 return nil, hostclaim.ErrWrongState
102 }
103 if a == ConsoleMoveCut {
104 return provision.MoveGuardedCut(g), nil
105 }
106 return provision.MoveGuardedAbort(g), nil
107 case ConsoleMoveRetry:
108 if op.Kind != hostclaim.KindRestoring && op.Kind != hostclaim.KindReverting {
109 return nil, hostclaim.ErrWrongState
110 }
111 return provision.MoveRetryRecovery(g), nil
112 }
113 return nil, ErrIdentity
114}
115
116// consoleMoveClaim rereads the claim and holds it to the observation: the
117// exact ETag, this account, app and incarnation, the move op under its own
118// admission id and app id, and the exact move registration (closed
119// admission and an expired registration still drain an accepted move).
120func (w *ControlWriter) consoleMoveClaim(ctx context.Context, o ConsoleMoveObservation) (hostclaim.Observed, error) {
121 obs, err := hostclaim.Read(ctx, w.raw, o.Host)
122 if err != nil {
123 return hostclaim.Observed{}, err
124 }
125 if obs.Absent || obs.Claim == nil || obs.Claim.Holder == nil {
126 return hostclaim.Observed{}, ErrIdentity
127 }
128 c, h := obs.Claim, obs.Claim.Holder
129 if !operationHomeMatches(w.homeRegion, c) {
130 return hostclaim.Observed{}, ErrIdentity
131 }
132 if h.Family != "instance" || h.Key != provision.RecordKey(o.Account, o.App, o.Host) || h.Account != o.Account || h.App != o.App || h.Incarnation != o.Incarnation || c.Op.ID != o.OpID ||
133 c.Op.Intent["admission_id"] != o.OpID || c.Op.Intent["app_id"] != o.AppID {
134 return hostclaim.Observed{}, ErrIdentity
135 }
136 if obs.ETag != o.ClaimETag {
137 return hostclaim.Observed{}, hostclaim.ErrConflict
138 }
139 if err := moveAdmission(ctx, w.raw, o.Host, c); err != nil {
140 return hostclaim.Observed{}, err
141 }
142 return obs, nil
143}
144
145// ConsoleMove applies one console move action to the record the observation
146// names. The transition is chosen here, never supplied; it runs against the
147// exact record (ETag and record_seq) the door read, and the claim and the
148// move registration are read again immediately before the conditional Put,
149// whose base is that record. Nothing but the record is written. A refusal
150// from the provision transition (provision.ErrMoveConflict) is returned
151// unchanged for the door's recorded sentence.
152func (w *ControlWriter) ConsoleMove(ctx context.Context, o ConsoleMoveObservation, action ConsoleMoveAction) (*provision.Record, error) {
153 if err := w.ready(); err != nil {
154 return nil, err
155 }
156 if o.Account == "" || o.App == "" || o.AppID == "" || o.Host == "" || o.Incarnation == "" || o.OpID == "" || o.ClaimETag == "" || o.RecordETag == "" {
157 return nil, ErrIdentity
158 }
159 switch action {
160 case ConsoleMoveCut, ConsoleMoveAbort, ConsoleMoveRetry:
161 default:
162 return nil, ErrIdentity
163 }
164 obs, err := w.consoleMoveClaim(ctx, o)
165 if err != nil {
166 return nil, err
167 }
168 h := obs.Claim.Holder
169 mutate, err := consoleMoveTransition(action, provision.MoveGuard{ID: o.OpID, Incarnation: o.Incarnation}, obs.Claim.Op)
170 if err != nil {
171 return nil, err
172 }
173 r, rec, seq, err := consoleMoveRecord(ctx, w.raw, h, o.OpID)
174 if err != nil {
175 return nil, err
176 }
177 if r.ETag != o.RecordETag || seq != o.RecordSeq {
178 return nil, release.ErrCASConflict
179 }
180 next, err := provision.DecodeRecord(r.Body)
181 if err != nil {
182 return nil, ErrIdentity
183 }
184 if err := mutate(&next); err != nil {
185 return nil, err
186 }
187 if next.Move != nil {
188 next.Move.UpdatedAt = w.now().UTC()
189 }
190 if next.Account != rec.Account || next.App != rec.App || next.Host != rec.Host || !next.CreatedAt.Equal(rec.CreatedAt) || next.RecordSeq != rec.RecordSeq || !bytes.Equal(next.Maintenance, rec.Maintenance) {
191 return nil, ErrIdentity
192 }
193 body, err := json.Marshal(next)
194 if err != nil {
195 return nil, err
196 }
197 if err := CheckCreateFreeze(ctx, w.raw, w.pub); err != nil {
198 return nil, err
199 }
200 // The final check: the same claim, the same registration, just before
201 // the Put whose If-Match is the observed record.
202 if _, err := w.consoleMoveClaim(ctx, o); err != nil {
203 return nil, err
204 }
205 _, written, err := provision.WriteRecord(provision.WithRecordSequence(ctx), w.raw, h.Key, r.Body, body, o.RecordETag)
206 if err != nil {
207 return nil, sequenceError(err)
208 }
209 next.RecordSeq = written
210 return &next, nil
211}
212