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 "encoding/json"
6 "path/filepath"
7 "sort"
8 "strings"
9 "time"
10
11 "github.com/carlosframework/platform/internal/hostactors"
12 "github.com/carlosframework/platform/internal/hostbox"
13 "github.com/carlosframework/platform/internal/hostclaim"
14 "github.com/carlosframework/platform/internal/hostguard"
15 "github.com/carlosframework/platform/internal/hostops"
16 "github.com/carlosframework/platform/internal/provision"
17 "github.com/carlosframework/platform/internal/registry"
18 "github.com/carlosframework/platform/internal/release"
19)
20
21type rootMigrationCollector struct {
22 suspects func(context.Context) ([]string, error)
23 retirements func(context.Context) ([]hostbox.RetirementJournal, error)
24 store release.Store
25 directory hostactors.Directory
26 identity rootMigrationIdentity
27 seeds hostops.SeedStateReader
28 installations func(context.Context) ([]hostguard.FenceView, error)
29 launches func(context.Context) ([]hostbox.LaunchInventory, error)
30 reservations func(context.Context) ([]release.GenerationReservation, error)
31 boot func() (string, error)
32 // routes is the box's registry listing and dataDir its data root. A
33 // legacy route row, an ordinary instance route no claim owns yet, is
34 // reported as the box's route installation of its frozen record, so the
35 // backfill can adopt it to this box. Nil reports none (the census then
36 // refuses the row's files, as before).
37 routes func() ([]registry.Route, error)
38 dataDir string
39}
40
41// legacyRouteRow is the one route shape the migration census reports as a
42// legacy installation: an ordinary nested-layout instance row, unowned by
43// any claim, of the frozen record's account and app. These are the routes
44// the files seed can serve; every other row refuses the census.
45func legacyRouteRow(row registry.Route, r hostops.Record, dataDir string) bool {
46 return row.Kind == "instance" && r.Holder.Family == "instance" && row.ClaimOwnership == (registry.ClaimOwnership{}) &&
47 !row.Replica && row.Seed == "" && row.SeedSource == "" && row.Backing == "" && row.Unit == "" && !row.IsSidecar() && row.Tunnel == "" &&
48 row.Account == r.Holder.Account && row.App == r.Holder.App && filepath.IsAbs(dataDir) && row.DBPath == provision.NestedDBPath(dataDir, row.Host) &&
49 release.ValidVersion(row.Version)
50}
51
52func (c rootMigrationCollector) collect(ctx context.Context) (rootMigrationSnapshot, error) {
53 s := rootMigrationSnapshot{Items: []rootMigrationItem{}}
54 if c.store == nil || c.seeds == nil || c.installations == nil || c.launches == nil || c.reservations == nil || c.boot == nil {
55 return s, errRootMigrationBusy
56 }
57 raw, tag, err := c.store.Get(ctx, hostops.BackfillKey)
58 if err != nil {
59 return s, err
60 }
61 var plan hostops.BackfillPlan
62 if tag == "" || release.VerifyRootEnvelope(c.directory.PublicKey, raw, &plan) != nil || plan.V != 1 || plan.ID == "" || plan.Phase != "collecting" || plan.Hosts == nil || plan.CreatedAt.IsZero() {
63 return s, errRootMigrationBusy
64 }
65 roster, err := c.directory.Read(ctx)
66 if err != nil {
67 return s, err
68 }
69 box, ok := roster.Roster.Boxes[c.identity.Box]
70 if !ok || box.Enrollment != c.identity.Enrollment || plan.DirectoryRevision != roster.Roster.Revision {
71 return s, errRootMigrationBusy
72 }
73 s.DirectoryRevision = roster.Roster.Revision
74 s.Exclusion = tag + "@" + generationInventoryDigest(plan)
75 boot, err := c.boot()
76 if err != nil || boot == "" {
77 return s, errRootMigrationBusy
78 }
79 s.Report = hostops.CensusReport{Box: c.identity.Box, Enrollment: c.identity.Enrollment, BootID: boot, Reservation: plan.ID, DirectoryRevision: s.DirectoryRevision, CapturedAt: time.Now().UTC(), Installations: []hostops.CensusInstallation{}}
80 // These service-owned fields only refuse; they establish no installation.
81 s.Report.Seeds, err = hostops.CollectCensusSeeds(c.seeds)
82 if err != nil {
83 return s, err
84 }
85 records := map[string]hostops.Record{}
86 apps, err := hostops.Apps(ctx, c.store)
87 if err != nil {
88 return s, err
89 }
90 for _, app := range apps {
91 rows, err := hostops.Records(ctx, c.store, app.Account, app.Name)
92 if err != nil {
93 return s, err
94 }
95 for _, r := range rows {
96 if _, ok := records[r.Holder.Host]; ok {
97 return s, errRootMigrationBusy
98 }
99 frozen, ok := plan.Hosts[r.Holder.Host]
100 if !ok || frozen.Key != r.Holder.Key || frozen.ETag != r.ETag {
101 return s, errRootMigrationBusy
102 }
103 records[r.Holder.Host] = r
104 if r.Holder.Family == "instance" {
105 var rec provision.Record
106 if json.Unmarshal(r.Body, &rec) != nil {
107 return s, errRootMigrationBusy
108 }
109 s.Records = append(s.Records, rec)
110 }
111 }
112 }
113 if len(records) != len(plan.Hosts) {
114 return s, errRootMigrationBusy
115 }
116 keys, err := c.store.List(ctx, "control/hosts/")
117 if err != nil {
118 return s, err
119 }
120 sort.Strings(keys)
121 observed := map[string]hostclaim.Observed{}
122 for _, key := range keys {
123 if !strings.HasSuffix(key, ".json") || strings.Contains(strings.TrimPrefix(key, "control/hosts/"), "/") {
124 continue
125 }
126 host := strings.TrimSuffix(strings.TrimPrefix(key, "control/hosts/"), ".json")
127 want, err := hostclaim.Key(host)
128 if err != nil || key != want {
129 return s, errRootMigrationBusy
130 }
131 o, err := hostclaim.Read(ctx, c.store, host)
132 if err != nil || o.Absent || o.ETag == "" {
133 return s, errRootMigrationBusy
134 }
135 s.Claims = append(s.Claims, *o.Claim)
136 observed[host] = o
137 }
138 retained, err := c.installations(ctx)
139 if err != nil {
140 return s, err
141 }
142 if err := refuseSettledInstallations(retained); err != nil {
143 return s, err
144 }
145 if c.retirements != nil {
146 retired, err := c.retirements(ctx)
147 if err != nil {
148 return s, err
149 }
150 for _, j := range retired {
151 if !j.Complete || j.Target == nil {
152 s.MoveJournals++
153 } else if _, ok := observed[j.Target.Host]; !ok {
154 return s, errRootMigrationBusy
155 }
156 }
157 }
158 if c.suspects != nil {
159 suspects, err := c.suspects(ctx)
160 if err != nil {
161 return s, err
162 }
163 for _, host := range suspects {
164 if _, ok := observed[host]; !ok {
165 return s, errRootMigrationBusy
166 }
167 }
168 }
169 local := map[string]hostguard.RootInstallation{}
170 for _, v := range retained {
171 local[v.Host] = v.Installation
172 }
173 launches, err := c.launches(ctx)
174 if err != nil {
175 return s, err
176 }
177 for _, l := range launches {
178 if l.Target.Op != "" && !l.WritersClosed {
179 s.MoveJournals++
180 }
181 if _, ok := local[l.Target.Host]; !ok {
182 return s, errRootMigrationBusy
183 }
184 }
185 reservations, err := c.reservations(ctx)
186 if err != nil {
187 return s, err
188 }
189 // Any unfinished reservation must be reconciled through normal generation
190 // retirement first. A move cannot hide by deleting its record or launch row.
191 for _, r := range reservations {
192 if !r.Closed {
193 s.MoveReservations++
194 }
195 }
196 names := make([]string, 0, len(observed))
197 for host := range observed {
198 names = append(names, host)
199 }
200 sort.Strings(names)
201 for _, host := range names {
202 o := observed[host]
203 h := o.Claim.Holder
204 _, retained := local[host]
205 if h == nil {
206 if retained {
207 return s, errRootMigrationBusy
208 }
209 continue
210 }
211 relevant := h.WriterBox == c.identity.Box || o.Claim.Op.Exec == c.identity.Box || retained
212 for b := range o.Claim.Inventory {
213 if b == c.identity.Box {
214 relevant = true
215 }
216 }
217 if !relevant {
218 continue
219 }
220 r, ok := records[host]
221 if !ok || r.Holder.Key != h.Key || r.Holder.Account != h.Account || r.Holder.App != h.App || h.Stamp == 0 || h.Session == 0 {
222 return s, errRootMigrationBusy
223 }
224 in, exists := local[host]
225 if exists {
226 if in.Incarnation != h.Incarnation || in.Stamp != h.Stamp || in.Box != c.identity.Box || in.Enrollment != c.identity.Enrollment || in.Record != h.Key || in.Account != h.Account || in.App != h.App {
227 return s, errRootMigrationBusy
228 }
229 } else {
230 in = hostguard.RootInstallation{Host: host, Incarnation: h.Incarnation, Stamp: h.Stamp, Account: h.Account, App: h.App, Record: h.Key, Box: c.identity.Box, Enrollment: c.identity.Enrollment, Migration: c.identity.Generation}
231 }
232 s.Items = append(s.Items, rootMigrationItem{Installation: in, Session: h.Session, ClaimETag: o.ETag})
233 s.Report.Installations = append(s.Report.Installations, hostops.CensusInstallation{Host: host, Account: h.Account, App: h.App, Family: h.Family, Key: h.Key, Incarnation: h.Incarnation, Stamp: h.Stamp, Local: true})
234 delete(local, host)
235 }
236 if len(local) != 0 {
237 return s, errRootMigrationBusy
238 }
239 if c.routes != nil {
240 rows, err := c.routes()
241 if err != nil {
242 return s, err
243 }
244 reported := map[string]int{}
245 for i, in := range s.Report.Installations {
246 reported[in.Host] = i
247 }
248 for _, row := range rows {
249 if _, ok := reported[row.Host]; ok {
250 // A claimed installation of this box: the claim's own
251 // inventory accounts for its row.
252 continue
253 }
254 r, ok := records[row.Host]
255 if !ok || !legacyRouteRow(row, r, c.dataDir) {
256 return s, errRootMigrationBusy
257 }
258 inc, err := r.Incarnation()
259 if err != nil {
260 return s, errRootMigrationBusy
261 }
262 reported[row.Host] = len(s.Report.Installations)
263 s.Report.Installations = append(s.Report.Installations, hostops.CensusInstallation{Host: row.Host, Account: r.Holder.Account, App: r.Holder.App, Family: r.Holder.Family, Key: r.Holder.Key, Incarnation: inc, Route: true})
264 }
265 sort.Slice(s.Report.Installations, func(i, j int) bool { return s.Report.Installations[i].Host < s.Report.Installations[j].Host })
266 }
267 fresh, ftag, err := c.store.Get(ctx, hostops.BackfillKey)
268 if err != nil || ftag != tag || string(fresh) != string(raw) {
269 return s, errRootMigrationBusy
270 }
271 s.Complete = true
272 s.Report.Complete = true
273 return s, nil
274}
275
276// refuseSettledInstallations keeps journal-migrate's collector deciding as
277// it did before roster binding's fix rounds: an installation root resolved
278// terminally (settled: an obsolete-binding or superseded-claim intent,
279// quarantined) refuses the collection with ErrFenced, exactly as
280// MigrationInstallations itself refused every quarantined record. Only
281// startup's verifyRootMigrationStartup inspects settled installations.
282func refuseSettledInstallations(views []hostguard.FenceView) error {
283 for _, v := range views {
284 if v.Settled {
285 return hostguard.ErrFenced
286 }
287 }
288 return nil
289}
290