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 "bufio"
5 "bytes"
6 "context"
7 "crypto/ed25519"
8 "log"
9 "os"
10 "slices"
11 "strconv"
12 "strings"
13 "sync/atomic"
14
15 "github.com/carlosframework/platform/internal/activator"
16 "github.com/carlosframework/platform/internal/dbdoor"
17 "github.com/carlosframework/platform/internal/dbreads"
18 "github.com/carlosframework/platform/internal/hostwrite"
19 "github.com/carlosframework/platform/internal/lease"
20 "github.com/carlosframework/platform/internal/release"
21)
22
23const (
24 dbReadSocketUnit = "carlos-dbread.socket"
25 // dbReadSocketPath is under /run/carlos, the tree every tenant unit hides
26 // (carlos-dbread.socket says why).
27 dbReadSocketPath = "/run/carlos/dbread/worker.sock"
28 dbReadMaxBytesEnv = "CARLOS_DB_READ_MAX_BYTES"
29 // dbPullMaxBytesEnv can only lower a pull's cap (spec §5.7, rev 7).
30 dbPullMaxBytesEnv = "CARLOS_DB_PULL_MAX_BYTES"
31 dbReadsSwitchEnv = "CARLOS_DB_READS"
32)
33
34type dbReadsEnv struct {
35 region string
36 pub ed25519.PublicKey
37 maxRestored int64
38 maxPull int64
39}
40
41// dbReadsSettings is the environment half of spec §7.1's conditions:
42// hibernation configured, CARLOS_REGION a region, CARLOS_RELEASE_PUBKEY
43// set, CARLOS_DB_READS not 0, CARLOS_HOST_CLAIM not set. The other half,
44// a running loop whose last probe passed, is the agent's. why is "" when
45// the loop may be armed.
46func dbReadsSettings(getenv func(string) string) (dbReadsEnv, string) {
47 switch {
48 case !hibernationConfigured(getenv):
49 return dbReadsEnv{}, "hibernation is not configured (CARLOS_HIBERNATE_BUCKET)"
50 case hostwrite.Enabled(getenv):
51 return dbReadsEnv{}, "CARLOS_HOST_CLAIM is set"
52 case strings.TrimSpace(getenv(dbReadsSwitchEnv)) == "0":
53 return dbReadsEnv{}, dbReadsSwitchEnv + " is 0"
54 }
55 region := strings.TrimSpace(getenv("CARLOS_REGION"))
56 if !dbreads.ValidRegion(region) {
57 return dbReadsEnv{}, "CARLOS_REGION is not a region"
58 }
59 pub, err := release.ParsePublicKey(getenv("CARLOS_RELEASE_PUBKEY"), "CARLOS_RELEASE_PUBKEY")
60 if err != nil {
61 return dbReadsEnv{}, err.Error()
62 }
63 env := dbReadsEnv{region: region, pub: pub, maxRestored: dbreads.DefaultMaxRestored, maxPull: dbreads.MaxPullBytes}
64 if raw := strings.TrimSpace(getenv(dbReadMaxBytesEnv)); raw != "" {
65 n, err := strconv.ParseInt(raw, 10, 64)
66 if err != nil || n <= 0 {
67 return dbReadsEnv{}, dbReadMaxBytesEnv + " is not a whole number of bytes"
68 }
69 env.maxRestored = n
70 }
71 if raw := strings.TrimSpace(getenv(dbPullMaxBytesEnv)); raw != "" {
72 n, err := strconv.ParseInt(raw, 10, 64)
73 if err != nil || n <= 0 {
74 return dbReadsEnv{}, dbPullMaxBytesEnv + " is not a whole number of bytes"
75 }
76 // Lowering only: no box ships more than a CLI accepts (Review
77 // Focus 5). NewAgent clamps again.
78 env.maxPull = min(n, dbreads.MaxPullBytes)
79 }
80 return env, ""
81}
82
83// armedDBReads is the armed loop; nil on a box that cannot serve.
84var armedDBReads atomic.Pointer[dbreads.Agent]
85
86// dbReadsNudge is the serial pass's word to the loop: list the queue now.
87var dbReadsNudge = make(chan struct{}, 1)
88
89// dbReadsDraining is the draining flag this box last published in its edges
90// file, which the edges loop stores on every pass. The agent reads the same
91// value the console's evidence rule reads, so the two never disagree (spec
92// §7.1). Nothing on main sets an edges file's Draining yet (region HA's
93// placer will); until then it is false.
94var dbReadsDraining atomic.Bool
95
96func nudgeDBReads() {
97 select {
98 case dbReadsNudge <- struct{}{}:
99 default:
100 }
101}
102
103// dbReadsCapable is whether this box publishes db-read-1: the loop is armed
104// and its last readiness probe passed (spec §7.1).
105func dbReadsCapable() bool {
106 a := armedDBReads.Load()
107 return a != nil && a.Capable()
108}
109
110// withDBReadCapability is caps with db-read-1 when capable; caps is not
111// modified.
112func withDBReadCapability(caps []string, capable bool) []string {
113 if !capable {
114 return caps
115 }
116 return append(slices.Clone(caps), dbreads.Capability)
117}
118
119// countDBReadUnits reads `systemctl list-units --plain --no-legend` output:
120// carlos-dbread@ units that are active, activating or deactivating. A
121// stopped unit's scratch is gone; any other state may still hold it.
122func countDBReadUnits(out string) int {
123 n := 0
124 for _, line := range strings.Split(out, "\n") {
125 f := strings.Fields(line)
126 if len(f) < 3 || !strings.HasPrefix(f[0], "carlos-dbread@") {
127 continue
128 }
129 switch f[2] {
130 case "active", "activating", "deactivating", "reloading", "refreshing":
131 n++
132 }
133 }
134 return n
135}
136
137// armDBReads arms the db-reads loop on a box that can serve (spec §7.1).
138// Thin, like armRunsPass: the behaviour is dbreads.Agent's, tested there.
139func armDBReads(hib activator.Config) {
140 env, why := dbReadsSettings(os.Getenv)
141 if why != "" {
142 log.Printf("agent: db reads not armed: %s", why)
143 return
144 }
145 ctx := context.Background()
146 store, err := openDeployStore(ctx)
147 if err != nil {
148 log.Printf("agent: db reads not armed: %v", err)
149 return
150 }
151 a := dbreads.NewAgent(dbreads.AgentConfig{
152 Store: store, Region: env.region, Box: BoxLabel(), Boot: newBootID(), Pub: env.pub,
153 HibernatePrefix: hib.Prefix,
154 OpenReplica: func(ctx context.Context, prefix string) (dbdoor.ReplicaReader, error) {
155 return lease.ReadReplicaClient(ctx, hib.Bucket, prefix, hib.Region)
156 },
157 DialWorker: dialDBReadWorker, Units: dbReadUnits, Free: varTmpFree,
158 MaxRestored: env.maxRestored, MaxPull: env.maxPull, Logf: log.Printf, Draining: dbReadsDraining.Load,
159 })
160 armedDBReads.Store(a)
161 go a.Run(ctx, dbReadsNudge)
162 log.Printf("agent: db reads armed (region %s)", env.region)
163}
164
165// dbReadsHostEnvKeys are the host.env settings dbReadsSettings reads.
166var dbReadsHostEnvKeys = []string{"CARLOS_HIBERNATE_BUCKET", "CARLOS_REGION", "CARLOS_RELEASE_PUBKEY", dbReadsSwitchEnv, "CARLOS_HOST_CLAIM", dbReadMaxBytesEnv, dbPullMaxBytesEnv}
167
168// dbReadsSocketWanted is root's half of the dark gate (Astra round 1,
169// finding 1): the adopt pass arms carlos-dbread.socket only when the box's
170// installed host.env, the file the agent's own environment comes from,
171// arms db reads by the same rule the capability follows (dbReadsSettings).
172// A host.env it cannot read or parse keeps the socket dark. why is the
173// reason when it is not wanted.
174func dbReadsSocketWanted(hostEnv string) (bool, string) {
175 raw, err := readClaimUnitFile(hostEnv)
176 if err != nil {
177 return false, "host.env: " + err.Error()
178 }
179 values := map[string]string{}
180 for _, k := range dbReadsHostEnvKeys {
181 values[k] = ""
182 }
183 scan := bufio.NewScanner(bytes.NewReader(raw))
184 for scan.Scan() {
185 line := strings.TrimSpace(scan.Text())
186 if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") {
187 continue
188 }
189 key, v, ok := strings.Cut(line, "=")
190 key, v = strings.TrimSpace(key), strings.TrimSpace(v)
191 if _, known := values[key]; !ok || !known {
192 continue
193 }
194 if len(v) >= 2 && ((v[0] == '"' && v[len(v)-1] == '"') || (v[0] == '\'' && v[len(v)-1] == '\'')) {
195 v = v[1 : len(v)-1]
196 }
197 if strings.ContainsAny(v, "\\\"'\x00\r\n") {
198 return false, "host.env: unsupported value for " + key
199 }
200 values[key] = v
201 }
202 if err := scan.Err(); err != nil {
203 return false, "host.env: " + err.Error()
204 }
205 if _, why := dbReadsSettings(func(k string) string { return values[k] }); why != "" {
206 return false, why
207 }
208 return true, ""
209}
210