package hostbox import ( "context" "database/sql" "encoding/json" "errors" "github.com/carlosframework/platform/internal/hostguard" "io/fs" "os" "path/filepath" "slices" "strings" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/registry" ) type EmptyInstallationCensus struct { Inventory *StopInventoryDB Registry *registry.Registry Launches *LaunchJournal Box, Enrollment, DirectoryRevision string Roots map[string]string // The factory owns this closed list of shared metadata and archives, // keyed by root, in the entry forms CensusFile, CensusTree, CensusLink // build. Hostguard checks its own records and // fences; excluding that directory here never assigns its contents to an // installation artifact inventory. SharedPaths map[string][]string OtherPath func(context.Context, string, string, string) (bool, error) } // CheckHeld is the hold-only path's local check (region HA R5-3; Astra // Task 10b round 1): the row this box would hold for a relocation must be // its own existing installation's, for the record's incarnation. It reads // the host's row and that installation's inventory at the row's own stamp, // never at the claim holder's: a relocation's source is held after // ToInstalling has moved holder.stamp to the target's install stamp, which // this box never had, and CheckLocal would ask for that. It writes nothing // and grants nothing: holding is not installing. The directory revision is // not compared: it says which roster admitted the installation, and a hold // needs only that the installation is this box's. Absence is never // evidence: no row is ErrChanged (there is nothing to hold); a row with no // valid ownership, no complete inventory of this box and enrollment at its // stamp, or a route entry that does not name exactly its owner and row // identity, is ErrTypedInventory; a read failure is its error. func (c EmptyInstallationCensus) CheckHeld(ctx context.Context, obs hostclaim.Observed, selected RecordSelection) error { if c.Inventory == nil || c.Registry == nil || obs.Claim == nil || obs.Claim.Holder == nil || c.Box == "" || c.Enrollment == "" { return ErrUnsupported } if err := ctx.Err(); err != nil { return err } if obs.Host != selected.Host || selected.Incarnation == "" || obs.Claim.Holder.Incarnation != selected.Incarnation { return ErrChanged } row, err := c.Registry.Get(selected.Host) if err != nil { return err } if row == nil { return ErrChanged } own := row.ClaimOwnership if !own.Valid() || own.ClaimIncarnation != selected.Incarnation { return ErrTypedInventory } in, err := c.Inventory.ReadInstallation(ctx, selected.Host, own.ClaimIncarnation, own.ClaimStamp) if errors.Is(err, sql.ErrNoRows) { return ErrTypedInventory } if err != nil { return err } return c.checkHeldInventory(selected, own, in) } func (c EmptyInstallationCensus) checkHeldInventory(selected RecordSelection, own registry.ClaimOwnership, in StopInventory) error { if !in.Complete || in.Host != selected.Host || in.Incarnation != own.ClaimIncarnation || in.InstallationStamp != own.ClaimStamp || in.Box != c.Box || in.Enrollment != c.Enrollment { return ErrTypedInventory } routes := 0 for _, e := range in.Entries { if e.Kind != "route" || e.RelativePath != selected.Host { continue } routes++ if e.Ownership != own.ClaimOwner || e.RowIdentity != own.ClaimRowID || e.Materialization != "materialized" { return ErrTypedInventory } } if routes != 1 { return ErrTypedInventory } return nil } func (c EmptyInstallationCensus) CheckLocal(ctx context.Context, obs hostclaim.Observed, selected RecordSelection) error { return c.checkLocal(ctx, obs, selected, registry.ClaimOwnership{}) } func (c EmptyInstallationCensus) CheckRootRegisteredRow(ctx context.Context, obs hostclaim.Observed, selected RecordSelection, owner registry.ClaimOwnership) error { if !owner.Valid() { return ErrTypedInventory } return c.checkLocal(ctx, obs, selected, owner) } func (c EmptyInstallationCensus) checkLocal(ctx context.Context, obs hostclaim.Observed, selected RecordSelection, registered registry.ClaimOwnership) error { if c.Inventory == nil || c.Registry == nil || c.Launches == nil || obs.Claim == nil || len(c.Roots) == 0 || c.Box == "" || c.Enrollment == "" || c.DirectoryRevision == "" { return ErrUnsupported } for _, root := range []string{"data", "opt", "units"} { if c.Roots[root] == "" { return ErrTypedInventory } } h := obs.Claim.Holder if obs.Host != selected.Host || selected.Incarnation == "" { return ErrChanged } cleanupOnly := h == nil || h.Incarnation != selected.Incarnation if cleanupOnly { // CC9 may discharge a previous controller after parking or retirement. // This branch proves absence; installation metadata cannot stand in for it. if obs.Absent || obs.Claim.Host != selected.Host || obs.Claim.Validate() != nil || (!obs.Claim.Free() && (h == nil || !slices.Contains(obs.Claim.RetiredIncarnations, selected.Incarnation))) { return ErrChanged } } else { 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) { return ErrChanged } in, err := c.Inventory.ReadInstallation(ctx, selected.Host, h.Incarnation, h.Stamp) if err == nil { 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 { return ErrTypedInventory } return nil } if !errors.Is(err, sql.ErrNoRows) { return err } } // Existing metadata is never reassigned. New installations can initialize // only from an explicitly empty census. Plan 5 owns populated backfill. rows, err := c.Registry.List() if err != nil { return err } for _, row := range rows { if row.Host == selected.Host && (!registered.Valid() || row.ClaimOwnership != registered) { return ErrTypedInventory } } timers, err := c.Registry.TimersFor(selected.Host) if err != nil { return err } if len(timers) != 0 { return ErrTypedInventory } has, err := c.Launches.HasHost(ctx, selected.Host) if err != nil { return err } if has { return ErrTypedInventory } old, err := c.Inventory.ReadStopInventory(ctx, selected.Host) if err == nil && (old.Incarnation != selected.Incarnation || (!cleanupOnly && old.InstallationStamp != h.Stamp)) { return ErrTypedInventory } if err != nil && !errors.Is(err, sql.ErrNoRows) { return err } for name, root := range c.Roots { if !component(name) || !filepath.IsAbs(root) { return ErrTypedInventory } info, err := os.Lstat(root) if err != nil { return err } if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 { return ErrTypedInventory } rules, err := censusRules(c.SharedPaths[name], selected.Host) if err != nil { return err } err = filepath.WalkDir(root, func(path string, d fs.DirEntry, walkErr error) error { if walkErr != nil { return walkErr } if err := ctx.Err(); err != nil { return err } rel, err := filepath.Rel(root, path) if err != nil { return err } if rel == "." { return nil } rel = filepath.ToSlash(rel) if censusNamesHost(rel, selected.Host) { return ErrTypedInventory } if c.OtherPath != nil { known, err := c.OtherPath(ctx, selected.Host, name, rel) if err != nil { return err } if known { if d.IsDir() { return filepath.SkipDir } return nil } } if rule, ok := rules[rel]; ok { return rule.check(path, d) } if name == "units" && censusVendorUnitLink(root, path) { return nil } if censusInTree(rules, rel) { // Shared release content: directories and regular files only. if d.IsDir() || d.Type().IsRegular() { return nil } return ErrTypedInventory } // A directory carries no ownership by itself; every entry below // it is censused. Conservatively refuse unclassified files from // any installation. This covers flat, nested and unknown legacy // layouts without guessing owners. if d.IsDir() { return nil } return ErrTypedInventory }) if err != nil { return err } } return nil } // SharedPaths entries. An untagged entry is an opaque shared file or // directory whose contents this census does not read (hostguard and the // claim journals check their own records). The tagged forms are the precise // classes. None of them assigns anything to an installation. const ( censusFileTag = "file:" censusTreeTag = "tree:" censusLinkTag = "link:" ) // CensusFile classifies rel as one shared regular file. func CensusFile(rel string) string { return censusFileTag + rel } // CensusTree classifies rel as a shared directory whose whole subtree may // hold only directories and regular files, still checked for the host name. func CensusTree(rel string) string { return censusTreeTag + rel } // CensusLink classifies rel as a symlink whose raw target is exactly target. func CensusLink(rel, target string) string { return censusLinkTag + rel + "\x00" + target } type censusRule struct { kind string target string } func censusRules(entries []string, host string) (map[string]censusRule, error) { rules := map[string]censusRule{} for _, entry := range entries { rule := censusRule{} rel := entry for _, tag := range []string{censusFileTag, censusTreeTag, censusLinkTag} { if rest, ok := strings.CutPrefix(entry, tag); ok { rule.kind, rel = tag, rest break } } if rule.kind == censusLinkTag { var ok bool rel, rule.target, ok = strings.Cut(rel, "\x00") if !ok || !filepath.IsAbs(rule.target) || filepath.Clean(rule.target) != rule.target || strings.ContainsRune(rule.target, 0) || censusNamesHost(rule.target, host) { return nil, ErrTypedInventory } } if !managedName(rel) || censusNamesHost(rel, host) { return nil, ErrTypedInventory } if old, ok := rules[rel]; ok && old != rule { return nil, ErrTypedInventory } rules[rel] = rule } return rules, nil } func (r censusRule) check(path string, d fs.DirEntry) error { symlink := d.Type()&os.ModeSymlink != 0 switch r.kind { case "": if symlink { return ErrTypedInventory } if d.IsDir() { return filepath.SkipDir } return nil case censusFileTag: if !d.Type().IsRegular() { return ErrTypedInventory } return nil case censusTreeTag: if symlink || !d.IsDir() { return ErrTypedInventory } return nil case censusLinkTag: if !symlink { return ErrTypedInventory } target, err := os.Readlink(path) if err != nil { return err } if target != r.target { return ErrTypedInventory } return nil } return ErrTypedInventory } // censusInTree reports whether the nearest classified ancestor of rel is a // shared tree. An opaque ancestor is skipped before its contents are read. func censusInTree(rules map[string]censusRule, rel string) bool { for dir := filepath.Dir(rel); dir != "." && dir != "/"; dir = filepath.Dir(dir) { if rule, ok := rules[filepath.ToSlash(dir)]; ok { return rule.kind == censusTreeTag } } return false } func censusNamesHost(s, host string) bool { return strings.Contains(s, host) || strings.Contains(s, strings.ReplaceAll(host, "-", `\x2d`)) } func (c EmptyInstallationCensus) Initialize(ctx context.Context, obs hostclaim.Observed, selected RecordSelection) error { if obs.Claim == nil || obs.Claim.Holder == nil || obs.Claim.Holder.Incarnation != selected.Incarnation || obs.Claim.Holder.Stamp == 0 { return ErrChanged } if err := c.CheckLocal(ctx, obs, selected); err != nil { return err } h := obs.Claim.Holder if _, err := c.Inventory.ReadInstallation(ctx, selected.Host, h.Incarnation, h.Stamp); !errors.Is(err, sql.ErrNoRows) { if err != nil { return err } return ErrChanged } 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}) } func (j *LaunchJournal) HasHost(ctx context.Context, host string) (bool, error) { if j == nil { return false, ErrUnsupported } if j.remote != nil { out, err := journalCall(ctx, j.remote, JournalRequest{Op: "has-host", Target: hostguard.FenceTarget{Host: host}}) return out.Found, err } if j == nil || j.db == nil || !component(host) { return false, ErrUnsupported } rows, err := journalExecutorFor(ctx, j.db).QueryContext(ctx, "SELECT body FROM launches") if err != nil { return false, err } defer rows.Close() for rows.Next() { var body []byte if err = rows.Scan(&body); err != nil { return false, err } var in LaunchInventory if err = json.Unmarshal(body, &in); err != nil { return false, err } if !validLaunchOwner(in.Target) { return false, ErrTypedInventory } if in.Target.Host == host { return true, nil } } return false, rows.Err() }