package release import ( "crypto/sha256" "encoding/hex" "encoding/json" "errors" "os" "slices" "strings" "github.com/carlosframework/platform/internal/controlname" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/rootstate" ) const RootRestartAbsent = "\x00absent" const RootRestartsDir = "/var/lib/carlos-release/private/restarts" // Inputs to this journal come from root's canonical providers. Callers hold // release exclusion across observation, dispatch and completion. No service // marks, registry rows or service RPC payloads establish these inputs. type RootRestarts struct{ Store rootstate.Store } // Bindings retains apps whose captured work outlives their local installation. func (h *RootRestarts) Bindings() ([]RestartBinding, error) { if h == nil || h.Store == nil { return nil, os.ErrPermission } names, err := h.Store.Names() if err != nil { return nil, err } var out []RestartBinding for _, name := range names { if strings.HasSuffix(name, ".lock") { continue } raw, err := h.Store.Read(name) if err != nil { return nil, err } var r rootRestartRecord if strictCanonicalJSON(raw, &r) != nil || !validRootRestart(r, r.Binding) || rootRestartKey(r.Binding) != name { return nil, os.ErrPermission } if _, err := h.read(r.Binding); err != nil { return nil, err } out = append(out, r.Binding) } return out, nil } type RestartBinding struct { Box string `json:"box"` Account string `json:"account"` App string `json:"app"` } type RestartTarget struct { Host string `json:"host"` Source string `json:"source"` Installation string `json:"installation"` Unit string `json:"unit"` Environment string `json:"environment"` Hibernate bool `json:"hibernate"` } // Complete means root has enumerated the canonical local target set, including // held installations. A held route remains a target; the dispatch gate waits. type RestartSnapshot struct { Complete bool AppID string AppBody []byte Scopes ScopedRestartGeneration Targets []RestartTarget } type RestartIntent struct { ID string `json:"id"` AppID string `json:"app_id"` AppBody []byte `json:"app_body,omitempty"` ScopeID string `json:"scope_id"` Requests []RestartRequest `json:"requests"` Targets []RestartProgress `json:"targets"` } type RestartProgress struct { Target RestartTarget `json:"target"` Before string `json:"before,omitempty"` After string `json:"after,omitempty"` Failed bool `json:"failed,omitempty"` } type rootRestartRecord struct { Schema int `json:"schema"` Binding RestartBinding `json:"binding"` Revision uint64 `json:"revision"` Complete uint64 `json:"complete"` AppID string `json:"app_id"` ScopeID string `json:"scope_id"` Pending *RestartIntent `json:"pending,omitempty"` } func restartValue(s string) bool { return s != "" && len(s) <= 4096 && !strings.ContainsAny(s, "\x00\n\r") } func (b RestartBinding) valid() bool { return restartValue(b.Box) && ValidAccount(b.Account) && ValidApp(b.App) } func (t RestartTarget) valid() bool { return controlname.ValidHostShape(t.Host) && restartValue(t.Source) && restartValue(t.Installation) && registry.ValidUnitName(t.Unit) && len(t.Environment) <= 4096 } func rootRestartKey(b RestartBinding) string { raw, _ := json.Marshal(b) sum := sha256.Sum256(raw) return hex.EncodeToString(sum[:]) + ".json" } func restartIntentID(p RestartIntent) string { p.ID = "" p.Targets = slices.Clone(p.Targets) for i := range p.Targets { p.Targets[i].Before = "" p.Targets[i].After = "" p.Targets[i].Failed = false } raw, _ := json.Marshal(p) sum := sha256.Sum256(raw) return hex.EncodeToString(sum[:]) } func validRootRestart(r rootRestartRecord, b RestartBinding) bool { 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) { return false } p := r.Pending if p == nil { return true } 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) { return false } seen := map[string]bool{} for _, v := range p.Targets { 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)) { return false } seen[v.Target.Host] = true } seen = map[string]bool{} for _, q := range p.Requests { if !restartValue(q.ID) || seen[q.ID] { return false } seen[q.ID] = true } copy := *p copy.Targets = slices.Clone(p.Targets) return p.ID == restartIntentID(copy) } func (h *RootRestarts) read(b RestartBinding) (rootRestartRecord, error) { var r rootRestartRecord if h == nil || h.Store == nil || !b.valid() { return r, os.ErrPermission } raw, err := h.Store.Read(rootRestartKey(b)) if err != nil { return r, err } if len(raw) > rootstate.MaxRecord || strictCanonicalJSON(raw, &r) != nil || !validRootRestart(r, b) { return r, os.ErrPermission } // A rename visible to this process may still need its directory synced. if err := h.Store.Write(rootRestartKey(b), raw); err != nil { return rootRestartRecord{}, err } return r, nil } func (h *RootRestarts) write(r rootRestartRecord) error { if !validRootRestart(r, r.Binding) { return os.ErrPermission } raw, err := json.Marshal(r) if err != nil { return err } if len(raw) > rootstate.MaxRecord { return os.ErrPermission } return h.Store.Write(rootRestartKey(r.Binding), raw) } func (r *rootRestartRecord) advance() error { if r.Revision == ^uint64(0) { return os.ErrPermission } r.Revision++ r.Complete = r.Revision return nil } // Observe baselines only an absent root record. Corruption is unavailable. // Pending work is returned before considering rollover or a changed inventory. // The caller must verify target binding and admission again before Begin. func (h *RootRestarts) Observe(b RestartBinding, in RestartSnapshot) (*RestartIntent, error) { r, err := h.read(b) if err != nil && !errors.Is(err, os.ErrNotExist) { return nil, err } if err == nil && r.Pending != nil { return r.Pending, nil } if !in.Complete || !(restartValue(in.AppID) || in.AppID == RootRestartAbsent) || in.Scopes.Schema != 1 || len(in.Scopes.Requests) > historyLimit || len(in.Targets) > historyLimit { return nil, os.ErrPermission } seen := map[string]bool{} for _, target := range in.Targets { if !target.valid() || seen[target.Host] { return nil, os.ErrPermission } seen[target.Host] = true } seen = map[string]bool{} for _, q := range in.Scopes.Requests { if !restartValue(q.ID) || q.ID == RestartScopeNone || seen[q.ID] { return nil, os.ErrPermission } seen[q.ID] = true } if errors.Is(err, os.ErrNotExist) { r = rootRestartRecord{Schema: 1, Binding: b, Revision: 1, Complete: 1, AppID: in.AppID, ScopeID: in.Scopes.Newest()} return nil, h.write(r) } if r.AppID == in.AppID && r.ScopeID == in.Scopes.Newest() { return nil, nil } requests, found := in.Scopes.PendingSince(r.ScopeID) if !found { requests = nil } p := RestartIntent{AppID: in.AppID, ScopeID: in.Scopes.Newest(), Requests: slices.Clone(requests)} if r.AppID != in.AppID { p.AppBody = slices.Clone(in.AppBody) } targets := slices.Clone(in.Targets) slices.SortFunc(targets, func(a, b RestartTarget) int { return strings.Compare(a.Host, b.Host) }) for _, target := range targets { selected := r.AppID != in.AppID for _, q := range requests { yes, known := q.Scope.Selects(registry.Route{Host: target.Host, Environment: target.Environment}) selected = selected || (known && yes) } if selected && !target.Hibernate { p.Targets = append(p.Targets, RestartProgress{Target: target}) } } if err := r.advance(); err != nil { return nil, err } if len(p.Targets) == 0 { r.AppID, r.ScopeID = p.AppID, p.ScopeID return nil, h.write(r) } p.ID = restartIntentID(p) r.Pending = &p if err := h.write(r); err != nil { return nil, err } return &p, nil } // Begin grants one dispatch only. Even an identical replay refuses, since a // crash after intent persistence cannot prove whether systemd saw the call. func (h *RootRestarts) Begin(b RestartBinding, id string, target RestartTarget, before string) error { r, err := h.read(b) if err != nil { return err } if r.Pending == nil || r.Pending.ID != id || !restartValue(before) { return os.ErrPermission } for i := range r.Pending.Targets { p := &r.Pending.Targets[i] if p.Target != target { continue } if p.Before != "" { return os.ErrPermission } p.Before = before if err := r.advance(); err != nil { return err } return h.write(r) } return os.ErrPermission } // Complete accepts only a freshly root-verified successor invocation on the // exact captured binding. An ambiguous or failed observation leaves pending // work intact. Repeating the identical completion only retries durability. func (h *RootRestarts) Complete(b RestartBinding, id string, target RestartTarget, after string) error { r, err := h.read(b) if err != nil { return err } if r.Pending == nil || r.Pending.ID != id || !restartValue(after) { return os.ErrPermission } for i := range r.Pending.Targets { p := &r.Pending.Targets[i] if p.Target != target { continue } if p.Failed || p.Before == "" || p.Before == after || (p.After != "" && p.After != after) { return os.ErrPermission } p.After = after if err := r.advance(); err != nil { return err } return h.write(r) } return os.ErrPermission } // Finish advances consumption only when every journaled target has evidence. // It is separate from Complete so a failed completion write can be retried // without losing the request identity used to verify that retry. func (h *RootRestarts) Finish(b RestartBinding, id string) error { r, err := h.read(b) if err != nil { return err } if r.Pending == nil || r.Pending.ID != id { return os.ErrPermission } for _, p := range r.Pending.Targets { if p.After == "" && !p.Failed { return os.ErrPermission } } r.AppID, r.ScopeID = r.Pending.AppID, r.Pending.ScopeID r.Pending = nil if err := r.advance(); err != nil { return err } return h.write(r) } // Fail records a definite dispatch failure observed by the dispatching root // process. Recovery must not infer it from missing invocation evidence. func (h *RootRestarts) Fail(b RestartBinding, id string, target RestartTarget) error { r, err := h.read(b) if err != nil { return err } if r.Pending == nil || r.Pending.ID != id { return os.ErrPermission } for i := range r.Pending.Targets { p := &r.Pending.Targets[i] if p.Target != target { continue } if p.Before == "" || p.After != "" { return os.ErrPermission } p.Failed = true if err := r.advance(); err != nil { return err } return h.write(r) } return os.ErrPermission }