carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package hostguard
2
3import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "errors"
8 "io"
9 "os"
10 "strings"
11
12 "github.com/carlosframework/platform/internal/hostclaim"
13 "github.com/carlosframework/platform/internal/rootstate"
14)
15
16type InstallationRegistration struct {
17 Op, Kind, Phase, RowID string
18 Session uint64
19}
20
21type InstallationArtifacts struct {
22 Schema int
23 StateDirectory bool
24 Timers string
25}
26
27type RootInstallation struct {
28 Artifacts InstallationArtifacts `json:",omitzero"`
29 Registration InstallationRegistration `json:",omitempty"`
30 Empty bool
31 Host, Incarnation, Account, App, Record, Box, Enrollment string
32 Stamp, Migration, InventoryRevision uint64
33}
34type RootAuthority struct {
35 store rootstate.Store
36 inventoryRevision func(context.Context, string, Captured) (uint64, error)
37}
38
39func NewRootAuthority(store rootstate.Store) *RootAuthority { return &RootAuthority{store: store} }
40
41type rootRecord struct {
42 MigrationDenies []MigrationDeny `json:",omitempty"`
43 Quarantined bool `json:",omitempty"`
44 Settled bool `json:",omitempty"`
45 RebindCommit []byte `json:",omitempty"`
46 Schema int
47 Installation RootInstallation
48 Retired []Captured
49 Barrier *FenceTarget
50 PendingInventory []byte `json:",omitempty"`
51 PendingInstallation *RootInstallation `json:",omitempty"`
52 State record
53}
54
55func (a *RootAuthority) read(host string) (record, error) {
56 if a == nil || a.store == nil || validateFileHost(host) != nil {
57 return record{}, ErrInvalidInput
58 }
59 b, err := a.store.Read(host + ".json")
60 if errors.Is(err, os.ErrNotExist) {
61 return record{}, ErrNoRecord
62 }
63 if err != nil {
64 return record{}, err
65 }
66 var saved rootRecord
67 dec := json.NewDecoder(bytes.NewReader(b))
68 dec.DisallowUnknownFields()
69 if dec.Decode(&saved) != nil || dec.Decode(new(any)) != io.EOF || saved.Schema != 1 || saved.Installation.Host != host || saved.Installation.Incarnation != saved.State.Incarnation || saved.Installation.Migration == 0 {
70 return record{}, ErrMalformedRecord
71 }
72 r := saved.State
73 if r.ActiveStamp != 0 {
74 if saved.Installation.Stamp != r.ActiveStamp {
75 return record{}, ErrMalformedRecord
76 }
77 } else {
78 // Older retired records cleared both stamps. Recover only the last
79 // exact root-retained tuple, never an agent supplied installation.
80 if len(saved.Retired) == 0 {
81 return record{}, ErrMalformedRecord
82 }
83 last := saved.Retired[len(saved.Retired)-1]
84 if last.Incarnation != r.Incarnation || last.Stamp == 0 || (saved.Installation.Stamp != 0 && saved.Installation.Stamp != last.Stamp) {
85 return record{}, ErrMalformedRecord
86 }
87 saved.Installation.Stamp = last.Stamp
88 }
89 r.migrationDenies = saved.MigrationDenies
90 for _, deny := range r.migrationDenies {
91 if deny.Incarnation == "" || deny.Stamp == 0 || deny.Session == 0 {
92 return record{}, ErrMalformedRecord
93 }
94 }
95 if saved.Settled && !saved.Quarantined {
96 return record{}, ErrMalformedRecord
97 }
98 r.quarantined, r.settled = saved.Quarantined, saved.Settled
99 r.root = true
100 r.installation = saved.Installation
101 r.rootRetired = saved.Retired
102 r.barrier = saved.Barrier
103 r.rebindCommit = saved.RebindCommit
104 r.pendingInventory = saved.PendingInventory
105 r.pendingInstallation = saved.PendingInstallation
106 if (len(r.pendingInventory) > 0) != (r.pendingInstallation != nil) {
107 return record{}, ErrMalformedRecord
108 }
109 if err := r.validate(host); err != nil {
110 return record{}, err
111 }
112 return r, nil
113}
114func (a *RootAuthority) write(r record) error {
115 if a == nil || a.store == nil || !r.root || r.validate(r.Host) != nil {
116 return ErrMalformedRecord
117 }
118 r.installation.Host, r.installation.Incarnation = r.Host, r.Incarnation
119 if r.ActiveStamp != 0 {
120 r.installation.Stamp = r.ActiveStamp
121 }
122 b, err := json.Marshal(rootRecord{MigrationDenies: r.migrationDenies, Quarantined: r.quarantined, Settled: r.settled, RebindCommit: r.rebindCommit, Schema: 1, Installation: r.installation, Retired: r.rootRetired, Barrier: r.barrier, PendingInventory: r.pendingInventory, PendingInstallation: r.pendingInstallation, State: r})
123 if err != nil {
124 return err
125 }
126 return a.store.Write(r.Host+".json", b)
127}
128func (g Guard) Capture(host string) (Captured, error) {
129 if g.Root == nil {
130 return Capture(g.DataDir, host)
131 }
132 r, err := g.Root.read(host)
133 if err != nil {
134 return Captured{}, err
135 }
136 return Captured{Incarnation: r.Incarnation, Stamp: r.ActiveStamp}, nil
137}
138func (g Guard) EstablishRoot(ctx context.Context, in RootInstallation, check func(*hostclaim.Claim) error) error {
139 return g.CommitRootInstallation(ctx, in, check, func() error { return nil })
140}
141
142func (g Guard) CommitRootInstallation(ctx context.Context, in RootInstallation, check func(*hostclaim.Claim) error, commit func() error) error {
143 if commit == nil {
144 return ErrInvalidInput
145 }
146 return g.CommitRootInventoryArtifacts(ctx, in, nil, check, nil, func(RootInstallation) error { return commit() }, nil)
147}
148
149func (g Guard) CommitRootInventoryArtifacts(ctx context.Context, in RootInstallation, intent []byte, check func(*hostclaim.Claim) error, validate func(RootInstallation) error, commit func(RootInstallation) error, artifacts func(RootInstallation) (InstallationArtifacts, error)) error {
150 if g.Root == nil || check == nil || commit == nil || in.Host == "" || in.Incarnation == "" || in.Stamp == 0 || in.Migration == 0 || in.Account == "" || in.App == "" || in.Record == "" || in.Box == "" || in.Enrollment == "" {
151 return ErrInvalidInput
152 }
153 return g.locked(ctx, in.Host, func(d *recordDir) error {
154 c, err := g.fresh(ctx, in.Host)
155 if err != nil {
156 return err
157 }
158 if err := check(c); err != nil {
159 return err
160 }
161 if c.Holder.Incarnation != in.Incarnation || c.Holder.Stamp != in.Stamp || c.Holder.Account != in.Account || c.Holder.App != in.App {
162 return ErrClaimIncarnation
163 }
164 r, err := d.read(in.Host)
165 if err != nil && !errors.Is(err, ErrNoRecord) {
166 return err
167 }
168 if errors.Is(err, ErrNoRecord) {
169 r = record{V: 1, Host: in.Host, Incarnation: in.Incarnation, root: true}
170 }
171 want := Captured{Incarnation: in.Incarnation, Stamp: in.Stamp}
172 for _, old := range r.rootRetired {
173 if old == want {
174 return ErrRetiredStamp
175 }
176 }
177 if r.Incarnation == in.Incarnation && r.ActiveStamp == in.Stamp {
178 prior := r.installation
179 if validate != nil {
180 if err := validate(prior); err != nil {
181 return err
182 }
183 }
184 if prior.Account != in.Account || prior.App != in.App || prior.Record != in.Record || prior.Box != in.Box || prior.Enrollment != in.Enrollment || prior.Migration != in.Migration || prior.InventoryRevision > in.InventoryRevision {
185 return ErrRecordIncarnation
186 }
187 }
188 if migrationAdmissionDenied(r.migrationDenies, r.FenceJournal, want, c.Holder.Session) {
189 return ErrFenced
190 }
191 if r.ActiveFence != nil && r.ActiveFence.matchesSession(c, want) {
192 return ErrFenced
193 }
194 for _, receipt := range r.FenceJournal {
195 if receipt.Target.matchesSession(c, want) {
196 return ErrFenced
197 }
198 }
199 if r.barrier != nil {
200 return ErrFenced
201 }
202 if r.Incarnation == in.Incarnation && r.ActiveStamp > in.Stamp {
203 return ErrOldStamp
204 }
205 prior := r.installation
206 if prior.Incarnation == in.Incarnation && prior.Stamp == in.Stamp && in.Registration == (InstallationRegistration{}) {
207 in.Registration = prior.Registration
208 }
209 if artifacts != nil {
210 in.Artifacts, err = artifacts(prior)
211 if err != nil {
212 return err
213 }
214 } else if prior.Incarnation == in.Incarnation && in.Artifacts == (InstallationArtifacts{}) {
215 in.Artifacts = prior.Artifacts
216 }
217 if r.pendingInstallation != nil && (*r.pendingInstallation != in || !bytes.Equal(r.pendingInventory, intent)) {
218 return ErrFenced
219 }
220 if validate != nil {
221 if err := validate(prior); err != nil {
222 return err
223 }
224 }
225 if len(intent) > 0 {
226 r.pendingInventory = append([]byte(nil), intent...)
227 pending := in
228 r.pendingInstallation = &pending
229 if r.ActiveStamp == 0 {
230 r.ActiveStamp = in.Stamp
231 r.installation = in
232 r.installation.InventoryRevision = 0
233 }
234 if err := d.write(r); err != nil {
235 return err
236 }
237 }
238 if err := commit(prior); err != nil {
239 return err
240 }
241 if r.Incarnation != in.Incarnation || r.ActiveStamp != in.Stamp {
242 r.retire()
243 r.RetiredStamps = nil
244 }
245 r.pendingInventory, r.pendingInstallation = nil, nil
246 r.Incarnation, r.ActiveStamp, r.installation = in.Incarnation, in.Stamp, in
247 r.quarantined, r.settled = false, false
248 return d.write(r)
249 })
250}
251
252func NewRootAuthorityWithInventory(store rootstate.Store, revision func(context.Context, string, Captured) (uint64, error)) *RootAuthority {
253 return &RootAuthority{store: store, inventoryRevision: revision}
254}
255func (g Guard) refreshRootInventory(ctx context.Context, r *record) error {
256 if g.Root == nil || g.Root.inventoryRevision == nil {
257 return nil
258 }
259 revision, err := g.Root.inventoryRevision(ctx, r.Host, Captured{Incarnation: r.Incarnation, Stamp: r.ActiveStamp})
260 if err != nil {
261 return err
262 }
263 if revision == 0 {
264 return ErrNoRecord
265 }
266 r.installation.InventoryRevision = revision
267 return nil
268}
269
270func (a *RootAuthority) PendingInstallations() ([][]byte, error) {
271 names, err := a.store.Names()
272 if err != nil {
273 return nil, err
274 }
275 var out [][]byte
276 for _, name := range names {
277 if !strings.HasSuffix(name, ".json") {
278 continue
279 }
280 r, err := a.read(strings.TrimSuffix(name, ".json"))
281 if err != nil {
282 return nil, err
283 }
284 if r.pendingInstallation != nil {
285 out = append(out, append([]byte(nil), r.pendingInventory...))
286 }
287 }
288 return out, nil
289}
290
291func (a *RootAuthority) PendingInstallation(ctx context.Context, host string) (RootInstallation, []byte, error) {
292 if err := ctx.Err(); err != nil {
293 return RootInstallation{}, nil, err
294 }
295 r, err := a.read(host)
296 if err != nil {
297 return RootInstallation{}, nil, err
298 }
299 if r.pendingInstallation == nil {
300 return RootInstallation{}, nil, ErrNoRecord
301 }
302 return *r.pendingInstallation, append([]byte(nil), r.pendingInventory...), nil
303}
304
305// AbandonRootInventory resolves a pending root inventory intent that a
306// fresh claim proves superseded: the exact intent bytes, then superseded
307// over the guard's fresh claim read, under the host lock. The installation
308// ends in the same terminal state as an obsolete-intent settlement
309// (resolveRootInventory): quarantined and settled, still refused for every
310// execution and admission, inspectable by the migration census, with a
311// landed commit's revision reconciled first (fix round 2: an abandoned
312// installation no longer blocks cleanup startup on every restart).
313func (g Guard) AbandonRootInventory(ctx context.Context, host string, intent []byte, superseded func(*hostclaim.Claim) bool, applied func(context.Context, RootInstallation) (bool, error)) error {
314 if superseded == nil {
315 return ErrInvalidInput
316 }
317 return g.resolveRootInventory(ctx, host, intent, superseded, applied)
318}
319
320// SettleObsoleteRootInventory settles a pending root inventory intent that
321// root's startup found obsolete: stamped under a roster binding other than
322// the startup binding (roster binding N3). The binding difference alone
323// supersedes it, so no claim is consulted. Under the host lock, with the
324// exact intent bytes, the intent is cleared and the installation is
325// quarantined and marked settled: every execution and admission path still
326// refuses it, and only the refusal-only migration census may inspect it.
327//
328// applied reports whether the pending installation's inventory already
329// holds this intent: root's commit landed and only the final record write
330// was lost. The record then takes the installation that commit would have
331// recorded, exactly as CommitRootInventoryArtifacts records it, so the
332// guard and the inventory agree on the revision. Otherwise the record keeps
333// the installation it had.
334func (g Guard) SettleObsoleteRootInventory(ctx context.Context, host string, intent []byte, applied func(context.Context, RootInstallation) (bool, error)) error {
335 return g.resolveRootInventory(ctx, host, intent, nil, applied)
336}
337
338// resolveRootInventory is the one terminal resolution of a pending root
339// inventory intent, for an obsolete binding (superseded nil: the binding
340// difference alone supersedes it, no claim is read) or a proven superseded
341// claim (superseded over the fresh claim, ErrFenced when it does not hold).
342// The intent must match exactly. applied reconciles a commit that landed
343// before its record write was lost; then the intent is cleared and the
344// record marked quarantined and settled. A settled record is terminal: no
345// pending intent remains to resolve it again, so it must stay inspectable
346// by the census, and the quarantine keeps every execution path refusing.
347func (g Guard) resolveRootInventory(ctx context.Context, host string, intent []byte, superseded func(*hostclaim.Claim) bool, applied func(context.Context, RootInstallation) (bool, error)) error {
348 if g.Root == nil || applied == nil || len(intent) == 0 {
349 return ErrInvalidInput
350 }
351 return g.readLocked(ctx, host, func(d *recordDir) error {
352 r, err := d.read(host)
353 if err != nil {
354 return err
355 }
356 if !bytes.Equal(r.pendingInventory, intent) || r.pendingInstallation == nil {
357 return ErrInvalidInput
358 }
359 if superseded != nil {
360 c, err := g.fresh(ctx, host)
361 if err != nil {
362 return err
363 }
364 if !superseded(c) {
365 return ErrFenced
366 }
367 }
368 pending := *r.pendingInstallation
369 landed, err := applied(ctx, pending)
370 if err != nil {
371 return err
372 }
373 if landed {
374 if r.Incarnation != pending.Incarnation || r.ActiveStamp != pending.Stamp {
375 r.retire()
376 r.RetiredStamps = nil
377 }
378 r.Incarnation, r.ActiveStamp, r.installation = pending.Incarnation, pending.Stamp, pending
379 }
380 r.pendingInventory, r.pendingInstallation = nil, nil
381 r.quarantined, r.settled = true, true
382 return d.write(r)
383 })
384}
385