| new file mode 100644 |
| index 000000000..964586cf6 |
| --- /dev/null |
| +++ b/internal/runs/stage_linux_test.go |
| @@ -0,0 +1,306 @@ |
| +//go:build linux |
| + |
| +package runs |
| + |
| +import ( |
| + "context" |
| + "crypto/sha256" |
| + "encoding/hex" |
| + "errors" |
| + "os" |
| + "path/filepath" |
| + "testing" |
| + |
| + "github.com/carlosframework/platform/internal/instancefs" |
| + "github.com/carlosframework/platform/internal/release" |
| +) |
| + |
| +func stagedRequest(t *testing.T, s release.Store, files map[string]string) Request { |
| + t.Helper() |
| + req := Request{ID: tid, Account: "bab", App: "titogo"} |
| + for name, body := range files { |
| + sum := sha256.Sum256([]byte(body)) |
| + req.Files = append(req.Files, File{Name: name, Size: int64(len(body)), SHA256: hex.EncodeToString(sum[:])}) |
| + if _, err := s.Put(context.Background(), InputKey("bab", "titogo", tid, name), []byte(body), ""); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + return req |
| +} |
| + |
| +func TestStageFilesWritesTheUploads(t *testing.T) { |
| + s := release.NewDirStore(t.TempDir()) |
| + inst := t.TempDir() |
| + req := stagedRequest(t, s, map[string]string{"a.csv": "1,2", "b.json": "{}"}) |
| + dir, err := StageFiles(context.Background(), s, inst, req) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if dir != filepath.Join(inst, DirName, tid) { |
| + t.Fatalf("dir %q", dir) |
| + } |
| + st, err := os.Stat(dir) |
| + if err != nil || st.Mode().Perm() != 0o700 { |
| + t.Fatalf("run dir mode %v, %v", st.Mode(), err) |
| + } |
| + for name, want := range map[string]string{"a.csv": "1,2", "b.json": "{}"} { |
| + raw, err := os.ReadFile(filepath.Join(dir, name)) |
| + fi, _ := os.Stat(filepath.Join(dir, name)) |
| + if err != nil || string(raw) != want || fi.Mode().Perm() != 0o600 { |
| + t.Errorf("%s: %q %v mode %v", name, raw, err, fi.Mode()) |
| + } |
| + } |
| +} |
| + |
| +func TestStageFilesMakesTheDirectoryWithNoFiles(t *testing.T) { |
| + s := release.NewDirStore(t.TempDir()) |
| + inst := t.TempDir() |
| + dir, err := StageFiles(context.Background(), s, inst, Request{ID: tid, Account: "bab", App: "titogo"}) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if st, err := os.Stat(dir); err != nil || !st.IsDir() { |
| + t.Fatalf("CARLOS_RUN_DIR must always exist: %v", err) |
| + } |
| +} |
| + |
| +func TestStageFilesChecksSizeAndHash(t *testing.T) { |
| + s := release.NewDirStore(t.TempDir()) |
| + inst := t.TempDir() |
| + req := stagedRequest(t, s, map[string]string{"a.csv": "1,2"}) |
| + req.Files[0].SHA256 = hex.EncodeToString(make([]byte, 32)) |
| + if _, err := StageFiles(context.Background(), s, inst, req); err == nil { |
| + t.Fatal("a hash mismatch was staged") |
| + } |
| + if _, err := os.Lstat(filepath.Join(inst, DirName, tid, "a.csv")); !os.IsNotExist(err) { |
| + t.Fatalf("a mismatched file was left behind: %v", err) |
| + } |
| + s = release.NewDirStore(t.TempDir()) // the store refuses a second Put of the same input |
| + req = stagedRequest(t, s, map[string]string{"a.csv": "1,2"}) |
| + req.Files[0].Size = 2 |
| + if err := RemoveRunDir(inst, tid); err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := StageFiles(context.Background(), s, inst, req); err == nil { |
| + t.Fatal("a size mismatch was staged") |
| + } |
| +} |
| + |
| +func TestStageFilesRemovesWhatItStagedWhenALaterFileFails(t *testing.T) { |
| + s := release.NewDirStore(t.TempDir()) |
| + inst := t.TempDir() |
| + req := stagedRequest(t, s, map[string]string{"a.csv": "1,2", "b.csv": "3,4"}) |
| + for i := range req.Files { |
| + if req.Files[i].Name == "b.csv" { |
| + req.Files[i].SHA256 = hex.EncodeToString(make([]byte, 32)) |
| + } |
| + } |
| + if _, err := StageFiles(context.Background(), s, inst, req); err == nil { |
| + t.Fatal("a hash mismatch was staged") |
| + } |
| + if _, err := os.Lstat(filepath.Join(inst, DirName, tid)); !os.IsNotExist(err) { |
| + t.Fatalf("a failed staging left its directory behind: %v", err) |
| + } |
| +} |
| + |
| +func TestStageFilesRefusesAPlantedFile(t *testing.T) { |
| + s := release.NewDirStore(t.TempDir()) |
| + inst := t.TempDir() |
| + req := stagedRequest(t, s, map[string]string{"a.csv": "1,2"}) |
| + if err := os.MkdirAll(filepath.Join(inst, DirName, tid), 0o700); err != nil { |
| + t.Fatal(err) |
| + } |
| + outside := filepath.Join(t.TempDir(), "victim") |
| + if err := os.WriteFile(outside, []byte("keep"), 0o600); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Symlink(outside, filepath.Join(inst, DirName, tid, "a.csv")); err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := StageFiles(context.Background(), s, inst, req); err == nil { |
| + t.Fatal("staged over a planted symlink") |
| + } |
| + if raw, _ := os.ReadFile(outside); string(raw) != "keep" { |
| + t.Fatal("the planted symlink's target was written") |
| + } |
| +} |
| + |
| +func TestStageFilesRefusesAPlantedRegularFile(t *testing.T) { |
| + s := release.NewDirStore(t.TempDir()) |
| + inst := t.TempDir() |
| + req := stagedRequest(t, s, map[string]string{"a.csv": "1,2"}) |
| + planted := filepath.Join(inst, DirName, tid, "a.csv") |
| + if err := os.MkdirAll(filepath.Dir(planted), 0o700); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.WriteFile(planted, []byte("tenant"), 0o600); err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := StageFiles(context.Background(), s, inst, req); err == nil { |
| + t.Fatal("staged over a planted file") |
| + } |
| +} |
| + |
| +func TestStageFilesRefusesASymlinkedRunRoot(t *testing.T) { |
| + s := release.NewDirStore(t.TempDir()) |
| + inst, outside := t.TempDir(), t.TempDir() |
| + if err := os.Symlink(outside, filepath.Join(inst, DirName)); err != nil { |
| + t.Fatal(err) |
| + } |
| + _, err := StageFiles(context.Background(), s, inst, Request{ID: tid, Account: "bab", App: "titogo"}) |
| + if !errors.Is(err, instancefs.ErrSymlink) { |
| + t.Fatalf("err %v, want ErrSymlink", err) |
| + } |
| + if ents, _ := os.ReadDir(outside); len(ents) != 0 { |
| + t.Fatalf("the symlink's target was written: %v", ents) |
| + } |
| +} |
| + |
| +func TestStageFilesRefusesASymlinkedRunDirectory(t *testing.T) { |
| + s := release.NewDirStore(t.TempDir()) |
| + inst, outside := t.TempDir(), t.TempDir() |
| + req := stagedRequest(t, s, map[string]string{"a.csv": "1,2"}) |
| + if err := os.Mkdir(filepath.Join(inst, DirName), 0o700); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Symlink(outside, filepath.Join(inst, DirName, tid)); err != nil { |
| + t.Fatal(err) |
| + } |
| + _, err := StageFiles(context.Background(), s, inst, req) |
| + if !errors.Is(err, instancefs.ErrSymlink) { |
| + t.Fatalf("err %v, want ErrSymlink", err) |
| + } |
| + if ents, _ := os.ReadDir(outside); len(ents) != 0 { |
| + t.Fatalf("the symlink's target was written: %v", ents) |
| + } |
| +} |
| + |
| +func TestStageFilesRefusesANameThatIsNotPlain(t *testing.T) { |
| + s := release.NewDirStore(t.TempDir()) |
| + req := Request{ID: tid, Account: "bab", App: "titogo", Files: []File{{Name: "../x", Size: 1}}} |
| + if _, err := StageFiles(context.Background(), s, t.TempDir(), req); err == nil { |
| + t.Fatal("a path name was staged (spec §7: checked again by the agent)") |
| + } |
| +} |
| + |
| +func TestStageFilesRefusesAnIDThatIsNotARunID(t *testing.T) { |
| + s := release.NewDirStore(t.TempDir()) |
| + inst := t.TempDir() |
| + if _, err := StageFiles(context.Background(), s, inst, Request{ID: "../x", Account: "bab", App: "titogo"}); err == nil { |
| + t.Fatal("a bad run id was staged") |
| + } |
| + if err := RemoveRunDir(inst, ".."); err == nil { |
| + t.Fatal("a bad run id was removed") |
| + } |
| +} |
| + |
| +func TestRemoveRunDirFollowsNoSymlink(t *testing.T) { |
| + inst, outside := t.TempDir(), t.TempDir() |
| + if err := os.WriteFile(filepath.Join(outside, "keep"), []byte("k"), 0o600); err != nil { |
| + t.Fatal(err) |
| + } |
| + dir := filepath.Join(inst, DirName, tid) |
| + if err := os.MkdirAll(dir, 0o700); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Symlink(outside, filepath.Join(dir, "link")); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := RemoveRunDir(inst, tid); err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := os.Lstat(dir); !os.IsNotExist(err) { |
| + t.Fatalf("run dir survived: %v", err) |
| + } |
| + if _, err := os.Stat(filepath.Join(outside, "keep")); err != nil { |
| + t.Fatalf("RemoveRunDir followed a symlink: %v", err) |
| + } |
| + if err := RemoveRunDir(inst, tid); err != nil { |
| + t.Fatalf("removing a missing run dir: %v", err) |
| + } |
| +} |
| + |
| +func TestSweepRunDirsRemovesEveryRunDirectory(t *testing.T) { |
| + data := t.TempDir() |
| + for _, host := range []string{"a.example.com", "b.example.com"} { |
| + if err := os.MkdirAll(filepath.Join(data, "instances", host, DirName, tid, "sub"), 0o700); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.WriteFile(filepath.Join(data, "instances", host, "app.db"), []byte("db"), 0o600); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + if err := SweepRunDirs(data); err != nil { |
| + t.Fatal(err) |
| + } |
| + for _, host := range []string{"a.example.com", "b.example.com"} { |
| + if _, err := os.Lstat(filepath.Join(data, "instances", host, DirName)); !os.IsNotExist(err) { |
| + t.Errorf("%s: .carlos-run survived", host) |
| + } |
| + if _, err := os.Stat(filepath.Join(data, "instances", host, "app.db")); err != nil { |
| + t.Errorf("%s: the sweep touched the database", host) |
| + } |
| + } |
| +} |
| + |
| +func TestSweepRunDirsWithNoInstancesIsNotAnError(t *testing.T) { |
| + if err := SweepRunDirs(t.TempDir()); err != nil { |
| + t.Fatal(err) |
| + } |
| +} |
| + |
| +func TestSweepRunDirsFollowsNoSymlink(t *testing.T) { |
| + data, outside := t.TempDir(), t.TempDir() |
| + if err := os.WriteFile(filepath.Join(outside, "keep"), []byte("k"), 0o600); err != nil { |
| + t.Fatal(err) |
| + } |
| + // One instance whose .carlos-run is a link; one instance that is itself a link. |
| + if err := os.MkdirAll(filepath.Join(data, "instances", "a.example.com"), 0o700); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Symlink(outside, filepath.Join(data, "instances", "a.example.com", DirName)); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Mkdir(filepath.Join(outside, DirName), 0o700); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Symlink(outside, filepath.Join(data, "instances", "b.example.com")); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := SweepRunDirs(data); err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := os.Lstat(filepath.Join(data, "instances", "a.example.com", DirName)); !os.IsNotExist(err) { |
| + t.Errorf("the planted link survived: %v", err) |
| + } |
| + if _, err := os.Stat(filepath.Join(outside, "keep")); err != nil { |
| + t.Errorf("the sweep followed a link: %v", err) |
| + } |
| + if _, err := os.Stat(filepath.Join(outside, DirName)); err != nil { |
| + t.Errorf("the sweep followed a linked instance: %v", err) |
| + } |
| +} |
| + |
| +func TestSweepRunDirsMovesPastAnInstanceItCannotClean(t *testing.T) { |
| + if os.Geteuid() == 0 { |
| + t.Skip("root reads a mode 000 directory") |
| + } |
| + data := t.TempDir() |
| + stuck := filepath.Join(data, "instances", "a.example.com", DirName, tid) |
| + if err := os.MkdirAll(filepath.Join(stuck, "sub"), 0o700); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := os.Chmod(stuck, 0); err != nil { |
| + t.Fatal(err) |
| + } |
| + t.Cleanup(func() { _ = os.Chmod(stuck, 0o700) }) |
| + if err := os.MkdirAll(filepath.Join(data, "instances", "b.example.com", DirName, tid), 0o700); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := SweepRunDirs(data); err == nil { |
| + t.Fatal("an uncleanable instance went unreported") |
| + } |
| + if _, err := os.Lstat(filepath.Join(data, "instances", "b.example.com", DirName)); !os.IsNotExist(err) { |
| + t.Fatalf("the sweep stopped at the first failure: %v", err) |
| + } |
| +} |