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 "encoding/json"
5 "errors"
6 "os"
7 "path/filepath"
8 "strings"
9
10 "github.com/carlosframework/platform/internal/hostguard"
11 "github.com/carlosframework/platform/internal/provision"
12 "github.com/carlosframework/platform/internal/registry"
13)
14
15// ValidateAgentInventory confines agent-authored artifacts to this route's
16// state directory and registry rows. Opt and units need root-captured authority;
17// an inventory written by the agent never supplies that authority.
18func ValidateAgentInventory(in StopInventory, row *registry.Route, dataDir string) error {
19 if !component(in.Host) {
20 return ErrTypedInventory
21 }
22 if row != nil && (row.Host != in.Host || row.ClaimIncarnation != in.Incarnation || row.ClaimStamp != in.InstallationStamp || !row.ClaimOwnership.Valid()) {
23 return ErrTypedInventory
24 }
25 seen := make(map[string]bool)
26 for _, e := range in.Entries {
27 if e.TimerRetired && e.Kind != "timer" {
28 return ErrTypedInventory
29 }
30 key := e.ManagedRoot + "\x00" + e.RelativePath
31 if seen[key] || !managedName(e.RelativePath) || (row != nil && e.Ownership != row.ClaimOwner) {
32 return ErrTypedInventory
33 }
34 seen[key] = true
35 switch e.Kind {
36 case "route":
37 if e.ManagedRoot != "registry" || e.RelativePath != in.Host || e.RowIdentity == "" || (row != nil && e.RowIdentity != row.ClaimRowID) || (row == nil && e.Materialization != "intended") {
38 return ErrTypedInventory
39 }
40 case "timer":
41 if row == nil || e.ManagedRoot != "registry" || !strings.HasPrefix(e.RelativePath, in.Host+"/") || !component(strings.TrimPrefix(e.RelativePath, in.Host+"/")) || e.RowIdentity == "" {
42 return ErrTypedInventory
43 }
44 case "path":
45 if row == nil || e.ManagedRoot != "data" || !filepath.IsAbs(dataDir) || !filepath.IsAbs(row.DBPath) {
46 return ErrTypedInventory
47 }
48 if row.DBPath != provision.NestedDBPath(dataDir, in.Host) && row.DBPath != provision.DefaultDBPath(dataDir, in.Host) {
49 return ErrTypedInventory
50 }
51 if filepath.Clean(e.RelativePath) != e.RelativePath || !filepath.IsLocal(e.RelativePath) {
52 return ErrTypedInventory
53 }
54 parent, err := filepath.Rel(dataDir, filepath.Dir(row.DBPath))
55 if err != nil || (parent != "." && !managedName(parent)) {
56 return ErrTypedInventory
57 }
58 if parent == "." {
59 if row.DBPath == filepath.Join(dataDir, "registry.db") {
60 return ErrTypedInventory
61 }
62 // Flat legacy layouts: the database and its exact SQLite
63 // companion names. Enabled claim launches require the nested
64 // layout, so an agent has no flat state-directory authority.
65 db := filepath.Base(row.DBPath)
66 isDB := e.RelativePath == db || e.RelativePath == db+"-wal" || e.RelativePath == db+"-shm" || e.RelativePath == db+"-journal"
67 if !isDB {
68 return ErrTypedInventory
69 }
70 } else if e.RelativePath != parent && !strings.HasPrefix(e.RelativePath, parent+string(filepath.Separator)) {
71 // The host's own nested state container (root derives it from
72 // NestedDBPath), or a path inside it. Parking the empty container
73 // is what lets a cold handover find the namespace empty.
74 return ErrTypedInventory
75 }
76 default:
77 return ErrTypedInventory
78 }
79 }
80 return nil
81}
82
83// retirementRoute recovers the row root archived before removing it. The
84// agent's inventory carries no database path and cannot choose this namespace.
85func (a ReconcileAdapter) retirementRoute(row registry.Route, target hostguard.FenceTarget) (*registry.Route, error) {
86 current, err := a.Registry.Get(row.Host)
87 if err != nil {
88 return nil, err
89 }
90 if current != nil && current.ClaimOwnership == row.ClaimOwnership {
91 return current, nil
92 }
93 raw, err := readTrustedJournal(filepath.Join(a.Retirement.JournalDir, target.FenceID+".json"))
94 if errors.Is(err, os.ErrNotExist) {
95 return nil, ErrTypedInventory
96 }
97 if err != nil {
98 return nil, err
99 }
100 var journal RetirementJournal
101 if decodeStrict(raw, &journal) != nil || journal.Target == nil || *journal.Target != target || journal.ID != target.FenceID {
102 return nil, ErrTypedJournal
103 }
104 var archives []registry.ClaimRowArchive
105 if json.Unmarshal(journal.Archives, &archives) != nil {
106 return nil, ErrTypedJournal
107 }
108 var found *registry.Route
109 for _, archive := range archives {
110 if archive.Row.Kind != "route" {
111 continue
112 }
113 var saved registry.Route
114 if found != nil || archive.Row.Host != row.Host || archive.Row.Ownership != row.ClaimOwnership || json.Unmarshal(archive.Contents, &saved) != nil || saved.Host != row.Host || saved.ClaimOwnership != row.ClaimOwnership {
115 return nil, ErrTypedJournal
116 }
117 found = &saved
118 }
119 if found == nil {
120 return nil, ErrTypedInventory
121 }
122 return found, nil
123}
124