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 "crypto/ed25519"
7 "crypto/rand"
8 "encoding/base64"
9 "fmt"
10 "os"
11 "path/filepath"
12 "slices"
13 "strings"
14 "testing"
15
16 "github.com/carlosframework/platform/internal/dbreads"
17)
18
19func TestDBReadsSettingsAreSpec71(t *testing.T) {
20 t.Parallel()
21 pub, _, _ := ed25519.GenerateKey(rand.Reader)
22 good := map[string]string{
23 "CARLOS_HIBERNATE_BUCKET": "data-uk", "CARLOS_REGION": "uk",
24 "CARLOS_RELEASE_PUBKEY": base64.StdEncoding.EncodeToString(pub),
25 }
26 env := func(over map[string]string) func(string) string {
27 return func(k string) string {
28 if v, ok := over[k]; ok {
29 return v
30 }
31 return good[k]
32 }
33 }
34 got, why := dbReadsSettings(env(nil))
35 if why != "" || got.region != "uk" || got.maxRestored != dbreads.DefaultMaxRestored {
36 t.Fatalf("a capable box: %+v %q", got, why)
37 }
38 if got, why := dbReadsSettings(env(map[string]string{"CARLOS_DB_READ_MAX_BYTES": "1073741824"})); why != "" || got.maxRestored != 1<<30 {
39 t.Fatalf("the size cap: %+v %q", got, why)
40 }
41 for name, over := range map[string]map[string]string{
42 "no hibernation": {"CARLOS_HIBERNATE_BUCKET": ""},
43 "no region": {"CARLOS_REGION": ""},
44 "a fleet placement": {"CARLOS_REGION": "fleet/acme"},
45 "no release key": {"CARLOS_RELEASE_PUBKEY": ""},
46 "switched off": {"CARLOS_DB_READS": "0"},
47 "host claims": {"CARLOS_HOST_CLAIM": "1"},
48 "a cap that is not a number": {"CARLOS_DB_READ_MAX_BYTES": "2GB"},
49 } {
50 if _, why := dbReadsSettings(env(over)); why == "" {
51 t.Errorf("%s: armed", name)
52 }
53 }
54}
55
56func TestCountDBReadUnits(t *testing.T) {
57 t.Parallel()
58 out := strings.Join([]string{
59 "carlos-dbread@0-1234-999.service loaded active running carlos db-read worker",
60 "carlos-dbread@1-1235-999.service loaded activating start carlos db-read worker",
61 "carlos-dbread@2-1236-999.service loaded deactivating stop-sigterm carlos db-read worker",
62 "carlos-dbread@3-1237-999.service loaded failed failed carlos db-read worker",
63 "carlos-dbread@4-1238-999.service loaded inactive dead carlos db-read worker",
64 "",
65 }, "\n")
66 if n := countDBReadUnits(out); n != 3 {
67 t.Fatalf("counted %d units, want active, activating and deactivating: 3", n)
68 }
69}
70
71func TestWithDBReadCapability(t *testing.T) {
72 t.Parallel()
73 base := []string{"region-ha-1"}
74 if got := withDBReadCapability(base, false); !slices.Equal(got, base) {
75 t.Fatalf("incapable: %v", got)
76 }
77 if got := withDBReadCapability(base, true); !slices.Equal(got, []string{"region-ha-1", dbreads.Capability}) || len(base) != 1 {
78 t.Fatalf("capable: %v (base %v)", got, base)
79 }
80}
81
82func unitLines(t *testing.T, name string) []string {
83 t.Helper()
84 b, err := unitsFS.ReadFile("units/" + name)
85 if err != nil {
86 t.Fatal(err)
87 }
88 var lines []string
89 sc := bufio.NewScanner(bytes.NewReader(b))
90 for sc.Scan() {
91 if l := strings.TrimSpace(sc.Text()); l != "" && !strings.HasPrefix(l, "#") {
92 lines = append(lines, l)
93 }
94 }
95 return lines
96}
97
98// The sandbox is the unit file (spec §8.3): every line the spec names is
99// there, and nothing loosens it.
100func TestDBReadUnitsCarryTheSandbox(t *testing.T) {
101 t.Parallel()
102 sock := unitLines(t, "carlos-dbread.socket")
103 // The socket lives under /run/carlos, which every tenant unit replaces
104 // with an empty TemporaryFileSystem: tenants run as carlos, the uid the
105 // worker serves, so a path they could see would let them start workers
106 // directly (Astra round 1, finding 1).
107 if !strings.HasPrefix(dbReadSocketPath, "/run/carlos/") {
108 t.Errorf("the worker socket %s is outside the tree tenant units hide", dbReadSocketPath)
109 }
110 for _, tenant := range []string{"carlos-instance@.service", "carlos-instance-cmd@.service"} {
111 if !slices.Contains(unitLines(t, tenant), "TemporaryFileSystem=/run/carlos:ro") {
112 t.Errorf("%s no longer hides /run/carlos", tenant)
113 }
114 }
115 for _, want := range []string{"ListenStream=" + dbReadSocketPath, "SocketUser=carlos", "SocketMode=0600", "Accept=yes", "MaxConnections=3"} {
116 if !slices.Contains(sock, want) {
117 t.Errorf("the socket lacks %q", want)
118 }
119 }
120 svc := unitLines(t, "carlos-dbread@.service")
121 for _, want := range []string{
122 "ExecStart=/opt/carlos/carlos system db-worker",
123 "StandardInput=socket", "StandardOutput=socket", "StandardError=journal",
124 "DynamicUser=yes", "PrivateNetwork=yes", "RestrictAddressFamilies=AF_UNIX",
125 "TemporaryFileSystem=/data:ro /var/lib:ro /opt/carlos:ro /run/carlos:ro",
126 "BindReadOnlyPaths=/opt/carlos/carlos",
127 "InaccessiblePaths=/etc/carlos -/run/carlos-control",
128 "PrivateTmp=yes", "MemoryMax=512M", "CPUQuota=100%", "TasksMax=16",
129 "LimitFSIZE=2147483648", "RuntimeMaxSec=20min",
130 "SystemCallFilter=@system-service", "NoNewPrivileges=yes", "CapabilityBoundingSet=", "ProtectProc=invisible",
131 "ProtectSystem=strict",
132 } {
133 if !slices.Contains(svc, want) {
134 t.Errorf("the service lacks %q", want)
135 }
136 }
137 for _, l := range svc {
138 for _, bad := range []string{"User=", "StandardOutput=file:", "IPAddressAllow=", "ReadWritePaths=", "AmbientCapabilities=CAP"} {
139 if strings.HasPrefix(l, bad) {
140 t.Errorf("the service loosens its sandbox: %q", l)
141 }
142 }
143 }
144}
145
146// dbReadsHostEnv writes etc/carlos/host.env with the settings that arm the
147// db-reads gate (spec §7.1), plus extra lines.
148func dbReadsHostEnv(t *testing.T, etc string, extra ...string) {
149 t.Helper()
150 pub, _, _ := ed25519.GenerateKey(rand.Reader)
151 lines := append([]string{"CARLOS_HIBERNATE_BUCKET=data-uk", "CARLOS_REGION=uk", "CARLOS_RELEASE_PUBKEY=" + base64.StdEncoding.EncodeToString(pub)}, extra...)
152 if err := os.MkdirAll(filepath.Join(etc, "carlos"), 0o755); err != nil {
153 t.Fatal(err)
154 }
155 if err := os.WriteFile(filepath.Join(etc, "carlos", "host.env"), []byte(strings.Join(lines, "\n")+"\n"), 0o600); err != nil {
156 t.Fatal(err)
157 }
158}
159
160func TestEnsureSystemUnitsInstallsTheDBReadUnits(t *testing.T) {
161 t.Parallel()
162 etc := t.TempDir()
163 os.MkdirAll(filepath.Join(etc, "systemd", "system"), 0o755)
164 dbReadsHostEnv(t, etc)
165 var calls []string
166 systemctl := func(args ...string) (string, error) {
167 calls = append(calls, strings.Join(args, " "))
168 return "", nil
169 }
170 probe := func(args ...string) (string, error) {
171 calls = append(calls, strings.Join(args, " "))
172 if args[0] == "is-active" {
173 return "inactive\n", systemctlExit(3)
174 }
175 return "disabled\n", systemctlExit(1)
176 }
177 ensureSystemUnits(etc, filepath.Join(t.TempDir(), "lock"), systemctl, probe, t.Logf)
178 for _, u := range []string{"carlos-dbread.socket", "carlos-dbread@.service"} {
179 got, err := os.ReadFile(filepath.Join(etc, "systemd", "system", u))
180 want, _ := unitsFS.ReadFile("units/" + u)
181 if err != nil || !bytes.Equal(got, want) {
182 t.Errorf("%s not installed as embedded: %v", u, err)
183 }
184 }
185 joined := strings.Join(calls, "\n")
186 for _, want := range []string{"is-active carlos-dbread.socket", "carlos-dbread.socket"} {
187 if !strings.Contains(joined, want) {
188 t.Errorf("calls lack %q:\n%s", want, joined)
189 }
190 }
191 // systemctlPlan starts every armed unit (sockets, paths, timers, mounts)
192 // in the order passed (bootstrap.go armedUnits), and the service
193 // template never.
194 if !strings.Contains(joined, "start carlos-system-update.path carlos-instance-control.socket carlos-dbread.socket") {
195 t.Errorf("the socket was not armed:\n%s", joined)
196 }
197 if strings.Contains(joined, "start carlos-dbread@") {
198 t.Fatal("a worker service was started")
199 }
200}
201
202// The socket follows the capability's dark gate (Astra round 1, finding 1):
203// a box whose host.env does not arm db reads (no hibernation, CARLOS_DB_READS=0,
204// host claims, no release key, or no host.env) has no listening worker
205// socket. The adopt pass never arms it there, and stops and disables one
206// left armed.
207func TestEnsureSystemUnitsKeepsTheDBReadSocketDarkWithoutTheGate(t *testing.T) {
208 t.Parallel()
209 for name, env := range map[string][]string{
210 "no host.env": nil,
211 "CARLOS_DB_READS=0": {"CARLOS_DB_READS=0"},
212 "host claims": {"CARLOS_HOST_CLAIM=1"},
213 "no hibernation": {"CARLOS_HIBERNATE_BUCKET="},
214 } {
215 for _, armed := range []bool{false, true} {
216 t.Run(fmt.Sprintf("%s/armed=%v", name, armed), func(t *testing.T) {
217 t.Parallel()
218 etc := t.TempDir()
219 os.MkdirAll(filepath.Join(etc, "systemd", "system"), 0o755)
220 if env != nil {
221 dbReadsHostEnv(t, etc, env...)
222 }
223 var calls []string
224 systemctl := func(args ...string) (string, error) {
225 calls = append(calls, strings.Join(args, " "))
226 return "", nil
227 }
228 probe := func(args ...string) (string, error) {
229 calls = append(calls, strings.Join(args, " "))
230 on := armed && args[1] == dbReadSocketUnit
231 switch {
232 case args[0] == "is-active" && on:
233 return "active\n", nil
234 case args[0] == "is-active":
235 return "inactive\n", systemctlExit(3)
236 case on:
237 return "enabled\n", nil
238 }
239 return "disabled\n", systemctlExit(1)
240 }
241 ensureSystemUnits(etc, filepath.Join(t.TempDir(), "lock"), systemctl, probe, t.Logf)
242 joined := strings.Join(calls, "\n")
243 for _, c := range calls {
244 if (strings.HasPrefix(c, "start ") || strings.HasPrefix(c, "enable ")) && strings.Contains(c, dbReadSocketUnit) {
245 t.Fatalf("the dark socket was armed: %q\n%s", c, joined)
246 }
247 }
248 if disabled := strings.Contains(joined, "disable --now "+dbReadSocketUnit); disabled != armed {
249 t.Fatalf("armed %v, disabled %v:\n%s", armed, disabled, joined)
250 }
251 })
252 }
253 }
254}
255
256// Review Focus 5: the pull's cap can only be lowered on a box. Mutation:
257// dbReadsSettings keeps a CARLOS_DB_PULL_MAX_BYTES above the cap (red).
258func TestPullCapOnlyLowers(t *testing.T) {
259 t.Parallel()
260 pub, _, _ := ed25519.GenerateKey(rand.Reader)
261 base := map[string]string{"CARLOS_HIBERNATE_BUCKET": "d", "CARLOS_REGION": "uk", "CARLOS_RELEASE_PUBKEY": base64.StdEncoding.EncodeToString(pub)}
262 get := func(v string) func(string) string {
263 return func(k string) string {
264 if k == "CARLOS_DB_PULL_MAX_BYTES" {
265 return v
266 }
267 return base[k]
268 }
269 }
270 if env, why := dbReadsSettings(get("")); why != "" || env.maxPull != dbreads.MaxPullBytes {
271 t.Fatalf("default %d %q", env.maxPull, why)
272 }
273 if env, why := dbReadsSettings(get("1048576")); why != "" || env.maxPull != 1<<20 {
274 t.Fatalf("lowered %d %q", env.maxPull, why)
275 }
276 if env, why := dbReadsSettings(get("99999999999999")); why != "" || env.maxPull != dbreads.MaxPullBytes {
277 t.Fatalf("raised past the built-in cap: %d %q", env.maxPull, why)
278 }
279 for _, bad := range []string{"0", "-5", "2GB"} {
280 if _, why := dbReadsSettings(get(bad)); why == "" {
281 t.Fatalf("CARLOS_DB_PULL_MAX_BYTES=%s armed the loop", bad)
282 }
283 }
284}
285