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 "errors"
7
8 "github.com/carlosframework/platform/internal/controlname"
9 "github.com/carlosframework/platform/internal/hostclaim"
10 "github.com/carlosframework/platform/internal/moves"
11 "github.com/carlosframework/platform/internal/provision"
12 "github.com/carlosframework/platform/internal/release"
13)
14
15// The recovery pass's move log writer (Plan 6 Task 3, fix round 3, ruling 1).
16// The console queues the move log entries its move pass makes in its outbox
17// (move_log_outbox) and delivers them through this writer, once per recovery
18// pass (Recovery.MoveLogs), whatever the moves have done since: a grant is
19// spent by the time its transition's entry exists, and the claim's release
20// never waits for the log. The writer exposes no store: one create-only entry
21// per call, under the entry's own move's report prefix.
22
23// MoveLogEntryMax bounds one entry the writer writes, in bytes.
24const MoveLogEntryMax = 4096
25
26// ErrMoveLogTaken is an entry whose name already holds other bytes: the same
27// transition, logged first by another writer. Nothing is written.
28var ErrMoveLogTaken = errors.New("hostops: the move log entry was written by another writer")
29
30// MoveLogEntry is one move log entry to deliver: the move's host and id, the
31// instance incarnation the move's evidence carried, the entry's name (its
32// identity) and its exact bytes. It names no account or app: those are the
33// host's claim's, once its holder is that incarnation.
34type MoveLogEntry struct {
35 Host, Op, Incarnation, Name string
36 Body []byte
37}
38
39// MoveLogWriter is one recovery pass's move log writer, valid only while its
40// hook runs.
41type MoveLogWriter struct {
42 w *ControlWriter
43 state *moveRecoveryGrantState
44}
45
46// runMoveLogDrain mints the pass's writer, inside RunRecovery only, and hands
47// it to hook. The hook receives the writer, never the minting context.
48func runMoveLogDrain(ctx context.Context, hook func(context.Context, *MoveLogWriter) error) error {
49 w := recoveryWriter(ctx)
50 if w == nil || hook == nil {
51 return ErrContract
52 }
53 lw := &MoveLogWriter{w: w, state: &moveRecoveryGrantState{active: true}}
54 state := lw.state
55 defer func() { state.mu.Lock(); state.active = false; state.mu.Unlock() }()
56 ctx = context.WithValue(ctx, lifecycleGrantKey{}, []lifecycleGrant(nil))
57 ctx = context.WithValue(ctx, moveRecoveryPassKey{}, (*moveRecoveryPassScope)(nil))
58 return hook(ctx, lw)
59}
60
61// WriteMoveLog writes one entry, with the checks platform-1e set for the
62// grant's Log, which this writer replaced (fix round 3, ruling 3):
63//
64// - The host's claim holder is e's incarnation (fix round 4, ruling 3): a
65// recreated instance (the same names, another incarnation), a retired
66// one (no claim) or another app's host never takes the entry. The
67// account and app are the holder's.
68// - The move is one the holder's record names: live at that incarnation,
69// or closed as its last move. The caller's move id is never taken alone.
70// - The key is derived, never given: e.Name under the report prefix of the
71// holder's account and app, e's host and move. The move id and the entry
72// name are one segment of letters, digits, '_' and '-'.
73// - Body is at most MoveLogEntryMax bytes and decodes, strictly, as a
74// moves.LogEvent of that move and host, named e.Name.
75// - The write is create-only. A replay of the same bytes is a no-op
76// (created false); other bytes under the name are ErrMoveLogTaken.
77//
78// ErrIdentity is an entry that can never be written. It answers ErrPending
79// once its hook has returned.
80func (lw *MoveLogWriter) WriteMoveLog(ctx context.Context, e MoveLogEntry) (created bool, err error) {
81 if lw == nil || lw.state == nil || lw.w == nil {
82 return false, ErrPending
83 }
84 lw.state.mu.Lock()
85 defer lw.state.mu.Unlock()
86 if !lw.state.active {
87 return false, ErrPending
88 }
89 if e.Incarnation == "" || !moveLogName(e.Op) || !moveLogName(e.Name) || len(e.Body) > MoveLogEntryMax {
90 return false, ErrIdentity
91 }
92 if _, err := hostclaim.Key(e.Host); err != nil {
93 return false, ErrIdentity
94 }
95 var ev moves.LogEvent
96 if err := strictJSON(e.Body, &ev); err != nil {
97 return false, ErrIdentity
98 }
99 if ev.V != 1 || ev.Op != e.Op || ev.Host != e.Host || ev.Identity != e.Name {
100 return false, ErrIdentity
101 }
102 o, err := hostclaim.Read(ctx, lw.w.raw, e.Host)
103 if err != nil {
104 return false, err
105 }
106 if o.Absent || o.Claim == nil || o.Claim.Holder == nil || o.Claim.Holder.Incarnation != e.Incarnation {
107 return false, ErrIdentity
108 }
109 h := o.Claim.Holder
110 if !controlname.ValidAccount(h.Account) || !controlname.ValidApp(h.App) {
111 return false, ErrIdentity
112 }
113 r, err := ReadRecord(ctx, lw.w.raw, h.Key)
114 if err != nil {
115 return false, err
116 }
117 if !matches(r, h) {
118 return false, ErrIdentity
119 }
120 rec, err := provision.DecodeRecord(r.Body)
121 if err != nil {
122 return false, ErrIdentity
123 }
124 live := rec.Move != nil && rec.Move.ID == e.Op && rec.Move.Incarnation == e.Incarnation
125 closed := rec.LastMove != nil && rec.LastMove.ID == e.Op
126 if !live && !closed {
127 return false, ErrIdentity
128 }
129 key := moves.LogKeyFor(moves.ReportPrefix(h.Account, h.App, e.Host, e.Op), e.Name)
130 if _, err := lw.w.raw.Put(ctx, key, e.Body, ""); err == nil {
131 return true, nil
132 } else if !errors.Is(err, release.ErrCASConflict) {
133 return false, err
134 }
135 have, _, err := lw.w.raw.Get(ctx, key)
136 if err != nil {
137 return false, err
138 }
139 if !bytes.Equal(have, e.Body) {
140 return false, ErrMoveLogTaken
141 }
142 return false, nil
143}
144
145// moveLogName is one path segment of letters, digits, '_' and '-', at most
146// 128 bytes: every identity the move log names (moves.TransitionOf) fits.
147func moveLogName(name string) bool {
148 if name == "" || len(name) > 128 {
149 return false
150 }
151 for _, r := range name {
152 if !(r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' || r == '_' || r == '-') {
153 return false
154 }
155 }
156 return true
157}
158