carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package claimactivation
2
3import (
4 "context"
5 "errors"
6 "os"
7 "reflect"
8 "sort"
9 "time"
10
11 "github.com/carlosframework/platform/internal/hostactors"
12 "github.com/carlosframework/platform/internal/hostfloor"
13 "github.com/carlosframework/platform/internal/hostops"
14 "github.com/carlosframework/platform/internal/release"
15)
16
17// BackfillInput is the backfill phases' plan: the
18// reservation id the boxes' census reports are published under
19// (control/host-census/<id>/<box>.json). Nothing else is typed: the hosts
20// come from the bucket's own record listing, and each host's box and region
21// from the boxes' signed census reports.
22type BackfillInput struct {
23 ID string `json:"id"`
24}
25
26// BackfillHost is one frozen host, as read back from the signed plan.
27type BackfillHost struct {
28 Host string `json:"host"`
29 hostops.BackfillHost
30}
31
32// BackfillSummary is what the backfill phases read back from the signed
33// plan after writing it (or, on a dry run, what they would write).
34type BackfillSummary struct {
35 ID string `json:"id"`
36 Phase string `json:"phase"`
37 DirectoryRevision uint64 `json:"directory_revision"`
38 Hosts []BackfillHost `json:"hosts"`
39 // Reports is each box's census report digest, adopting only.
40 Reports map[string]string `json:"reports,omitempty"`
41 // Unowned lists the hosts no box reported. They keep no box and are
42 // never adopted. Adopting only.
43 Unowned []string `json:"unowned,omitempty"`
44 // Unreported lists the roster boxes with no census key, which
45 // backfill-done did not ask to report. Done only.
46 Unreported []string `json:"unreported,omitempty"`
47}
48
49var (
50 // ErrBackfillState is the refusal when the roster or the floor does not
51 // allow a backfill yet.
52 ErrBackfillState = errors.New("backfill refused: the roster must be published with a home region and a verified floor")
53 // ErrBackfillExists is backfill-collect's refusal over an existing plan.
54 ErrBackfillExists = errors.New("backfill refused: a backfill plan already exists")
55 // ErrBackfillRecords is backfill-collect's refusal when a record cannot
56 // be frozen: one without an acquisition, or records that changed while
57 // the plan was written.
58 ErrBackfillRecords = errors.New("backfill refused: every instance record must carry an acquisition and stay unchanged while the plan is written")
59 // ErrBackfillPlan is backfill-adopt's refusal when the plan is not the
60 // named reservation, still collecting, at this roster revision.
61 ErrBackfillPlan = errors.New("backfill refused: the plan must be the named reservation, still collecting, at this roster revision")
62 // ErrBackfillInstallations is backfill-done's refusal when a box's
63 // census report lists an installation: done is for an empty cohort,
64 // and there is no override.
65 ErrBackfillInstallations = errors.New("backfill refused: a census report lists installations; this phase is for an empty cohort")
66)
67
68// backfillReady is both phases' refusal up front: a published roster with a
69// home region, whose floor digest is the published floor's.
70func backfillReady(ctx context.Context, d hostactors.Directory, o hostactors.Observed) error {
71 if o.Roster.Publication != "published" || o.Roster.HomeRegion == "" || o.Roster.FloorSHA256 == "" {
72 return ErrBackfillState
73 }
74 floor, err := hostfloor.Read(ctx, d.Store, d.PublicKey, d.Bucket)
75 if err != nil {
76 return errors.Join(ErrBackfillState, err)
77 }
78 if floor.Digest != o.Roster.FloorSHA256 {
79 return ErrBackfillState
80 }
81 return nil
82}
83
84// listBackfillHosts enumerates every record in the deployment through the
85// same listing the box census and the adopters read (hostops.Apps and
86// hostops.Records).
87func listBackfillHosts(ctx context.Context, s release.Store) (map[string]hostops.BackfillHost, error) {
88 apps, err := hostops.Apps(ctx, s)
89 if err != nil {
90 return nil, err
91 }
92 hosts := map[string]hostops.BackfillHost{}
93 for _, app := range apps {
94 records, err := hostops.Records(ctx, s, app.Account, app.Name)
95 if err != nil {
96 return nil, err
97 }
98 for _, r := range records {
99 if _, dup := hosts[r.Holder.Host]; dup || r.Holder.Host == "" || r.Holder.Key == "" || r.ETag == "" {
100 return nil, ErrBackfillRecords
101 }
102 if r.Holder.Acquisition == "" {
103 return nil, ErrBackfillRecords
104 }
105 hosts[r.Holder.Host] = hostops.BackfillHost{Key: r.Holder.Key, ETag: r.ETag, Acquisition: r.Holder.Acquisition}
106 }
107 }
108 return hosts, nil
109}
110
111func backfillSummary(p hostops.BackfillPlan) BackfillSummary {
112 out := BackfillSummary{ID: p.ID, Phase: p.Phase, DirectoryRevision: p.DirectoryRevision, Hosts: []BackfillHost{}, Reports: p.Reports}
113 for host, h := range p.Hosts {
114 out.Hosts = append(out.Hosts, BackfillHost{Host: host, BackfillHost: h})
115 if p.Phase == "adopting" && h.Box == "" {
116 out.Unowned = append(out.Unowned, host)
117 }
118 }
119 sort.Slice(out.Hosts, func(i, j int) bool { return out.Hosts[i].Host < out.Hosts[j].Host })
120 sort.Strings(out.Unowned)
121 return out
122}
123
124// readSignedPlan reads the backfill plan and verifies its root signature.
125func readSignedPlan(ctx context.Context, d hostactors.Directory) (hostops.BackfillPlan, string, error) {
126 var p hostops.BackfillPlan
127 raw, tag, err := d.Store.Get(ctx, hostops.BackfillKey)
128 if err != nil {
129 return p, "", err
130 }
131 if tag == "" || release.VerifyRootEnvelope(d.PublicKey, raw, &p) != nil || p.V != 1 {
132 return p, "", ErrBackfillPlan
133 }
134 return p, tag, nil
135}
136
137// backfillCollect writes the signed collecting plan, create-only. The plan
138// freezes every instance record in the deployment by key and etag: the box
139// census refuses any record it does not name, and claim-mode record writers
140// refuse while it exists. It is read back, verified, and the records are
141// listed again afterwards: a record that moved while the plan was written
142// is a refusal, because the box census would refuse the plan anyway.
143func (r Runner) backfillCollect(ctx context.Context, d hostactors.Directory, o hostactors.Observed, in BackfillInput, result *Result) error {
144 if !validBackfillID(in.ID) {
145 return ErrActivation
146 }
147 if err := backfillReady(ctx, d, o); err != nil {
148 return err
149 }
150 if _, _, err := d.Store.Get(ctx, hostops.BackfillKey); err == nil {
151 return ErrBackfillExists
152 } else if !errors.Is(err, os.ErrNotExist) {
153 return err
154 }
155 hosts, err := listBackfillHosts(ctx, d.Store)
156 if err != nil {
157 return err
158 }
159 p := hostops.BackfillPlan{V: 1, ID: in.ID, Phase: "collecting", DirectoryRevision: o.Roster.Revision, CreatedAt: time.Now().UTC(), Hosts: hosts}
160 body, err := release.SignRootEnvelope(r.Signer, p)
161 if err != nil {
162 return err
163 }
164 if _, err = d.Store.Put(ctx, hostops.BackfillKey, body, ""); err != nil {
165 return err
166 }
167 got, _, err := readSignedPlan(ctx, d)
168 if err != nil {
169 return err
170 }
171 if got.Phase != "collecting" || got.ID != in.ID || got.DirectoryRevision != o.Roster.Revision || !reflect.DeepEqual(got.Hosts, hosts) {
172 return ErrBackfillPlan
173 }
174 again, err := listBackfillHosts(ctx, d.Store)
175 if err != nil {
176 return err
177 }
178 if !reflect.DeepEqual(again, hosts) {
179 return ErrBackfillRecords
180 }
181 summary := backfillSummary(got)
182 result.Backfill = &summary
183 return nil
184}
185
186// backfillAdopt advances the named collecting plan to adopting through
187// hostops' Backfill.Publish, under the plan's own etag (If-Match). Publish
188// requires every roster box's signed census report for the reservation,
189// freezes their digests, takes each host's box and region from them, and
190// refuses unfinished seed state. The adopters' own acceptance
191// (VerifyAdopting) is then run on the result as readback.
192func (r Runner) backfillAdopt(ctx context.Context, d hostactors.Directory, o hostactors.Observed, in BackfillInput, result *Result) error {
193 if !validBackfillID(in.ID) {
194 return ErrActivation
195 }
196 if err := backfillReady(ctx, d, o); err != nil {
197 return err
198 }
199 p, tag, err := readSignedPlan(ctx, d)
200 if err != nil {
201 return err
202 }
203 if p.ID != in.ID || p.Phase != "collecting" || p.DirectoryRevision != o.Roster.Revision {
204 return ErrBackfillPlan
205 }
206 b := &hostops.Backfill{Store: d.Store, Directory: d}
207 if err := b.Publish(ctx, r.Signer, tag); err != nil {
208 return err
209 }
210 got, err := b.VerifyAdopting(ctx)
211 if err != nil {
212 return err
213 }
214 if got.ID != in.ID || got.DirectoryRevision != o.Roster.Revision || len(got.Hosts) != len(p.Hosts) {
215 return ErrBackfillPlan
216 }
217 for host, h := range p.Hosts {
218 n, ok := got.Hosts[host]
219 if !ok || n.Key != h.Key || n.ETag != h.ETag || n.Acquisition != h.Acquisition {
220 return ErrBackfillPlan
221 }
222 }
223 summary := backfillSummary(got)
224 result.Backfill = &summary
225 return nil
226}
227
228// backfillDone closes the named collecting plan straight to done, for an
229// empty cohort (claim trial Option B, Paul's D1 of 2026-10-20), through
230// hostops' Backfill.Done under the plan's own etag (If-Match). Done adopts
231// nothing: it refuses unless the records are the plan's, no host claim or
232// app admission exists, and every roster box with a census key reports no
233// installation. Boxes with no key are listed as unreported. The plan is then
234// read back as done.
235func (r Runner) backfillDone(ctx context.Context, d hostactors.Directory, o hostactors.Observed, in BackfillInput, result *Result) error {
236 if !validBackfillID(in.ID) {
237 return ErrActivation
238 }
239 if err := backfillReady(ctx, d, o); err != nil {
240 return err
241 }
242 p, tag, err := readSignedPlan(ctx, d)
243 if err != nil {
244 return err
245 }
246 if p.ID != in.ID || p.Phase != "collecting" || p.DirectoryRevision != o.Roster.Revision {
247 return ErrBackfillPlan
248 }
249 b := &hostops.Backfill{Store: d.Store, Directory: d}
250 unreported, err := b.Done(ctx, r.Signer, tag)
251 switch {
252 case errors.Is(err, hostops.ErrDoneInstallations):
253 return errors.Join(ErrBackfillInstallations, err)
254 case errors.Is(err, hostops.ErrDoneRecords):
255 return errors.Join(ErrBackfillRecords, err)
256 case errors.Is(err, hostops.ErrDoneETag), errors.Is(err, hostops.ErrDoneEnvelope), errors.Is(err, hostops.ErrDonePhase):
257 return errors.Join(ErrBackfillPlan, err)
258 case err != nil:
259 return err
260 }
261 got, _, err := readSignedPlan(ctx, d)
262 if err != nil {
263 return err
264 }
265 if got.ID != in.ID || got.Phase != "done" || got.DirectoryRevision != o.Roster.Revision || len(got.Hosts) != len(p.Hosts) {
266 return ErrBackfillPlan
267 }
268 for host, h := range p.Hosts {
269 if got.Hosts[host] != h {
270 return ErrBackfillPlan
271 }
272 }
273 summary := backfillSummary(got)
274 summary.Unreported = unreported
275 result.Backfill = &summary
276 return nil
277}
278
279// validBackfillID keeps the reservation id one safe path segment.
280func validBackfillID(id string) bool {
281 if id == "" || len(id) > 64 || id == "." || id == ".." {
282 return false
283 }
284 for _, c := range id {
285 if !(c >= 'a' && c <= 'z' || c >= '0' && c <= '9' || c == '-' || c == '_' || c == '.') {
286 return false
287 }
288 }
289 return true
290}
291