carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package hostbox
2
3import (
4 "context"
5 "database/sql"
6 "encoding/json"
7 "errors"
8 "github.com/carlosframework/platform/internal/hostguard"
9 "io/fs"
10 "os"
11 "path/filepath"
12 "slices"
13 "strings"
14
15 "github.com/carlosframework/platform/internal/hostclaim"
16 "github.com/carlosframework/platform/internal/registry"
17)
18
19type EmptyInstallationCensus struct {
20 Inventory *StopInventoryDB
21 Registry *registry.Registry
22 Launches *LaunchJournal
23 Box, Enrollment, DirectoryRevision string
24 Roots map[string]string
25 // The factory owns this closed list of shared metadata and archives,
26 // keyed by root, in the entry forms CensusFile, CensusTree, CensusLink
27 // build. Hostguard checks its own records and
28 // fences; excluding that directory here never assigns its contents to an
29 // installation artifact inventory.
30 SharedPaths map[string][]string
31 OtherPath func(context.Context, string, string, string) (bool, error)
32}
33
34// CheckHeld is the hold-only path's local check (region HA R5-3; Astra
35// Task 10b round 1): the row this box would hold for a relocation must be
36// its own existing installation's, for the record's incarnation. It reads
37// the host's row and that installation's inventory at the row's own stamp,
38// never at the claim holder's: a relocation's source is held after
39// ToInstalling has moved holder.stamp to the target's install stamp, which
40// this box never had, and CheckLocal would ask for that. It writes nothing
41// and grants nothing: holding is not installing. The directory revision is
42// not compared: it says which roster admitted the installation, and a hold
43// needs only that the installation is this box's. Absence is never
44// evidence: no row is ErrChanged (there is nothing to hold); a row with no
45// valid ownership, no complete inventory of this box and enrollment at its
46// stamp, or a route entry that does not name exactly its owner and row
47// identity, is ErrTypedInventory; a read failure is its error.
48func (c EmptyInstallationCensus) CheckHeld(ctx context.Context, obs hostclaim.Observed, selected RecordSelection) error {
49 if c.Inventory == nil || c.Registry == nil || obs.Claim == nil || obs.Claim.Holder == nil || c.Box == "" || c.Enrollment == "" {
50 return ErrUnsupported
51 }
52 if err := ctx.Err(); err != nil {
53 return err
54 }
55 if obs.Host != selected.Host || selected.Incarnation == "" || obs.Claim.Holder.Incarnation != selected.Incarnation {
56 return ErrChanged
57 }
58 row, err := c.Registry.Get(selected.Host)
59 if err != nil {
60 return err
61 }
62 if row == nil {
63 return ErrChanged
64 }
65 own := row.ClaimOwnership
66 if !own.Valid() || own.ClaimIncarnation != selected.Incarnation {
67 return ErrTypedInventory
68 }
69 in, err := c.Inventory.ReadInstallation(ctx, selected.Host, own.ClaimIncarnation, own.ClaimStamp)
70 if errors.Is(err, sql.ErrNoRows) {
71 return ErrTypedInventory
72 }
73 if err != nil {
74 return err
75 }
76 return c.checkHeldInventory(selected, own, in)
77}
78
79func (c EmptyInstallationCensus) checkHeldInventory(selected RecordSelection, own registry.ClaimOwnership, in StopInventory) error {
80 if !in.Complete || in.Host != selected.Host || in.Incarnation != own.ClaimIncarnation || in.InstallationStamp != own.ClaimStamp || in.Box != c.Box || in.Enrollment != c.Enrollment {
81 return ErrTypedInventory
82 }
83 routes := 0
84 for _, e := range in.Entries {
85 if e.Kind != "route" || e.RelativePath != selected.Host {
86 continue
87 }
88 routes++
89 if e.Ownership != own.ClaimOwner || e.RowIdentity != own.ClaimRowID || e.Materialization != "materialized" {
90 return ErrTypedInventory
91 }
92 }
93 if routes != 1 {
94 return ErrTypedInventory
95 }
96 return nil
97}
98
99func (c EmptyInstallationCensus) CheckLocal(ctx context.Context, obs hostclaim.Observed, selected RecordSelection) error {
100 return c.checkLocal(ctx, obs, selected, registry.ClaimOwnership{})
101}
102func (c EmptyInstallationCensus) CheckRootRegisteredRow(ctx context.Context, obs hostclaim.Observed, selected RecordSelection, owner registry.ClaimOwnership) error {
103 if !owner.Valid() {
104 return ErrTypedInventory
105 }
106 return c.checkLocal(ctx, obs, selected, owner)
107}
108func (c EmptyInstallationCensus) checkLocal(ctx context.Context, obs hostclaim.Observed, selected RecordSelection, registered registry.ClaimOwnership) error {
109 if c.Inventory == nil || c.Registry == nil || c.Launches == nil || obs.Claim == nil || len(c.Roots) == 0 || c.Box == "" || c.Enrollment == "" || c.DirectoryRevision == "" {
110 return ErrUnsupported
111 }
112 for _, root := range []string{"data", "opt", "units"} {
113 if c.Roots[root] == "" {
114 return ErrTypedInventory
115 }
116 }
117 h := obs.Claim.Holder
118 if obs.Host != selected.Host || selected.Incarnation == "" {
119 return ErrChanged
120 }
121 cleanupOnly := h == nil || h.Incarnation != selected.Incarnation
122 if cleanupOnly {
123 // CC9 may discharge a previous controller after parking or retirement.
124 // This branch proves absence; installation metadata cannot stand in for it.
125 if obs.Absent || obs.Claim.Host != selected.Host || obs.Claim.Validate() != nil ||
126 (!obs.Claim.Free() && (h == nil || !slices.Contains(obs.Claim.RetiredIncarnations, selected.Incarnation))) {
127 return ErrChanged
128 }
129 } else {
130 if h.Stamp == 0 && (obs.Claim.Op.Kind != hostclaim.KindCreating || obs.Claim.Op.Phase != "" || obs.Claim.Op.Exec != "" || h.Session != 0 || h.WriterBox != "" || len(obs.Claim.Op.Participants) != 1 || obs.Claim.Op.Participants[0] != c.Box || obs.Claim.Op.Intent["selected_executor"] != c.Box || obs.Claim.Op.Intent["executor_enrollment"] != c.Enrollment) {
131 return ErrChanged
132 }
133 in, err := c.Inventory.ReadInstallation(ctx, selected.Host, h.Incarnation, h.Stamp)
134 if err == nil {
135 if !in.Complete || in.Host != selected.Host || in.Incarnation != h.Incarnation || in.InstallationStamp != h.Stamp || in.Box != c.Box || in.Enrollment != c.Enrollment || in.DirectoryRevision != c.DirectoryRevision {
136 return ErrTypedInventory
137 }
138 return nil
139 }
140 if !errors.Is(err, sql.ErrNoRows) {
141 return err
142 }
143 }
144 // Existing metadata is never reassigned. New installations can initialize
145 // only from an explicitly empty census. Plan 5 owns populated backfill.
146 rows, err := c.Registry.List()
147 if err != nil {
148 return err
149 }
150 for _, row := range rows {
151 if row.Host == selected.Host && (!registered.Valid() || row.ClaimOwnership != registered) {
152 return ErrTypedInventory
153 }
154 }
155 timers, err := c.Registry.TimersFor(selected.Host)
156 if err != nil {
157 return err
158 }
159 if len(timers) != 0 {
160 return ErrTypedInventory
161 }
162 has, err := c.Launches.HasHost(ctx, selected.Host)
163 if err != nil {
164 return err
165 }
166 if has {
167 return ErrTypedInventory
168 }
169 old, err := c.Inventory.ReadStopInventory(ctx, selected.Host)
170 if err == nil && (old.Incarnation != selected.Incarnation || (!cleanupOnly && old.InstallationStamp != h.Stamp)) {
171 return ErrTypedInventory
172 }
173 if err != nil && !errors.Is(err, sql.ErrNoRows) {
174 return err
175 }
176 for name, root := range c.Roots {
177 if !component(name) || !filepath.IsAbs(root) {
178 return ErrTypedInventory
179 }
180 info, err := os.Lstat(root)
181 if err != nil {
182 return err
183 }
184 if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
185 return ErrTypedInventory
186 }
187 rules, err := censusRules(c.SharedPaths[name], selected.Host)
188 if err != nil {
189 return err
190 }
191 err = filepath.WalkDir(root, func(path string, d fs.DirEntry, walkErr error) error {
192 if walkErr != nil {
193 return walkErr
194 }
195 if err := ctx.Err(); err != nil {
196 return err
197 }
198 rel, err := filepath.Rel(root, path)
199 if err != nil {
200 return err
201 }
202 if rel == "." {
203 return nil
204 }
205 rel = filepath.ToSlash(rel)
206 if censusNamesHost(rel, selected.Host) {
207 return ErrTypedInventory
208 }
209 if c.OtherPath != nil {
210 known, err := c.OtherPath(ctx, selected.Host, name, rel)
211 if err != nil {
212 return err
213 }
214 if known {
215 if d.IsDir() {
216 return filepath.SkipDir
217 }
218 return nil
219 }
220 }
221 if rule, ok := rules[rel]; ok {
222 return rule.check(path, d)
223 }
224 if name == "units" && censusVendorUnitLink(root, path) {
225 return nil
226 }
227 if censusInTree(rules, rel) {
228 // Shared release content: directories and regular files only.
229 if d.IsDir() || d.Type().IsRegular() {
230 return nil
231 }
232 return ErrTypedInventory
233 }
234 // A directory carries no ownership by itself; every entry below
235 // it is censused. Conservatively refuse unclassified files from
236 // any installation. This covers flat, nested and unknown legacy
237 // layouts without guessing owners.
238 if d.IsDir() {
239 return nil
240 }
241 return ErrTypedInventory
242 })
243 if err != nil {
244 return err
245 }
246 }
247 return nil
248}
249
250// SharedPaths entries. An untagged entry is an opaque shared file or
251// directory whose contents this census does not read (hostguard and the
252// claim journals check their own records). The tagged forms are the precise
253// classes. None of them assigns anything to an installation.
254const (
255 censusFileTag = "file:"
256 censusTreeTag = "tree:"
257 censusLinkTag = "link:"
258)
259
260// CensusFile classifies rel as one shared regular file.
261func CensusFile(rel string) string { return censusFileTag + rel }
262
263// CensusTree classifies rel as a shared directory whose whole subtree may
264// hold only directories and regular files, still checked for the host name.
265func CensusTree(rel string) string { return censusTreeTag + rel }
266
267// CensusLink classifies rel as a symlink whose raw target is exactly target.
268func CensusLink(rel, target string) string { return censusLinkTag + rel + "\x00" + target }
269
270type censusRule struct {
271 kind string
272 target string
273}
274
275func censusRules(entries []string, host string) (map[string]censusRule, error) {
276 rules := map[string]censusRule{}
277 for _, entry := range entries {
278 rule := censusRule{}
279 rel := entry
280 for _, tag := range []string{censusFileTag, censusTreeTag, censusLinkTag} {
281 if rest, ok := strings.CutPrefix(entry, tag); ok {
282 rule.kind, rel = tag, rest
283 break
284 }
285 }
286 if rule.kind == censusLinkTag {
287 var ok bool
288 rel, rule.target, ok = strings.Cut(rel, "\x00")
289 if !ok || !filepath.IsAbs(rule.target) || filepath.Clean(rule.target) != rule.target || strings.ContainsRune(rule.target, 0) || censusNamesHost(rule.target, host) {
290 return nil, ErrTypedInventory
291 }
292 }
293 if !managedName(rel) || censusNamesHost(rel, host) {
294 return nil, ErrTypedInventory
295 }
296 if old, ok := rules[rel]; ok && old != rule {
297 return nil, ErrTypedInventory
298 }
299 rules[rel] = rule
300 }
301 return rules, nil
302}
303
304func (r censusRule) check(path string, d fs.DirEntry) error {
305 symlink := d.Type()&os.ModeSymlink != 0
306 switch r.kind {
307 case "":
308 if symlink {
309 return ErrTypedInventory
310 }
311 if d.IsDir() {
312 return filepath.SkipDir
313 }
314 return nil
315 case censusFileTag:
316 if !d.Type().IsRegular() {
317 return ErrTypedInventory
318 }
319 return nil
320 case censusTreeTag:
321 if symlink || !d.IsDir() {
322 return ErrTypedInventory
323 }
324 return nil
325 case censusLinkTag:
326 if !symlink {
327 return ErrTypedInventory
328 }
329 target, err := os.Readlink(path)
330 if err != nil {
331 return err
332 }
333 if target != r.target {
334 return ErrTypedInventory
335 }
336 return nil
337 }
338 return ErrTypedInventory
339}
340
341// censusInTree reports whether the nearest classified ancestor of rel is a
342// shared tree. An opaque ancestor is skipped before its contents are read.
343func censusInTree(rules map[string]censusRule, rel string) bool {
344 for dir := filepath.Dir(rel); dir != "." && dir != "/"; dir = filepath.Dir(dir) {
345 if rule, ok := rules[filepath.ToSlash(dir)]; ok {
346 return rule.kind == censusTreeTag
347 }
348 }
349 return false
350}
351
352func censusNamesHost(s, host string) bool {
353 return strings.Contains(s, host) || strings.Contains(s, strings.ReplaceAll(host, "-", `\x2d`))
354}
355
356func (c EmptyInstallationCensus) Initialize(ctx context.Context, obs hostclaim.Observed, selected RecordSelection) error {
357 if obs.Claim == nil || obs.Claim.Holder == nil || obs.Claim.Holder.Incarnation != selected.Incarnation || obs.Claim.Holder.Stamp == 0 {
358 return ErrChanged
359 }
360 if err := c.CheckLocal(ctx, obs, selected); err != nil {
361 return err
362 }
363 h := obs.Claim.Holder
364 if _, err := c.Inventory.ReadInstallation(ctx, selected.Host, h.Incarnation, h.Stamp); !errors.Is(err, sql.ErrNoRows) {
365 if err != nil {
366 return err
367 }
368 return ErrChanged
369 }
370 return c.Inventory.WriteStopInventory(ctx, StopInventory{Complete: true, Host: selected.Host, Incarnation: h.Incarnation, InstallationStamp: h.Stamp, Box: c.Box, Enrollment: c.Enrollment, DirectoryRevision: c.DirectoryRevision})
371}
372func (j *LaunchJournal) HasHost(ctx context.Context, host string) (bool, error) {
373 if j == nil {
374 return false, ErrUnsupported
375 }
376 if j.remote != nil {
377 out, err := journalCall(ctx, j.remote, JournalRequest{Op: "has-host", Target: hostguard.FenceTarget{Host: host}})
378 return out.Found, err
379 }
380
381 if j == nil || j.db == nil || !component(host) {
382 return false, ErrUnsupported
383 }
384 rows, err := journalExecutorFor(ctx, j.db).QueryContext(ctx, "SELECT body FROM launches")
385 if err != nil {
386 return false, err
387 }
388 defer rows.Close()
389 for rows.Next() {
390 var body []byte
391 if err = rows.Scan(&body); err != nil {
392 return false, err
393 }
394 var in LaunchInventory
395 if err = json.Unmarshal(body, &in); err != nil {
396 return false, err
397 }
398 if !validLaunchOwner(in.Target) {
399 return false, ErrTypedInventory
400 }
401 if in.Target.Host == host {
402 return true, nil
403 }
404 }
405 return false, rows.Err()
406}
407