carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package release
2
3import (
4 "crypto/sha256"
5 "encoding/hex"
6 "encoding/json"
7 "errors"
8 "os"
9 "slices"
10 "strings"
11
12 "github.com/carlosframework/platform/internal/controlname"
13 "github.com/carlosframework/platform/internal/registry"
14 "github.com/carlosframework/platform/internal/rootstate"
15)
16
17const RootRestartAbsent = "\x00absent"
18
19const RootRestartsDir = "/var/lib/carlos-release/private/restarts"
20
21// Inputs to this journal come from root's canonical providers. Callers hold
22// release exclusion across observation, dispatch and completion. No service
23// marks, registry rows or service RPC payloads establish these inputs.
24type RootRestarts struct{ Store rootstate.Store }
25
26// Bindings retains apps whose captured work outlives their local installation.
27func (h *RootRestarts) Bindings() ([]RestartBinding, error) {
28 if h == nil || h.Store == nil {
29 return nil, os.ErrPermission
30 }
31 names, err := h.Store.Names()
32 if err != nil {
33 return nil, err
34 }
35 var out []RestartBinding
36 for _, name := range names {
37 if strings.HasSuffix(name, ".lock") {
38 continue
39 }
40 raw, err := h.Store.Read(name)
41 if err != nil {
42 return nil, err
43 }
44 var r rootRestartRecord
45 if strictCanonicalJSON(raw, &r) != nil || !validRootRestart(r, r.Binding) || rootRestartKey(r.Binding) != name {
46 return nil, os.ErrPermission
47 }
48 if _, err := h.read(r.Binding); err != nil {
49 return nil, err
50 }
51 out = append(out, r.Binding)
52 }
53 return out, nil
54}
55
56type RestartBinding struct {
57 Box string `json:"box"`
58 Account string `json:"account"`
59 App string `json:"app"`
60}
61
62type RestartTarget struct {
63 Host string `json:"host"`
64 Source string `json:"source"`
65 Installation string `json:"installation"`
66 Unit string `json:"unit"`
67 Environment string `json:"environment"`
68 Hibernate bool `json:"hibernate"`
69}
70
71// Complete means root has enumerated the canonical local target set, including
72// held installations. A held route remains a target; the dispatch gate waits.
73type RestartSnapshot struct {
74 Complete bool
75 AppID string
76 AppBody []byte
77 Scopes ScopedRestartGeneration
78 Targets []RestartTarget
79}
80
81type RestartIntent struct {
82 ID string `json:"id"`
83 AppID string `json:"app_id"`
84 AppBody []byte `json:"app_body,omitempty"`
85 ScopeID string `json:"scope_id"`
86 Requests []RestartRequest `json:"requests"`
87 Targets []RestartProgress `json:"targets"`
88}
89
90type RestartProgress struct {
91 Target RestartTarget `json:"target"`
92 Before string `json:"before,omitempty"`
93 After string `json:"after,omitempty"`
94 Failed bool `json:"failed,omitempty"`
95}
96
97type rootRestartRecord struct {
98 Schema int `json:"schema"`
99 Binding RestartBinding `json:"binding"`
100 Revision uint64 `json:"revision"`
101 Complete uint64 `json:"complete"`
102 AppID string `json:"app_id"`
103 ScopeID string `json:"scope_id"`
104 Pending *RestartIntent `json:"pending,omitempty"`
105}
106
107func restartValue(s string) bool {
108 return s != "" && len(s) <= 4096 && !strings.ContainsAny(s, "\x00\n\r")
109}
110func (b RestartBinding) valid() bool {
111 return restartValue(b.Box) && ValidAccount(b.Account) && ValidApp(b.App)
112}
113func (t RestartTarget) valid() bool {
114 return controlname.ValidHostShape(t.Host) && restartValue(t.Source) && restartValue(t.Installation) && registry.ValidUnitName(t.Unit) && len(t.Environment) <= 4096
115}
116func rootRestartKey(b RestartBinding) string {
117 raw, _ := json.Marshal(b)
118 sum := sha256.Sum256(raw)
119 return hex.EncodeToString(sum[:]) + ".json"
120}
121func restartIntentID(p RestartIntent) string {
122 p.ID = ""
123 p.Targets = slices.Clone(p.Targets)
124 for i := range p.Targets {
125 p.Targets[i].Before = ""
126 p.Targets[i].After = ""
127 p.Targets[i].Failed = false
128 }
129 raw, _ := json.Marshal(p)
130 sum := sha256.Sum256(raw)
131 return hex.EncodeToString(sum[:])
132}
133func validRootRestart(r rootRestartRecord, b RestartBinding) bool {
134 if !b.valid() || r.Schema != 1 || r.Binding != b || r.Revision == 0 || r.Complete != r.Revision || !(restartValue(r.AppID) || r.AppID == RootRestartAbsent) || !(restartValue(r.ScopeID) || r.ScopeID == RestartScopeNone) {
135 return false
136 }
137 p := r.Pending
138 if p == nil {
139 return true
140 }
141 if !restartValue(p.ID) || !(restartValue(p.AppID) || p.AppID == RootRestartAbsent) || !(restartValue(p.ScopeID) || p.ScopeID == RestartScopeNone) || len(p.Requests) > historyLimit || len(p.Targets) > historyLimit || (p.AppID == r.AppID && p.ScopeID == r.ScopeID) {
142 return false
143 }
144 seen := map[string]bool{}
145 for _, v := range p.Targets {
146 if (v.Failed && (v.Before == "" || v.After != "")) || !v.Target.valid() || seen[v.Target.Host] || (v.Before != "" && !restartValue(v.Before)) || (v.After != "" && (!restartValue(v.After) || v.Before == "" || v.Before == v.After)) {
147 return false
148 }
149 seen[v.Target.Host] = true
150 }
151 seen = map[string]bool{}
152 for _, q := range p.Requests {
153 if !restartValue(q.ID) || seen[q.ID] {
154 return false
155 }
156 seen[q.ID] = true
157 }
158 copy := *p
159 copy.Targets = slices.Clone(p.Targets)
160 return p.ID == restartIntentID(copy)
161}
162func (h *RootRestarts) read(b RestartBinding) (rootRestartRecord, error) {
163 var r rootRestartRecord
164 if h == nil || h.Store == nil || !b.valid() {
165 return r, os.ErrPermission
166 }
167 raw, err := h.Store.Read(rootRestartKey(b))
168 if err != nil {
169 return r, err
170 }
171 if len(raw) > rootstate.MaxRecord || strictCanonicalJSON(raw, &r) != nil || !validRootRestart(r, b) {
172 return r, os.ErrPermission
173 }
174 // A rename visible to this process may still need its directory synced.
175 if err := h.Store.Write(rootRestartKey(b), raw); err != nil {
176 return rootRestartRecord{}, err
177 }
178 return r, nil
179}
180func (h *RootRestarts) write(r rootRestartRecord) error {
181 if !validRootRestart(r, r.Binding) {
182 return os.ErrPermission
183 }
184 raw, err := json.Marshal(r)
185 if err != nil {
186 return err
187 }
188 if len(raw) > rootstate.MaxRecord {
189 return os.ErrPermission
190 }
191 return h.Store.Write(rootRestartKey(r.Binding), raw)
192}
193func (r *rootRestartRecord) advance() error {
194 if r.Revision == ^uint64(0) {
195 return os.ErrPermission
196 }
197 r.Revision++
198 r.Complete = r.Revision
199 return nil
200}
201
202// Observe baselines only an absent root record. Corruption is unavailable.
203// Pending work is returned before considering rollover or a changed inventory.
204// The caller must verify target binding and admission again before Begin.
205func (h *RootRestarts) Observe(b RestartBinding, in RestartSnapshot) (*RestartIntent, error) {
206 r, err := h.read(b)
207 if err != nil && !errors.Is(err, os.ErrNotExist) {
208 return nil, err
209 }
210 if err == nil && r.Pending != nil {
211 return r.Pending, nil
212 }
213 if !in.Complete || !(restartValue(in.AppID) || in.AppID == RootRestartAbsent) || in.Scopes.Schema != 1 || len(in.Scopes.Requests) > historyLimit || len(in.Targets) > historyLimit {
214 return nil, os.ErrPermission
215 }
216 seen := map[string]bool{}
217 for _, target := range in.Targets {
218 if !target.valid() || seen[target.Host] {
219 return nil, os.ErrPermission
220 }
221 seen[target.Host] = true
222 }
223 seen = map[string]bool{}
224 for _, q := range in.Scopes.Requests {
225 if !restartValue(q.ID) || q.ID == RestartScopeNone || seen[q.ID] {
226 return nil, os.ErrPermission
227 }
228 seen[q.ID] = true
229 }
230 if errors.Is(err, os.ErrNotExist) {
231 r = rootRestartRecord{Schema: 1, Binding: b, Revision: 1, Complete: 1, AppID: in.AppID, ScopeID: in.Scopes.Newest()}
232 return nil, h.write(r)
233 }
234 if r.AppID == in.AppID && r.ScopeID == in.Scopes.Newest() {
235 return nil, nil
236 }
237 requests, found := in.Scopes.PendingSince(r.ScopeID)
238 if !found {
239 requests = nil
240 }
241 p := RestartIntent{AppID: in.AppID, ScopeID: in.Scopes.Newest(), Requests: slices.Clone(requests)}
242 if r.AppID != in.AppID {
243 p.AppBody = slices.Clone(in.AppBody)
244 }
245 targets := slices.Clone(in.Targets)
246 slices.SortFunc(targets, func(a, b RestartTarget) int { return strings.Compare(a.Host, b.Host) })
247 for _, target := range targets {
248 selected := r.AppID != in.AppID
249 for _, q := range requests {
250 yes, known := q.Scope.Selects(registry.Route{Host: target.Host, Environment: target.Environment})
251 selected = selected || (known && yes)
252 }
253 if selected && !target.Hibernate {
254 p.Targets = append(p.Targets, RestartProgress{Target: target})
255 }
256 }
257 if err := r.advance(); err != nil {
258 return nil, err
259 }
260 if len(p.Targets) == 0 {
261 r.AppID, r.ScopeID = p.AppID, p.ScopeID
262 return nil, h.write(r)
263 }
264 p.ID = restartIntentID(p)
265 r.Pending = &p
266 if err := h.write(r); err != nil {
267 return nil, err
268 }
269 return &p, nil
270}
271
272// Begin grants one dispatch only. Even an identical replay refuses, since a
273// crash after intent persistence cannot prove whether systemd saw the call.
274func (h *RootRestarts) Begin(b RestartBinding, id string, target RestartTarget, before string) error {
275 r, err := h.read(b)
276 if err != nil {
277 return err
278 }
279 if r.Pending == nil || r.Pending.ID != id || !restartValue(before) {
280 return os.ErrPermission
281 }
282 for i := range r.Pending.Targets {
283 p := &r.Pending.Targets[i]
284 if p.Target != target {
285 continue
286 }
287 if p.Before != "" {
288 return os.ErrPermission
289 }
290 p.Before = before
291 if err := r.advance(); err != nil {
292 return err
293 }
294 return h.write(r)
295 }
296 return os.ErrPermission
297}
298
299// Complete accepts only a freshly root-verified successor invocation on the
300// exact captured binding. An ambiguous or failed observation leaves pending
301// work intact. Repeating the identical completion only retries durability.
302func (h *RootRestarts) Complete(b RestartBinding, id string, target RestartTarget, after string) error {
303 r, err := h.read(b)
304 if err != nil {
305 return err
306 }
307 if r.Pending == nil || r.Pending.ID != id || !restartValue(after) {
308 return os.ErrPermission
309 }
310 for i := range r.Pending.Targets {
311 p := &r.Pending.Targets[i]
312 if p.Target != target {
313 continue
314 }
315 if p.Failed || p.Before == "" || p.Before == after || (p.After != "" && p.After != after) {
316 return os.ErrPermission
317 }
318 p.After = after
319 if err := r.advance(); err != nil {
320 return err
321 }
322 return h.write(r)
323 }
324 return os.ErrPermission
325}
326
327// Finish advances consumption only when every journaled target has evidence.
328// It is separate from Complete so a failed completion write can be retried
329// without losing the request identity used to verify that retry.
330func (h *RootRestarts) Finish(b RestartBinding, id string) error {
331 r, err := h.read(b)
332 if err != nil {
333 return err
334 }
335 if r.Pending == nil || r.Pending.ID != id {
336 return os.ErrPermission
337 }
338 for _, p := range r.Pending.Targets {
339 if p.After == "" && !p.Failed {
340 return os.ErrPermission
341 }
342 }
343 r.AppID, r.ScopeID = r.Pending.AppID, r.Pending.ScopeID
344 r.Pending = nil
345 if err := r.advance(); err != nil {
346 return err
347 }
348 return h.write(r)
349}
350
351// Fail records a definite dispatch failure observed by the dispatching root
352// process. Recovery must not infer it from missing invocation evidence.
353func (h *RootRestarts) Fail(b RestartBinding, id string, target RestartTarget) error {
354 r, err := h.read(b)
355 if err != nil {
356 return err
357 }
358 if r.Pending == nil || r.Pending.ID != id {
359 return os.ErrPermission
360 }
361 for i := range r.Pending.Targets {
362 p := &r.Pending.Targets[i]
363 if p.Target != target {
364 continue
365 }
366 if p.Before == "" || p.After != "" {
367 return os.ErrPermission
368 }
369 p.Failed = true
370 if err := r.advance(); err != nil {
371 return err
372 }
373 return h.write(r)
374 }
375 return os.ErrPermission
376}
377