carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1//go:build linux
2
3package runs
4
5import (
6 "context"
7 "crypto/sha256"
8 "encoding/hex"
9 "errors"
10 "io"
11 "os"
12 "path/filepath"
13 "sort"
14 "syscall"
15 "testing"
16 "time"
17
18 "github.com/carlosframework/platform/internal/instancefs"
19 "github.com/carlosframework/platform/internal/release"
20)
21
22func stagedRequest(t *testing.T, s release.Store, files map[string]string) Request {
23 t.Helper()
24 req := Request{ID: tid, Account: "bab", App: "shop"}
25 for name, body := range files {
26 sum := sha256.Sum256([]byte(body))
27 req.Files = append(req.Files, File{Name: name, Size: int64(len(body)), SHA256: hex.EncodeToString(sum[:])})
28 if _, err := s.Put(context.Background(), InputKey("bab", "shop", tid, name), []byte(body), ""); err != nil {
29 t.Fatal(err)
30 }
31 }
32 return req
33}
34
35func TestStageFilesWritesTheUploads(t *testing.T) {
36 s := release.NewDirStore(t.TempDir())
37 inst := t.TempDir()
38 req := stagedRequest(t, s, map[string]string{"a.csv": "1,2", "b.json": "{}"})
39 dir, err := StageFiles(context.Background(), s, inst, req)
40 if err != nil {
41 t.Fatal(err)
42 }
43 if dir != filepath.Join(inst, DirName, tid) {
44 t.Fatalf("dir %q", dir)
45 }
46 st, err := os.Stat(dir)
47 if err != nil || st.Mode().Perm() != 0o700 {
48 t.Fatalf("run dir mode %v, %v", st.Mode(), err)
49 }
50 for name, want := range map[string]string{"a.csv": "1,2", "b.json": "{}"} {
51 raw, err := os.ReadFile(filepath.Join(dir, name))
52 fi, _ := os.Stat(filepath.Join(dir, name))
53 if err != nil || string(raw) != want || fi.Mode().Perm() != 0o600 {
54 t.Errorf("%s: %q %v mode %v", name, raw, err, fi.Mode())
55 }
56 }
57}
58
59func TestStageFilesMakesTheDirectoryWithNoFiles(t *testing.T) {
60 s := release.NewDirStore(t.TempDir())
61 inst := t.TempDir()
62 dir, err := StageFiles(context.Background(), s, inst, Request{ID: tid, Account: "bab", App: "shop"})
63 if err != nil {
64 t.Fatal(err)
65 }
66 if st, err := os.Stat(dir); err != nil || !st.IsDir() {
67 t.Fatalf("CARLOS_RUN_DIR must always exist: %v", err)
68 }
69}
70
71func TestStageFilesChecksSizeAndHash(t *testing.T) {
72 s := release.NewDirStore(t.TempDir())
73 inst := t.TempDir()
74 req := stagedRequest(t, s, map[string]string{"a.csv": "1,2"})
75 req.Files[0].SHA256 = hex.EncodeToString(make([]byte, 32))
76 if _, err := StageFiles(context.Background(), s, inst, req); err == nil {
77 t.Fatal("a hash mismatch was staged")
78 }
79 if _, err := os.Lstat(filepath.Join(inst, DirName, tid, "a.csv")); !os.IsNotExist(err) {
80 t.Fatalf("a mismatched file was left behind: %v", err)
81 }
82 s = release.NewDirStore(t.TempDir()) // the store refuses a second Put of the same input
83 req = stagedRequest(t, s, map[string]string{"a.csv": "1,2"})
84 req.Files[0].Size = 2
85 if err := RemoveRunDir(inst, tid); err != nil {
86 t.Fatal(err)
87 }
88 if _, err := StageFiles(context.Background(), s, inst, req); err == nil {
89 t.Fatal("a size mismatch was staged")
90 }
91}
92
93func TestStageFilesRemovesWhatItStagedWhenALaterFileFails(t *testing.T) {
94 s := release.NewDirStore(t.TempDir())
95 inst := t.TempDir()
96 req := stagedRequest(t, s, map[string]string{"a.csv": "1,2", "b.csv": "3,4"})
97 sort.Slice(req.Files, func(i, j int) bool { return req.Files[i].Name < req.Files[j].Name })
98 req.Files[1].SHA256 = hex.EncodeToString(make([]byte, 32)) // b.csv fails after a.csv is staged
99 if _, err := StageFiles(context.Background(), s, inst, req); err == nil {
100 t.Fatal("a hash mismatch was staged")
101 }
102 if _, err := os.Lstat(filepath.Join(inst, DirName, tid)); !os.IsNotExist(err) {
103 t.Fatalf("a failed staging left its directory behind: %v", err)
104 }
105}
106
107func TestStageFilesRefusesAPlantedFile(t *testing.T) {
108 s := release.NewDirStore(t.TempDir())
109 inst := t.TempDir()
110 req := stagedRequest(t, s, map[string]string{"a.csv": "1,2"})
111 if err := os.MkdirAll(filepath.Join(inst, DirName, tid), 0o700); err != nil {
112 t.Fatal(err)
113 }
114 outside := filepath.Join(t.TempDir(), "victim")
115 if err := os.WriteFile(outside, []byte("keep"), 0o600); err != nil {
116 t.Fatal(err)
117 }
118 if err := os.Symlink(outside, filepath.Join(inst, DirName, tid, "a.csv")); err != nil {
119 t.Fatal(err)
120 }
121 if _, err := StageFiles(context.Background(), s, inst, req); err == nil {
122 t.Fatal("staged over a planted symlink")
123 }
124 if raw, _ := os.ReadFile(outside); string(raw) != "keep" {
125 t.Fatal("the planted symlink's target was written")
126 }
127}
128
129func TestStageFilesRefusesAPlantedRegularFile(t *testing.T) {
130 s := release.NewDirStore(t.TempDir())
131 inst := t.TempDir()
132 req := stagedRequest(t, s, map[string]string{"a.csv": "1,2"})
133 planted := filepath.Join(inst, DirName, tid, "a.csv")
134 if err := os.MkdirAll(filepath.Dir(planted), 0o700); err != nil {
135 t.Fatal(err)
136 }
137 if err := os.WriteFile(planted, []byte("tenant"), 0o600); err != nil {
138 t.Fatal(err)
139 }
140 if _, err := StageFiles(context.Background(), s, inst, req); err == nil {
141 t.Fatal("staged over a planted file")
142 }
143}
144
145func TestStageFilesRefusesASymlinkedRunRoot(t *testing.T) {
146 s := release.NewDirStore(t.TempDir())
147 inst, outside := t.TempDir(), t.TempDir()
148 if err := os.Symlink(outside, filepath.Join(inst, DirName)); err != nil {
149 t.Fatal(err)
150 }
151 _, err := StageFiles(context.Background(), s, inst, Request{ID: tid, Account: "bab", App: "shop"})
152 if !errors.Is(err, instancefs.ErrSymlink) {
153 t.Fatalf("err %v, want ErrSymlink", err)
154 }
155 if ents, _ := os.ReadDir(outside); len(ents) != 0 {
156 t.Fatalf("the symlink's target was written: %v", ents)
157 }
158}
159
160func TestStageFilesRefusesASymlinkedRunDirectory(t *testing.T) {
161 s := release.NewDirStore(t.TempDir())
162 inst, outside := t.TempDir(), t.TempDir()
163 req := stagedRequest(t, s, map[string]string{"a.csv": "1,2"})
164 if err := os.Mkdir(filepath.Join(inst, DirName), 0o700); err != nil {
165 t.Fatal(err)
166 }
167 if err := os.Symlink(outside, filepath.Join(inst, DirName, tid)); err != nil {
168 t.Fatal(err)
169 }
170 _, err := StageFiles(context.Background(), s, inst, req)
171 if !errors.Is(err, instancefs.ErrSymlink) {
172 t.Fatalf("err %v, want ErrSymlink", err)
173 }
174 if ents, _ := os.ReadDir(outside); len(ents) != 0 {
175 t.Fatalf("the symlink's target was written: %v", ents)
176 }
177}
178
179func TestStageFilesRefusesANameThatIsNotPlain(t *testing.T) {
180 s := release.NewDirStore(t.TempDir())
181 req := Request{ID: tid, Account: "bab", App: "shop", Files: []File{{Name: "../x", Size: 1}}}
182 if _, err := StageFiles(context.Background(), s, t.TempDir(), req); err == nil {
183 t.Fatal("a path name was staged (spec §7: checked again by the agent)")
184 }
185}
186
187func TestStageFilesRefusesAnIDThatIsNotARunID(t *testing.T) {
188 s := release.NewDirStore(t.TempDir())
189 inst := t.TempDir()
190 if _, err := StageFiles(context.Background(), s, inst, Request{ID: "../x", Account: "bab", App: "shop"}); err == nil {
191 t.Fatal("a bad run id was staged")
192 }
193 if err := RemoveRunDir(inst, ".."); err == nil {
194 t.Fatal("a bad run id was removed")
195 }
196}
197
198func TestRemoveRunDirFollowsNoSymlink(t *testing.T) {
199 inst, outside := t.TempDir(), t.TempDir()
200 if err := os.WriteFile(filepath.Join(outside, "keep"), []byte("k"), 0o600); err != nil {
201 t.Fatal(err)
202 }
203 dir := filepath.Join(inst, DirName, tid)
204 if err := os.MkdirAll(dir, 0o700); err != nil {
205 t.Fatal(err)
206 }
207 if err := os.Symlink(outside, filepath.Join(dir, "link")); err != nil {
208 t.Fatal(err)
209 }
210 if err := RemoveRunDir(inst, tid); err != nil {
211 t.Fatal(err)
212 }
213 if _, err := os.Lstat(dir); !os.IsNotExist(err) {
214 t.Fatalf("run dir survived: %v", err)
215 }
216 if _, err := os.Stat(filepath.Join(outside, "keep")); err != nil {
217 t.Fatalf("RemoveRunDir followed a symlink: %v", err)
218 }
219 if err := RemoveRunDir(inst, tid); err != nil {
220 t.Fatalf("removing a missing run dir: %v", err)
221 }
222}
223
224func TestSweepRunDirsRemovesEveryRunDirectory(t *testing.T) {
225 data := t.TempDir()
226 for _, host := range []string{"a.example.com", "b.example.com"} {
227 if err := os.MkdirAll(filepath.Join(data, "instances", host, DirName, tid, "sub"), 0o700); err != nil {
228 t.Fatal(err)
229 }
230 if err := os.WriteFile(filepath.Join(data, "instances", host, "app.db"), []byte("db"), 0o600); err != nil {
231 t.Fatal(err)
232 }
233 }
234 if err := SweepRunDirs(data); err != nil {
235 t.Fatal(err)
236 }
237 for _, host := range []string{"a.example.com", "b.example.com"} {
238 if _, err := os.Lstat(filepath.Join(data, "instances", host, DirName)); !os.IsNotExist(err) {
239 t.Errorf("%s: .carlos-run survived", host)
240 }
241 if _, err := os.Stat(filepath.Join(data, "instances", host, "app.db")); err != nil {
242 t.Errorf("%s: the sweep touched the database", host)
243 }
244 }
245}
246
247func TestSweepRunDirsWithNoInstancesIsNotAnError(t *testing.T) {
248 if err := SweepRunDirs(t.TempDir()); err != nil {
249 t.Fatal(err)
250 }
251}
252
253func TestSweepRunDirsFollowsNoSymlink(t *testing.T) {
254 data, outside := t.TempDir(), t.TempDir()
255 if err := os.WriteFile(filepath.Join(outside, "keep"), []byte("k"), 0o600); err != nil {
256 t.Fatal(err)
257 }
258 // One instance whose .carlos-run is a link; one instance that is itself a link.
259 if err := os.MkdirAll(filepath.Join(data, "instances", "a.example.com"), 0o700); err != nil {
260 t.Fatal(err)
261 }
262 if err := os.Symlink(outside, filepath.Join(data, "instances", "a.example.com", DirName)); err != nil {
263 t.Fatal(err)
264 }
265 if err := os.Mkdir(filepath.Join(outside, DirName), 0o700); err != nil {
266 t.Fatal(err)
267 }
268 if err := os.Symlink(outside, filepath.Join(data, "instances", "b.example.com")); err != nil {
269 t.Fatal(err)
270 }
271 if err := SweepRunDirs(data); err != nil {
272 t.Fatal(err)
273 }
274 if _, err := os.Lstat(filepath.Join(data, "instances", "a.example.com", DirName)); !os.IsNotExist(err) {
275 t.Errorf("the planted link survived: %v", err)
276 }
277 if _, err := os.Stat(filepath.Join(outside, "keep")); err != nil {
278 t.Errorf("the sweep followed a link: %v", err)
279 }
280 if _, err := os.Stat(filepath.Join(outside, DirName)); err != nil {
281 t.Errorf("the sweep followed a linked instance: %v", err)
282 }
283}
284
285func TestSweepRunDirsMovesPastAnInstanceItCannotClean(t *testing.T) {
286 if os.Geteuid() == 0 {
287 t.Skip("root reads a mode 000 directory")
288 }
289 data := t.TempDir()
290 stuck := filepath.Join(data, "instances", "a.example.com", DirName, tid)
291 if err := os.MkdirAll(filepath.Join(stuck, "sub"), 0o700); err != nil {
292 t.Fatal(err)
293 }
294 if err := os.Chmod(stuck, 0); err != nil {
295 t.Fatal(err)
296 }
297 t.Cleanup(func() { _ = os.Chmod(stuck, 0o700) })
298 if err := os.MkdirAll(filepath.Join(data, "instances", "b.example.com", DirName, tid), 0o700); err != nil {
299 t.Fatal(err)
300 }
301 if err := SweepRunDirs(data); err == nil {
302 t.Fatal("an uncleanable instance went unreported")
303 }
304 if _, err := os.Lstat(filepath.Join(data, "instances", "b.example.com", DirName)); !os.IsNotExist(err) {
305 t.Fatalf("the sweep stopped at the first failure: %v", err)
306 }
307}
308
309// countingStore streams every object through a reader that counts the bytes
310// handed out, so a test can tell a bounded read from a whole-object one.
311type countingStore struct {
312 release.Store
313 read int64
314}
315
316type countingReader struct {
317 io.ReadCloser
318 n *int64
319}
320
321func (r countingReader) Read(p []byte) (int, error) {
322 n, err := r.ReadCloser.Read(p)
323 *r.n += int64(n)
324 return n, err
325}
326
327func (c *countingStore) Open(ctx context.Context, key string) (io.ReadCloser, string, error) {
328 rc, etag, err := release.OpenObject(ctx, c.Store, key)
329 if err != nil {
330 return nil, "", err
331 }
332 return countingReader{rc, &c.read}, etag, nil
333}
334
335func TestStageFilesBoundsTheReadByTheRecordedSize(t *testing.T) {
336 inner := release.NewDirStore(t.TempDir())
337 big := make([]byte, 1<<20)
338 if _, err := inner.Put(context.Background(), InputKey("bab", "shop", tid, "a.csv"), big, ""); err != nil {
339 t.Fatal(err)
340 }
341 s := &countingStore{Store: inner}
342 sum := sha256.Sum256(big[:3])
343 req := Request{ID: tid, Account: "bab", App: "shop",
344 Files: []File{{Name: "a.csv", Size: 3, SHA256: hex.EncodeToString(sum[:])}}}
345 inst := t.TempDir()
346 if _, err := StageFiles(context.Background(), s, inst, req); err == nil {
347 t.Fatal("an object larger than its recorded size was staged")
348 }
349 if s.read > 4 {
350 t.Fatalf("read %d bytes of an object recorded as 3", s.read)
351 }
352 if _, err := os.Lstat(filepath.Join(inst, DirName, tid)); !os.IsNotExist(err) {
353 t.Fatalf("the refused file's directory was left behind: %v", err)
354 }
355}
356
357func TestStageFilesRefusesAFIFOPromptly(t *testing.T) {
358 cases := map[string]func(inst string) string{
359 "at a file name": func(inst string) string {
360 if err := os.MkdirAll(filepath.Join(inst, DirName, tid), 0o700); err != nil {
361 t.Fatal(err)
362 }
363 return filepath.Join(inst, DirName, tid, "a.csv")
364 },
365 "at the run directory": func(inst string) string {
366 if err := os.MkdirAll(filepath.Join(inst, DirName), 0o700); err != nil {
367 t.Fatal(err)
368 }
369 return filepath.Join(inst, DirName, tid)
370 },
371 "at the run root": func(inst string) string { return filepath.Join(inst, DirName) },
372 }
373 for name, plant := range cases {
374 t.Run(name, func(t *testing.T) {
375 s := release.NewDirStore(t.TempDir())
376 req := stagedRequest(t, s, map[string]string{"a.csv": "1,2"})
377 inst := t.TempDir()
378 if err := syscall.Mkfifo(plant(inst), 0o600); err != nil {
379 t.Fatal(err)
380 }
381 done := make(chan error, 1)
382 go func() { _, err := StageFiles(context.Background(), s, inst, req); done <- err }()
383 select {
384 case err := <-done:
385 if err == nil {
386 t.Fatal("staged through a FIFO")
387 }
388 case <-time.After(5 * time.Second):
389 t.Fatal("staging blocked on a FIFO")
390 }
391 })
392 }
393}
394