승인된 execution preset을 Edge 조정 경계와 Node workspace/tool 실행 경계로 연결해 단일 요청 수명주기와 관측 계약을 일관되게 처리한다.
304 lines
10 KiB
Go
304 lines
10 KiB
Go
package workspace
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"golang.org/x/sys/unix"
|
|
|
|
iop "iop/proto/gen/iop"
|
|
)
|
|
|
|
func openedRuntime(t *testing.T) (*Runtime, string) {
|
|
t.Helper()
|
|
root := t.TempDir()
|
|
rt, err := NewRuntime([]*iop.WorkspaceConfig{testWorkspaceConfig(root)}, "darwin", nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = rt.Close() })
|
|
if _, err := rt.Open(testRequestAuthority("request-1")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return rt, root
|
|
}
|
|
|
|
func TestFileExecutorReadListWriteDelete(t *testing.T) {
|
|
rt, root := openedRuntime(t)
|
|
if err := os.WriteFile(filepath.Join(root, "input.txt"), []byte("hello"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
read := rt.Read("request-1", "input.txt")
|
|
if read.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS || string(read.Content) != "hello" {
|
|
t.Fatalf("read=%+v", read)
|
|
}
|
|
list := rt.List("request-1", ".")
|
|
if list.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS || len(list.Entries) != 1 || list.Entries[0] != "input.txt\tfile" {
|
|
t.Fatalf("list=%+v", list)
|
|
}
|
|
if write := rt.Write("request-1", "nested/output.txt", []byte("written")); write.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
|
|
t.Fatalf("write=%+v", write)
|
|
}
|
|
data, err := os.ReadFile(filepath.Join(root, "nested", "output.txt"))
|
|
if err != nil || string(data) != "written" {
|
|
t.Fatalf("output=%q err=%v", data, err)
|
|
}
|
|
if deleted := rt.Delete("request-1", "nested/output.txt"); deleted.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
|
|
t.Fatalf("delete=%+v", deleted)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(root, "nested", "output.txt")); !os.IsNotExist(err) {
|
|
t.Fatalf("deleted file remains: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestFileExecutorRejectsReservedSymlinkAndBounds(t *testing.T) {
|
|
rt, root := openedRuntime(t)
|
|
outside := filepath.Join(t.TempDir(), "outside.txt")
|
|
if err := os.WriteFile(outside, []byte("outside"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Symlink(outside, filepath.Join(root, "escape")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.MkdirAll(filepath.Join(root, ".iop"), 0700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, target := range []string{".iop", ".iop/job/request-2/x", "escape"} {
|
|
if result := rt.Read("request-1", target); result.Status == iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
|
|
t.Fatalf("read admitted %q: %+v", target, result)
|
|
}
|
|
}
|
|
if err := os.WriteFile(filepath.Join(root, "large"), []byte(strings.Repeat("x", 65)), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if result := rt.Read("request-1", "large"); !result.Truncated || len(result.Content) != 64 {
|
|
t.Fatalf("bounded read=%+v", result)
|
|
}
|
|
if result := rt.Write("request-1", "too-large", []byte(strings.Repeat("x", 65))); result.Status == iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
|
|
t.Fatalf("oversize write=%+v", result)
|
|
}
|
|
request, err := rt.Request("request-1")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
device := request.entry.device
|
|
request.entry.device++
|
|
if result := rt.Read("request-1", "large"); result.Status == iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
|
|
t.Fatalf("cross-filesystem read=%+v", result)
|
|
}
|
|
request.entry.device = device
|
|
}
|
|
|
|
func TestFileExecutorWriteFailurePreservesTarget(t *testing.T) {
|
|
rt, root := openedRuntime(t)
|
|
target := filepath.Join(root, "target.txt")
|
|
if err := os.WriteFile(target, []byte("old"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if result := rt.Write("request-1", "target.txt", []byte(strings.Repeat("x", 65))); result.Status == iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
|
|
t.Fatalf("write=%+v", result)
|
|
}
|
|
data, err := os.ReadFile(target)
|
|
if err != nil || string(data) != "old" {
|
|
t.Fatalf("target=%q err=%v", data, err)
|
|
}
|
|
request, err := rt.Request("request-1")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
request.entry.beforeRename = func() error { return errors.New("injected before rename") }
|
|
if result := rt.Write("request-1", "target.txt", []byte("new")); result.Status == iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
|
|
t.Fatalf("injected write=%+v", result)
|
|
}
|
|
request.entry.beforeRename = nil
|
|
data, err = os.ReadFile(target)
|
|
if err != nil || string(data) != "old" {
|
|
t.Fatalf("target after injected failure=%q err=%v", data, err)
|
|
}
|
|
assertNoWriteTemps(t, root)
|
|
if result := rt.Delete("request-1", "."); result.Status == iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
|
|
t.Fatalf("root delete=%+v", result)
|
|
}
|
|
if err := os.Mkdir(filepath.Join(root, "nonempty"), 0700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(root, "nonempty", "child"), []byte("x"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if result := rt.Delete("request-1", "nonempty"); result.Status == iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
|
|
t.Fatalf("recursive delete=%+v", result)
|
|
}
|
|
}
|
|
|
|
func TestFileExecutorWriteRejectsUnsafeParentsWithoutEffects(t *testing.T) {
|
|
rt, root := openedRuntime(t)
|
|
outside := t.TempDir()
|
|
if err := os.Symlink(outside, filepath.Join(root, "link")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if result := rt.Write("request-1", "link/new/output.txt", []byte("bad")); result.Status == iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
|
|
t.Fatalf("symlink-parent write=%+v", result)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(outside, "new")); !os.IsNotExist(err) {
|
|
t.Fatalf("rejected symlink write created outside parent: %v", err)
|
|
}
|
|
|
|
request, err := rt.Request("request-1")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
originalDevice := request.entry.device
|
|
request.entry.device++
|
|
if result := rt.Write("request-1", "mount-substitute/output.txt", []byte("bad")); result.Status == iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
|
|
t.Fatalf("foreign-device write=%+v", result)
|
|
}
|
|
request.entry.device = originalDevice
|
|
if _, err := os.Stat(filepath.Join(root, "mount-substitute")); !os.IsNotExist(err) {
|
|
t.Fatalf("foreign-device rejection created parent: %v", err)
|
|
}
|
|
|
|
parent := filepath.Join(root, "parent")
|
|
if err := os.Mkdir(parent, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(parent, "target.txt"), []byte("old"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
moved := filepath.Join(root, "parent-moved")
|
|
request.entry.beforeRename = func() error {
|
|
if err := os.Rename(parent, moved); err != nil {
|
|
return err
|
|
}
|
|
return os.Mkdir(parent, 0o700)
|
|
}
|
|
if result := rt.Write("request-1", "parent/target.txt", []byte("new")); result.Status == iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
|
|
t.Fatalf("replaced-parent write=%+v", result)
|
|
}
|
|
request.entry.beforeRename = nil
|
|
data, err := os.ReadFile(filepath.Join(moved, "target.txt"))
|
|
if err != nil || string(data) != "old" {
|
|
t.Fatalf("moved target=%q err=%v", data, err)
|
|
}
|
|
entries, err := os.ReadDir(parent)
|
|
if err != nil || len(entries) != 0 {
|
|
t.Fatalf("replacement parent entries=%v err=%v", entries, err)
|
|
}
|
|
assertNoWriteTemps(t, root)
|
|
}
|
|
|
|
func TestFileExecutorBoundedDeterministicLargeList(t *testing.T) {
|
|
root := t.TempDir()
|
|
config := testWorkspaceConfig(root)
|
|
config.MaxOutputBytes = 256
|
|
rt, err := NewRuntime([]*iop.WorkspaceConfig{config}, "darwin", nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = rt.Close() })
|
|
authority := testRequestAuthority("request-1")
|
|
authority.MaxOutputBytes = 256
|
|
if _, err := rt.Open(authority); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for index := 0; index < maxListEntries+200; index++ {
|
|
name := fmt.Sprintf("entry-%04d-with-bounded-name", index)
|
|
if err := os.WriteFile(filepath.Join(root, name), nil, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
first := rt.List("request-1", ".")
|
|
second := rt.List("request-1", ".")
|
|
if first.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS || !first.Truncated || len(first.Entries) == 0 {
|
|
t.Fatalf("first list=%+v", first)
|
|
}
|
|
if strings.Join(first.Entries, "\n") != strings.Join(second.Entries, "\n") || first.Entries[0] != "entry-0000-with-bounded-name\tfile" {
|
|
t.Fatalf("list is not deterministic: first=%v second=%v", first.Entries, second.Entries)
|
|
}
|
|
}
|
|
|
|
func TestFileExecutorRejectsSpecialFileAndRunsParallelRequests(t *testing.T) {
|
|
rt, root := openedRuntime(t)
|
|
fifo := filepath.Join(root, "special")
|
|
if err := unix.Mkfifo(fifo, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, result := range []Result{
|
|
rt.Read("request-1", "special"),
|
|
rt.Write("request-1", "special", []byte("bad")),
|
|
rt.Delete("request-1", "special"),
|
|
} {
|
|
if result.Status == iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
|
|
t.Fatalf("special file operation succeeded: %+v", result)
|
|
}
|
|
}
|
|
if err := os.Remove(fifo); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
requestIDs := make([]string, 16)
|
|
for index := 0; index < 16; index++ {
|
|
requestID := fmt.Sprintf("request-%d", index+2)
|
|
requestIDs[index] = requestID
|
|
if _, err := rt.Open(testRequestAuthority(requestID)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
var group sync.WaitGroup
|
|
for index, requestID := range requestIDs {
|
|
group.Add(1)
|
|
go func(index int, requestID string) {
|
|
defer group.Done()
|
|
name := fmt.Sprintf("parallel/%02d.txt", index)
|
|
if result := rt.Write(requestID, name, []byte(requestID)); result.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
|
|
t.Errorf("write %s=%+v", requestID, result)
|
|
return
|
|
}
|
|
if result := rt.Read(requestID, name); result.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS || string(result.Content) != requestID {
|
|
t.Errorf("read %s=%+v", requestID, result)
|
|
}
|
|
}(index, requestID)
|
|
}
|
|
group.Wait()
|
|
for _, requestID := range requestIDs {
|
|
group.Add(1)
|
|
go func(requestID string) {
|
|
defer group.Done()
|
|
if result := rt.List(requestID, "parallel"); result.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
|
|
t.Errorf("list %s=%+v", requestID, result)
|
|
}
|
|
}(requestID)
|
|
}
|
|
group.Wait()
|
|
for index, requestID := range requestIDs {
|
|
group.Add(1)
|
|
go func(index int, requestID string) {
|
|
defer group.Done()
|
|
name := fmt.Sprintf("parallel/%02d.txt", index)
|
|
if result := rt.Delete(requestID, name); result.Status != iop.WorkspaceStatus_WORKSPACE_STATUS_SUCCESS {
|
|
t.Errorf("delete %s=%+v", requestID, result)
|
|
}
|
|
}(index, requestID)
|
|
}
|
|
group.Wait()
|
|
}
|
|
|
|
func assertNoWriteTemps(t *testing.T, root string) {
|
|
t.Helper()
|
|
err := filepath.WalkDir(root, func(path string, entry os.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if strings.HasPrefix(entry.Name(), ".iop-write-") {
|
|
t.Fatalf("temporary write artifact remains: %s", path)
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|