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/ed25519"
6 "encoding/json"
7 "errors"
8 "os"
9 "path/filepath"
10 "time"
11
12 "github.com/carlosframework/platform/internal/registry"
13 "github.com/carlosframework/platform/internal/release"
14 "github.com/carlosframework/platform/internal/rollout"
15 "github.com/carlosframework/platform/internal/rootparticipation"
16 "github.com/carlosframework/platform/internal/rootstate"
17 "github.com/carlosframework/platform/internal/status"
18)
19
20type rootLegacyRetry struct {
21 Build, Book string
22 Epoch uint64
23}
24type rootRetryStatusRecord struct {
25 status.HostRollout
26 Mirror *rootLegacyRetry `json:"root_retry,omitempty"`
27}
28
29const rootRetryProjection = "/var/lib/carlos-swap-status"
30
31func (p rootUpdateSource) beginRuntime(saved *rootUpdateRecord, save func() error, version, digest, attempt string) (func(string, error, func() error) error, error) {
32 rt := saved.Route
33 if version == "" || digest == "" || attempt == "" {
34 return nil, os.ErrPermission
35 }
36 start := time.Now().UTC()
37 snap := p.book.Snapshot()
38 // Claim launches currently use the serial, hold-mode path. Workers and
39 // overlapping generations remain refused by the existing launch boundary.
40 if err := p.book.Begin(rt.Host, rollout.Intent{Account: rt.Account, App: rt.App, Build: version, ID: attempt, Mode: "hold", Start: start}); err != nil {
41 return nil, err
42 }
43 return func(invocation string, failure error, verify func() error) error {
44 if failure == nil && invocation == "" {
45 return os.ErrPermission
46 }
47 var readinessErr error
48 if failure == nil && p.ready != nil {
49 readinessErr = p.ready(rt)
50 failure = readinessErr
51 }
52 if failure == nil && verify != nil {
53 readinessErr = verify()
54 failure = readinessErr
55 }
56 build := &release.BuildState{Schema: 1, Host: rt.Host}
57 if saved.Build.State != nil {
58 *build = *saved.Build.State
59 }
60 if failure != nil {
61 if _, err := p.book.Record(rollout.FailureRecord{Account: rt.Account, App: rt.App, Host: rt.Host, Build: version, Mode: "hold", At: start, HeldDown: []string{rt.Host}}); err != nil {
62 return err
63 }
64 if err := p.book.MarkHoldRan(rt.Host, rollout.HoldRan{Account: rt.Account, App: rt.App, Build: version, Exec: attempt, At: start}); err != nil {
65 return err
66 }
67 } else {
68 if _, err := p.book.ClearHoldRanBefore(rt.Host, snap); err != nil {
69 return err
70 }
71 if saved.Served == nil {
72 saved.Served = map[string]rootServedBuild{}
73 }
74 saved.Served[version] = rootServedBuild{Digest: digest, Invocation: invocation, Source: saved.Installation + "/" + attempt, Epoch: p.book.RestrictionEpoch(rt.Account, rt.App, version, rt.Host)}
75 build.LastExecuted = version
76 }
77 if saved.Consumed != nil && saved.Consumed.Resolved != nil && saved.Consumed.Resolved.Target == version {
78 build.Resolved = saved.Consumed.Resolved
79 build.Completed = build.Resolved.ID
80 build.CompletedFailed = failure != nil
81 }
82 saved.Build = release.BuildStateCopy{Known: true, State: build}
83 if err := save(); err != nil {
84 return err
85 }
86 if err := p.book.End(rt.Host, attempt); err != nil {
87 return err
88 }
89 deliveryErr := p.publishRetry(*saved, version)
90 if !p.enforceFailures {
91 deliveryErr = nil
92 }
93 return errors.Join(readinessErr, deliveryErr)
94 }, nil
95}
96
97func (p rootUpdateSource) retryStatus(saved rootUpdateRecord, version string) status.HostRollout {
98 rt := saved.Route
99 out := status.HostRollout{Account: rt.Account, App: rt.App, Installed: version, RetryV: status.HostRetryV}
100 if saved.Build.State != nil {
101 out.Good = saved.Build.State.LastExecuted
102 }
103 held := version
104 if h, ok := p.book.HoldRanOn(rt.Host); ok && h.Account == rt.Account && h.App == rt.App {
105 held = h.Build
106 }
107 served, ok := saved.Served[held]
108 ok = ok && served.Epoch == p.book.RestrictionEpoch(rt.Account, rt.App, held, rt.Host)
109 if held != "" && (p.book.Blocks(rt.Account, rt.App, held, rt.Host, ok) || p.book.HeldDown(rt.Account, rt.App, held, rt.Host)) {
110 out.HeldBy, out.AttemptBuild = held, held
111 out.HeldBook = p.book.ID()
112 out.HeldEpoch = p.book.PublishedEpoch(rt.Account, rt.App, held, rt.Host)
113 out.Attempt = status.AttemptFailed
114 if p.book.HeldDown(rt.Account, rt.App, held, rt.Host) {
115 out.Attempt = status.AttemptDown
116 }
117 }
118 return out
119}
120
121func (p rootUpdateSource) publishRetry(saved rootUpdateRecord, version string) error {
122 if p.publish == nil {
123 return nil
124 }
125 record := rootRetryStatusRecord{HostRollout: p.retryStatus(saved, version)}
126 if ack := saved.RetryMirror; ack != nil && !p.book.SaveOwed() && !p.book.Blocks(saved.Route.Account, saved.Route.App, ack.Build, saved.Route.Host, false) && !p.book.HeldDown(saved.Route.Account, saved.Route.App, ack.Build, saved.Route.Host) {
127 record.Mirror = ack
128 }
129 return p.publish(saved.Route.Host, record)
130}
131
132func publishRootRetry(host string, report rootRetryStatusRecord) error {
133 s, err := rootstate.Open(rootRetryProjection, true, true)
134 if err != nil {
135 return err
136 }
137 defer s.Close()
138 raw, err := json.Marshal(report)
139 if err != nil {
140 return err
141 }
142 return s.Write(host+".json", raw)
143}
144
145func rootRetryStatus(legacy *status.RolloutReport) *status.RolloutReport {
146 if _, err := os.Lstat(rootRetryProjection); errors.Is(err, os.ErrNotExist) {
147 return legacy
148 }
149 s, err := rootstate.Open(rootRetryProjection, false, true)
150 if errors.Is(err, os.ErrNotExist) {
151 return legacy
152 }
153 if err != nil {
154 return &status.RolloutReport{Unavailable: rolloutBookUnreadable}
155 }
156 defer s.Close()
157 names, err := s.Names()
158 if err != nil {
159 return &status.RolloutReport{Unavailable: rolloutBookUnreadable}
160 }
161 if legacy == nil {
162 legacy = &status.RolloutReport{}
163 }
164 out := *legacy
165 out.Hosts = map[string]status.HostRollout{}
166 for h, r := range legacy.Hosts {
167 out.Hosts[h] = r
168 }
169 for _, name := range names {
170 raw, e := s.Read(name)
171 var r rootRetryStatusRecord
172 if e != nil || json.Unmarshal(raw, &r) != nil || len(name) < 6 || name[len(name)-5:] != ".json" {
173 return &status.RolloutReport{Unavailable: rolloutBookUnreadable}
174 }
175 host := name[:len(name)-5]
176 prior := out.Hosts[host]
177 if ack := r.Mirror; ack != nil && ack.Book != "" && ack.Epoch != 0 && prior.HeldBook == ack.Book && prior.HeldEpoch == ack.Epoch && prior.HeldBy == ack.Build {
178 if m := armedRollout(); m != nil {
179 m.RetryHost(host, ack.Build, ack.Book, ack.Epoch)
180 } else if h := heldHolds(); h != nil {
181 h.retryHost(host, ack.Build, ack.Book, ack.Epoch)
182 }
183 }
184 prior.Account, prior.App = r.Account, r.App
185 prior.HeldBy, prior.HeldBook, prior.HeldEpoch, prior.RetryV = r.HeldBy, r.HeldBook, r.HeldEpoch, r.RetryV
186 if r.HeldBy != "" {
187 prior.Attempt, prior.AttemptBuild = r.Attempt, r.AttemptBuild
188 }
189 out.Hosts[host] = prior
190 }
191 return &out
192}
193
194func beginRootGenerationUpdate(ctx context.Context, s *rootGenerationSource, b release.GenerationBinding) (func(string, error, func() error) error, func() error, error) {
195 pub, err := s.pub()
196 if err != nil {
197 return nil, nil, err
198 }
199 in, err := productionRootUpdatesHeld(dataDir(), &b)(ctx, s.store, pub)
200 if err != nil {
201 return nil, nil, err
202 }
203 if in == nil {
204 return nil, nil, os.ErrPermission
205 }
206 fail := func(err error) (func(string, error, func() error) error, func() error, error) {
207 in.Close()
208 return nil, nil, err
209 }
210 row, err := in.Resolve(ctx, b.Binding.Host)
211 if err != nil {
212 return fail(err)
213 }
214 if row.Begin == nil || row.Failed(b.Version) {
215 return fail(os.ErrPermission)
216 }
217 finish, err := row.Begin(b.Version, b.Digest, b.Attempt)
218 if err != nil {
219 return fail(err)
220 }
221 return finish, in.Close, nil
222}
223
224func rootUpdateReady(ctx context.Context, rt registry.Route) error {
225 probe := proofRouteReady(rt, time.Second)
226 if probe == nil {
227 return os.ErrPermission
228 }
229 deadline := time.NewTimer(5 * time.Second)
230 defer deadline.Stop()
231 for {
232 if err := probe(); err == nil {
233 return nil
234 }
235 select {
236 case <-ctx.Done():
237 return ctx.Err()
238 case <-deadline.C:
239 return os.ErrPermission
240 case <-time.After(25 * time.Millisecond):
241 }
242 }
243}
244
245func relayRootRetry(pub ed25519.PublicKey, rq retryRequest) bool {
246 state, err := rootstate.Open(rootRetryProjection, false, true)
247 if err != nil {
248 return false
249 }
250 defer state.Close()
251 raw, err := state.Read(rq.host + ".json")
252 var report rootRetryStatusRecord
253 if err != nil || json.Unmarshal(raw, &report) != nil {
254 return false
255 }
256 grant, why := verifyRetry(pub, rq, time.Now())
257 if why != "" || report.Account != rq.acct || report.App != rq.app || report.HeldBy != grant.Build || report.HeldBook != grant.Book || report.HeldEpoch == 0 || report.HeldEpoch != grant.Epoch {
258 return false
259 }
260 return os.WriteFile(filepath.Join(dataDir(), stageRequestMarker), []byte(time.Now().UTC().Format(time.RFC3339)+"\n"), 0644) == nil
261}
262
263func prepareRootRetryUnitDirectory() (bool, error) {
264 if _, err := os.Lstat(filepath.Join(rootparticipation.Namespace, "census.json")); errors.Is(err, os.ErrNotExist) {
265 return false, nil
266 } else if err != nil {
267 return false, err
268 }
269 ledger, required, err := rootparticipation.OpenForUpdate()
270 if ledger.State != nil {
271 defer ledger.State.Close()
272 }
273 if err != nil {
274 return false, err
275 }
276 if !required {
277 return false, nil
278 }
279 census, err := ledger.Census()
280 if err != nil {
281 return false, err
282 }
283 if !census.Complete || census.NeverEnrolled {
284 return false, os.ErrPermission
285 }
286 // install-control runs from the window's root shell: make the agent's
287 // read directory 0755 whatever that shell's umask (#317's class).
288 if err := mkdirAllExact(rootRetryProjection, 0755); err != nil {
289 return false, err
290 }
291 state, err := rootstate.Open(rootRetryProjection, true, true)
292 if err != nil {
293 return false, err
294 }
295 return true, state.Close()
296}
297