carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "context"
5 "crypto/rand"
6 "crypto/sha256"
7 "encoding/hex"
8 "encoding/json"
9 "errors"
10 "os"
11 "strings"
12 "time"
13
14 "github.com/carlosframework/platform/internal/hostwrite"
15 "github.com/carlosframework/platform/internal/release"
16 "github.com/carlosframework/platform/internal/rootstate"
17)
18
19const rollbackMargin = 30 * time.Second
20const rootRestartState = "/var/lib/carlos-release/private/restarts"
21
22type rollbackAttempt struct {
23 ID string
24 Host string
25 Created time.Time
26 Deadline time.Time
27 Failed bool
28 Recovering bool
29 Settled bool
30 Verified bool `json:",omitempty"`
31 Independent bool `json:",omitempty"`
32 Target string
33 Previous string `json:",omitempty"`
34 PreviousDigest string `json:",omitempty"`
35 Identity hostwrite.Identity
36 History *release.HistoryAttempt `json:",omitempty"`
37 HistoryBinding release.HistoryBinding `json:",omitempty"`
38 TargetDigest string
39}
40
41type rollbackCapability struct {
42 Attempt rollbackAttempt
43 Binding proofBinding
44}
45
46func rollbackHostKey(host string) string {
47 h := sha256.Sum256([]byte(host))
48 return hex.EncodeToString(h[:])
49}
50func rollbackJournalKey(host string) string { return rollbackHostKey(host) + ".attempt" }
51func rollbackCapabilityKey(host, id string) string { return rollbackHostKey(host) + "." + id + ".cap" }
52
53func writeRollbackState(s rootstate.Store, key string, v any) error {
54 b, err := json.Marshal(v)
55 if err != nil {
56 return err
57 }
58 return s.Write(key, b)
59}
60
61func readRollbackAttempt(s rootstate.Store, host string) (rollbackAttempt, error) {
62 var a rollbackAttempt
63 b, err := s.Read(rollbackJournalKey(host))
64 if err != nil {
65 return a, err
66 }
67 if json.Unmarshal(b, &a) != nil || a.Host != host || len(a.ID) != 32 {
68 return a, os.ErrPermission
69 }
70 if _, err := hex.DecodeString(a.ID); err != nil {
71 return a, os.ErrPermission
72 }
73 return a, nil
74}
75
76func settleRollbackAttempt(s rootstate.Store, a rollbackAttempt) error {
77 if !a.Settled {
78 a.Settled = true
79 if a.Independent {
80 if err := writeRollbackState(s, recoveryResultKey(a), a); err != nil {
81 return err
82 }
83 }
84 if err := writeRollbackState(s, rollbackJournalKey(a.Host), a); err != nil {
85 return err
86 }
87 }
88 return removeRollbackCapabilities(s, a.Host)
89}
90
91func captureRollback(s rootstate.Store, b proofBinding, now time.Time, timeout time.Duration, target string, identity hostwrite.Identity, link ...historyRollbackLink) (rollbackAttempt, error) {
92 if b.Source == "" || b.Version == "" || b.Digest == "" {
93 return rollbackAttempt{}, os.ErrPermission
94 }
95 if prior, err := readRollbackAttempt(s, b.Route.Host); err == nil {
96 if !prior.Settled && !now.After(prior.Deadline.Add(rollbackMargin)) {
97 return rollbackAttempt{}, os.ErrPermission
98 }
99 if err := settleRollbackAttempt(s, prior); err != nil {
100 return rollbackAttempt{}, err
101 }
102 } else if !errors.Is(err, os.ErrNotExist) {
103 return rollbackAttempt{}, err
104 }
105 id := make([]byte, 16)
106 if _, err := rand.Read(id); err != nil {
107 return rollbackAttempt{}, err
108 }
109 a := rollbackAttempt{ID: hex.EncodeToString(id), Host: b.Route.Host, Created: now, Deadline: now.Add(2 * timeout), Target: target, Identity: identity, Previous: b.Version, PreviousDigest: b.Digest}
110 if len(link) > 1 {
111 return rollbackAttempt{}, os.ErrPermission
112 }
113 if len(link) == 1 {
114 l := link[0]
115 a.TargetDigest = l.TargetDigest
116 if l.Attempt.ID != "" {
117 if l.Binding.Host != b.Route.Host || l.Binding.Account != b.Route.Account || l.Binding.App != b.Route.App || l.Attempt.Source != b.Source {
118 return rollbackAttempt{}, os.ErrPermission
119 }
120 if l.Independent {
121 l.Attempt.ID = a.ID
122 }
123 a.History, a.HistoryBinding = &l.Attempt, l.Binding
124 a.Independent = l.Independent
125 a.TargetDigest = l.Attempt.SHA256
126 }
127 }
128 if err := writeRollbackState(s, rollbackCapabilityKey(a.Host, a.ID), rollbackCapability{Attempt: a, Binding: b}); err != nil {
129 return rollbackAttempt{}, err
130 }
131 if err := writeRollbackState(s, rollbackJournalKey(a.Host), a); err != nil {
132 return rollbackAttempt{}, errors.Join(err, s.Remove(rollbackCapabilityKey(a.Host, a.ID)))
133 }
134 return a, nil
135}
136
137func loadRollback(s rootstate.Store, host string, now time.Time) (rollbackAttempt, proofBinding, error) {
138 a, err := readRollbackAttempt(s, host)
139 if err != nil {
140 return rollbackAttempt{}, proofBinding{}, errors.Join(err, removeRollbackCapabilities(s, host))
141 }
142 if a.Settled || now.Before(a.Created) || now.After(a.Deadline.Add(rollbackMargin)) {
143 return a, proofBinding{}, errors.Join(os.ErrPermission, settleRollbackAttempt(s, a))
144 }
145 b, err := s.Read(rollbackCapabilityKey(host, a.ID))
146 if err != nil {
147 return a, proofBinding{}, err
148 }
149 var cap rollbackCapability
150 if json.Unmarshal(b, &cap) != nil || cap.Attempt.ID != a.ID || cap.Attempt.TargetDigest != a.TargetDigest || cap.Attempt.Independent != a.Independent || cap.Attempt.HistoryBinding != a.HistoryBinding || !sameHistoryAttempt(cap.Attempt.History, a.History) || cap.Attempt.Previous != a.Previous || cap.Attempt.PreviousDigest != a.PreviousDigest || cap.Attempt.Host != host || cap.Attempt.Target != a.Target || cap.Attempt.Identity != a.Identity || !cap.Attempt.Deadline.Equal(a.Deadline) || !cap.Attempt.Created.Equal(a.Created) || cap.Binding.Route.Host != host {
151 return a, proofBinding{}, os.ErrPermission
152 }
153 return a, cap.Binding, nil
154}
155
156func (c rootRestartChecks) finish(host string) error {
157 if c.attempts == nil || c.started == nil {
158 return os.ErrPermission
159 }
160 expected, ok := c.started.Load(host)
161 if !ok {
162 return nil
163 }
164 defer c.started.Delete(host)
165 s, err := c.attempts()
166 if err != nil {
167 return err
168 }
169 defer s.Close()
170 unlock, err := s.Lock(context.Background(), rollbackHostKey(host)+".lock")
171 if err != nil {
172 return err
173 }
174 defer unlock()
175 a, err := readRollbackAttempt(s, host)
176 if errors.Is(err, os.ErrNotExist) {
177 return nil
178 }
179 if err != nil {
180 return err
181 }
182 if a.ID != expected {
183 return os.ErrPermission
184 }
185 return settleRollbackAttempt(s, a)
186}
187
188func removeRollbackCapabilities(s rootstate.Store, host string) error {
189 names, err := s.Names()
190 if err != nil {
191 return err
192 }
193 for _, name := range names {
194 if !strings.HasPrefix(name, rollbackHostKey(host)+".") || !strings.HasSuffix(name, ".cap") {
195 continue
196 }
197 if err := s.Remove(name); err != nil {
198 return err
199 }
200 }
201 return nil
202}
203
204func sameHistoryAttempt(a, b *release.HistoryAttempt) bool {
205 if a == nil || b == nil {
206 return a == nil && b == nil
207 }
208 return *a == *b
209}
210