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 "bytes"
5 "errors"
6 "os"
7 "path/filepath"
8 "reflect"
9 "strings"
10 "testing"
11)
12
13func TestClaimStagingBootstrapIgnoresUnreadableLiveEnvironment(t *testing.T) {
14 t.Setenv("CARLOS_HOST_CLAIM", "")
15 fallback := filepath.Join(t.TempDir(), "host.env")
16 if err := os.WriteFile(fallback, []byte("CARLOS_HOST_CLAIM=1\n"), 0000); err != nil {
17 t.Fatal(err)
18 }
19 original, read := claimBootstrapHostEnv, readClaimUnitFile
20 claimBootstrapHostEnv = fallback
21 reads := 0
22 readClaimUnitFile = func(path string) ([]byte, error) {
23 if path == fallback {
24 reads++
25 return nil, &os.PathError{Op: "open", Path: path, Err: os.ErrPermission}
26 }
27 return read(path)
28 }
29 t.Cleanup(func() { claimBootstrapHostEnv, readClaimUnitFile = original, read })
30 root := t.TempDir()
31 if err := bootstrap(root, nil); err != nil {
32 t.Fatalf("flag-off staging read live environment: %v", err)
33 }
34 if reads != 0 {
35 t.Fatalf("staging read live environment %d times", reads)
36 }
37 got, err := os.ReadFile(filepath.Join(root, "etc/systemd/system", claimControlUnit))
38 if err != nil {
39 t.Fatal(err)
40 }
41 want, err := unitsFS.ReadFile("units/" + claimControlUnit)
42 if err != nil {
43 t.Fatal(err)
44 }
45 if !bytes.Equal(got, want) {
46 t.Fatal("flag-off staging changed the unit")
47 }
48}
49
50func TestClaimControlUnitInstallers(t *testing.T) {
51 legacy, err := unitsFS.ReadFile("units/" + claimControlUnit)
52 if err != nil {
53 t.Fatal(err)
54 }
55 for _, flag := range []string{"", " ", "1", "0"} {
56 t.Run("flag="+flag, func(t *testing.T) {
57 t.Setenv("CARLOS_HOST_CLAIM", flag)
58 enabled := strings.TrimSpace(flag) != ""
59 check := func(path string) {
60 t.Helper()
61 got, err := os.ReadFile(path)
62 if err != nil {
63 t.Fatal(err)
64 }
65 if !enabled {
66 if !bytes.Equal(got, legacy) {
67 t.Fatal("flag-off unit differs from main")
68 }
69 return
70 }
71 props := map[string][]string{}
72 for _, line := range strings.Split(string(got), "\n") {
73 k, v, ok := strings.Cut(line, "=")
74 if ok {
75 props[k] = append(props[k], v)
76 }
77 }
78 for key, want := range map[string][]string{
79 "EnvironmentFile": {"-/etc/carlos/host.env"},
80 "StateDirectory": {"carlos-claims"},
81 "StateDirectoryMode": {"0700"},
82 "ReadWritePaths": {"/run/carlos-control /run/carlos /var/lib/carlos-claims /var/lib/carlos-release/private /run/systemd/system /data /opt/carlos /etc/systemd/system /sys/fs/cgroup"},
83 "RestrictAddressFamilies": {"AF_UNIX AF_INET AF_INET6"},
84 "ProtectSystem": {"strict"},
85 "ProtectHome": {"yes"},
86 } {
87 if !reflect.DeepEqual(props[key], want) {
88 t.Errorf("%s = %q, want %q", key, props[key], want)
89 }
90 }
91 }
92 writeEnv := func(etc string) {
93 t.Helper()
94 if err := os.MkdirAll(filepath.Join(etc, "carlos"), 0755); err != nil {
95 t.Fatal(err)
96 }
97 if err := os.WriteFile(filepath.Join(etc, "carlos", "host.env"), []byte("CARLOS_HOST_CLAIM=\""+flag+"\"\n"), 0600); err != nil {
98 t.Fatal(err)
99 }
100 }
101 stage := t.TempDir()
102 writeEnv(filepath.Join(stage, "etc"))
103 if enabled {
104 t.Setenv("CARLOS_HOST_CLAIM", "")
105 } else {
106 t.Setenv("CARLOS_HOST_CLAIM", "1")
107 }
108 if err := bootstrap(stage, nil); err != nil {
109 t.Fatal(err)
110 }
111 check(filepath.Join(stage, "etc/systemd/system", claimControlUnit))
112 f := newApplyFixture(t, "stable", "v1.0.0")
113 writeEnv(f.a.etcDir)
114 if err := os.Remove(filepath.Join(stage, "etc/carlos/host.env")); err != nil {
115 t.Fatal(err)
116 }
117 if err := bootstrap(stage, nil); err != nil {
118 t.Fatal(err)
119 }
120 if err := f.a.installUnits(t.Context(), stage, nil); err != nil {
121 t.Fatal(err)
122 }
123 check(filepath.Join(f.a.etcDir, "systemd/system", claimControlUnit))
124 if enabled && !strings.Contains(strings.Join(f.calls, "\n"), "systemctl try-restart "+claimControlUnit) {
125 t.Fatal("apply did not restart the control service")
126 }
127
128 // Repair a running, armed box whose root unit still has the other mode.
129 etc := t.TempDir()
130 writeEnv(etc)
131 units := filepath.Join(etc, "systemd/system")
132 if err := os.MkdirAll(units, 0755); err != nil {
133 t.Fatal(err)
134 }
135 for _, name := range systemUnitFiles {
136 b, e := unitsFS.ReadFile("units/" + name)
137 if e != nil {
138 t.Fatal(e)
139 }
140 if name == claimControlUnit && !enabled {
141 b = append(append([]byte{}, b...), []byte("\n[Service]\nEnvironmentFile=-/etc/carlos/host.env\n")...)
142 }
143 if e = os.WriteFile(filepath.Join(units, name), b, 0644); e != nil {
144 t.Fatal(e)
145 }
146 }
147 var calls []string
148 ctl := func(args ...string) (string, error) { calls = append(calls, strings.Join(args, " ")); return "", nil }
149 probe := func(args ...string) (string, error) {
150 // host.env here does not arm db reads, so the db-read socket
151 // is dark (dbReadsSocketWanted), as ensureSystemUnits keeps it.
152 if args[1] == dbReadSocketUnit {
153 if args[0] == "is-active" {
154 return "inactive\n", systemctlExit(3)
155 }
156 return "disabled\n", systemctlExit(1)
157 }
158 if args[0] == "is-active" {
159 return "active\n", nil
160 }
161 return "enabled\n", nil
162 }
163 lock := filepath.Join(t.TempDir(), "lock")
164 ensureSystemUnits(etc, lock, ctl, probe, t.Logf)
165 check(filepath.Join(units, claimControlUnit))
166 if !strings.Contains(strings.Join(calls, "\n"), "try-restart "+claimControlUnit) {
167 t.Fatal("stale armed unit was not repaired")
168 }
169 calls = nil
170 ensureSystemUnits(etc, lock, ctl, probe, t.Logf)
171 if len(calls) != 0 {
172 t.Fatalf("unchanged unit was reinstalled: %v", calls)
173 }
174 })
175 }
176}
177
178func TestClaimControlUnitSettings(t *testing.T) {
179 t.Setenv("CARLOS_HOST_CLAIM", "1")
180 t.Setenv("CARLOS_DATA_DIR", "/wrong")
181 t.Setenv("CARLOS_OPT_DIR", "/wrong")
182 path := filepath.Join(t.TempDir(), "host.env")
183 missing, err := readClaimUnitSettings(path)
184 if err != nil || missing.enabled || missing.data != "/data" || missing.opt != "/opt/carlos" {
185 t.Fatalf("missing env: %+v %v", missing, err)
186 }
187
188 for _, tc := range []struct {
189 name, body string
190 fail bool
191 enabled bool
192 data, opt string
193 }{
194 {"absent flag", "OTHER=value\n", false, false, "/data", "/opt/carlos"},
195 {"quoted", "CARLOS_HOST_CLAIM='0'\nCARLOS_DATA_DIR=\"/srv/my data\"\nCARLOS_OPT_DIR=/srv/opt%box\n", false, true, "/srv/my data", "/srv/opt%box"},
196 {"last assignment", "CARLOS_HOST_CLAIM=1\nCARLOS_HOST_CLAIM=\" \"\n", false, false, "/data", "/opt/carlos"},
197 {"multiline", "OTHER='line\nCARLOS_HOST_CLAIM=1\n'\n", true, false, "", ""},
198 {"root", "CARLOS_HOST_CLAIM=1\nCARLOS_DATA_DIR=/\n", true, false, "", ""},
199 {"relative", "CARLOS_HOST_CLAIM=1\nCARLOS_OPT_DIR=relative\n", true, false, "", ""},
200 } {
201 t.Run(tc.name, func(t *testing.T) {
202 if err := os.WriteFile(path, []byte(tc.body), 0600); err != nil {
203 t.Fatal(err)
204 }
205 cfg, err := readClaimUnitSettings(path)
206 if (err != nil) != tc.fail {
207 t.Fatalf("settings: %+v %v", cfg, err)
208 }
209 if tc.fail {
210 return
211 }
212 if cfg.enabled != tc.enabled || cfg.data != tc.data || cfg.opt != tc.opt {
213 t.Fatalf("settings: %+v", cfg)
214 }
215 if tc.name == "quoted" {
216 raw, _ := unitsFS.ReadFile("units/" + claimControlUnit)
217 got := string(claimUnitContent(claimControlUnit, raw, cfg))
218 if !strings.Contains(got, `"/srv/my data" "/srv/opt%%box"`) {
219 t.Fatal(got)
220 }
221 }
222 })
223 }
224}
225
226func TestClaimControlRestartFailureRemainsRepairable(t *testing.T) {
227 path := filepath.Join(t.TempDir(), claimControlUnit)
228 before, _ := unitsFS.ReadFile("units/" + claimControlUnit)
229 after := claimUnitContent(claimControlUnit, before, claimUnitSettings{enabled: true, data: "/data", opt: "/opt/carlos"})
230 if err := os.WriteFile(path, after, 0644); err != nil {
231 t.Fatal(err)
232 }
233 sentinel := errors.New("restart failed")
234 calls := 0
235 err := restartClaimControl(path, before, after, func(args ...string) (string, error) {
236 calls++
237 if args[0] == "try-restart" {
238 return "", sentinel
239 }
240 return "", nil
241 })
242 if !errors.Is(err, sentinel) || calls != 2 {
243 t.Fatalf("restart: %v calls=%d", err, calls)
244 }
245 got, err := os.ReadFile(path)
246 if err != nil || !bytes.Equal(got, before) {
247 t.Fatalf("not restored: %v", err)
248 }
249 if !claimControlChanged(got, after) {
250 t.Fatal("next repair would not retry")
251 }
252}
253
254func TestClaimBadUnitSettingsStillRepairSystemUnits(t *testing.T) {
255 etc := t.TempDir()
256 if err := os.MkdirAll(filepath.Join(etc, "carlos"), 0755); err != nil {
257 t.Fatal(err)
258 }
259 if err := os.WriteFile(filepath.Join(etc, "carlos", "host.env"), []byte("OTHER='multiline\n"), 0600); err != nil {
260 t.Fatal(err)
261 }
262 units := filepath.Join(etc, "systemd", "system")
263 if err := os.MkdirAll(units, 0755); err != nil {
264 t.Fatal(err)
265 }
266 old := []byte("old control unit")
267 if err := os.WriteFile(filepath.Join(units, claimControlUnit), old, 0644); err != nil {
268 t.Fatal(err)
269 }
270 var calls []string
271 ctl := func(args ...string) (string, error) { calls = append(calls, strings.Join(args, " ")); return "", nil }
272 probe := func(args ...string) (string, error) { return "inactive\n", nil }
273 ensureSystemUnits(etc, filepath.Join(t.TempDir(), "lock"), ctl, probe, t.Logf)
274 got, err := os.ReadFile(filepath.Join(units, claimControlUnit))
275 want, _ := unitsFS.ReadFile("units/" + claimControlUnit)
276 if err != nil || !bytes.Equal(got, want) {
277 t.Fatal("control unit not restored to main on unknown settings")
278 }
279 for _, name := range systemUnitFiles {
280 if name != claimControlUnit {
281 if _, err := os.Stat(filepath.Join(units, name)); err != nil {
282 t.Fatal(err)
283 }
284 }
285 }
286 if !strings.Contains(strings.Join(calls, "\n"), "daemon-reload") {
287 t.Fatal("system units were not repaired")
288 }
289 if strings.Contains(strings.Join(calls, "\n"), "try-restart") {
290 t.Fatal("unknown settings restarted control service")
291 }
292}
293
294func TestPlan14RootRetryUnitDirectoryScope(t *testing.T) {
295 raw := []byte("[Service]\nReadWritePaths=/run/carlos-control /run/systemd/system\nRestrictAddressFamilies=AF_UNIX\nMemoryMax=64M\n")
296 legacy := claimUnitContent(claimControlUnit, raw, claimUnitSettings{enabled: true, data: "/data", opt: "/opt/carlos"})
297 if bytes.Contains(legacy, []byte(rootRetryProjection)) {
298 t.Fatal("no-census unit received E3 write access")
299 }
300 census := claimUnitContent(claimControlUnit, raw, claimUnitSettings{enabled: true, rootRetry: true, data: "/data", opt: "/opt/carlos"})
301 if !bytes.Contains(census, []byte(" "+rootRetryProjection+"\n")) {
302 t.Fatal("root retry projection is read-only in the controller")
303 }
304 if got := claimUnitContent(claimControlUnit, raw, claimUnitSettings{rootRetry: true}); !bytes.Equal(got, raw) {
305 t.Fatal("disabled claims changed the unit")
306 }
307}
308