refactor(sandbox/v2): transition from pool to node configuration

- Updated benchmark and test functions to utilize node configurations instead of pool configurations for improved clarity and consistency.
- Refactored related setup functions and test cases to align with the new node-based architecture.
- Adjusted error messages and documentation to reflect the transition from pool to node terminology.

Made-with: Cursor
This commit is contained in:
Max 2026-03-09 03:20:09 +08:00
parent ce0a97c0af
commit e633640998
25 changed files with 329 additions and 328 deletions

View file

@ -627,11 +627,11 @@ YAO_REFRESH_TOKEN=<refresh_token>
```go ```go
var ( var (
ErrNotAvailable = errors.New("sandbox: not available (no pools configured)") ErrNotAvailable = errors.New("sandbox: not available (no nodes registered)")
ErrNotFound = errors.New("sandbox: not found") ErrNotFound = errors.New("sandbox: not found")
ErrLimitExceeded = errors.New("sandbox: limit exceeded") ErrLimitExceeded = errors.New("sandbox: limit exceeded")
ErrPoolNotFound = errors.New("sandbox: pool not found") ErrNodeNotFound = errors.New("sandbox: node not found")
ErrPoolInUse = errors.New("sandbox: pool has running boxes") ErrNodeMissing = errors.New("sandbox: node ID missing")
) )
``` ```

View file

@ -22,11 +22,10 @@ Reference: [DESIGN.md](./DESIGN.md)
| File | What | Status | | File | What | Status |
|------|------|--------| |------|------|--------|
| `sandbox.go` | `Init()`, `M()`, global singleton | DONE | | `sandbox.go` | `Init()`, `M()`, global singleton | DONE |
| `manager.go` | Manager: Create/Get/GetOrCreate/List/Remove/Cleanup/Close, Start (container recovery), AddPool/RemovePool/Pools, Heartbeat, SetGRPCPort, SetWorkspaceManager, ImageExists/PullImage/EnsureImage | DONE | | `manager.go` | Manager: Create/Get/GetOrCreate/List/Remove/Cleanup/Close, Start (container recovery), Nodes, Heartbeat, ImageExists/PullImage/EnsureImage | DONE |
| `box.go` | Box: Exec, Stream, Attach, Workspace, VNC, Proxy, Start/Stop/Remove, Info, touch/lastActiveTime/idleTimeout/maxLifetime/stopTimeout | DONE | | `box.go` | Box: Exec, Stream, Attach, Workspace, VNC, Proxy, Start/Stop/Remove, Info, touch/lastActiveTime/idleTimeout/maxLifetime/stopTimeout | DONE |
| `types.go` | LifecyclePolicy (OneShot/Session/LongRunning/Persistent), Pool, PoolInfo, PortMapping, CreateOptions (with WorkspaceID/MountMode/MountPath), ListOptions, ExecOption/ExecResult/ExecStream, AttachOption/ServiceConn, ImagePullOptions/RegistryAuth, BoxInfo, DefaultStopTimeout | DONE | | `types.go` | LifecyclePolicy (OneShot/Session/LongRunning/Persistent), NodeID, PortMapping, CreateOptions (with WorkspaceID/MountMode/MountPath), ListOptions, ExecOption/ExecResult/ExecStream, AttachOption/ServiceConn, ImagePullOptions/RegistryAuth, BoxInfo, DefaultStopTimeout | DONE |
| `config.go` | Config struct | DONE | | `errors.go` | ErrNotAvailable, ErrNotFound, ErrNodeNotFound, ErrNodeMissing | DONE |
| `errors.go` | ErrNotAvailable, ErrNotFound, ErrLimitExceeded, ErrPoolNotFound, ErrPoolInUse | DONE |
| `grpc.go` | BuildGRPCEnv (sandbox ID + gRPC addr only; token injection is caller's responsibility via Env) | DONE | | `grpc.go` | BuildGRPCEnv (sandbox ID + gRPC addr only; token injection is caller's responsibility via Env) | DONE |
### workspace Module — DONE ### workspace Module — DONE
@ -51,7 +50,7 @@ Reference: [DESIGN.md](./DESIGN.md)
| `box_image_test.go` | ImageExists (Docker+K8s), PullImage (progress+K8s no-op), EnsureImage, bad ref | DONE | | `box_image_test.go` | ImageExists (Docker+K8s), PullImage (progress+K8s no-op), EnsureImage, bad ref | DONE |
| `grpc_test.go` | Token creation/revocation, env var building (local vs remote) | DONE | | `grpc_test.go` | Token creation/revocation, env var building (local vs remote) | DONE |
| `bench_test.go` | ContainerLifecycle, Create, Exec, ExecHeavy, Remove, Info, StopStart, WorkspaceReadWrite | DONE | | `bench_test.go` | ContainerLifecycle, Create, Exec, ExecHeavy, Remove, Info, StopStart, WorkspaceReadWrite | DONE |
| `testutils_test.go` | testPools (local/remote/k8s), setupManager, createTestBox, ensureTestImage | DONE | | `testutils_test.go` | testNodes (local/remote/k8s), setupManager, setupManagerForNode, createTestBox, ensureTestImage | DONE |
| `export_test.go` | ResetForTest | DONE | | `export_test.go` | ResetForTest | DONE |
| **workspace** | | | | **workspace** | | |
| `workspace_test.go` | Create (auto/explicit ID, labels, invalid node), Get, List (owner/node filter), Update (name/labels), Delete, Nodes, NodeForWorkspace, AddPool/RemovePool, MountPath | DONE | | `workspace_test.go` | Create (auto/explicit ID, labels, invalid node), Get, List (owner/node filter), Update (name/labels), Delete, Nodes, NodeForWorkspace, AddPool/RemovePool, MountPath | DONE |
@ -88,7 +87,7 @@ Box and Host now share a single `Computer` interface (`types.go`). Both `sandbox
|------|---------|------|--------| |------|---------|------|--------|
| Computer interface | `sandbox/v2/types.go` | `Computer` interface: Exec, Stream, VNC, Proxy, ComputerInfo, BindWorkplace, Workplace | DONE | | Computer interface | `sandbox/v2/types.go` | `Computer` interface: Exec, Stream, VNC, Proxy, ComputerInfo, BindWorkplace, Workplace | DONE |
| Host implementation | `sandbox/v2/host.go` | `Host` struct implements `Computer` via tai HostExec + VNC/Proxy | DONE | | Host implementation | `sandbox/v2/host.go` | `Host` struct implements `Computer` via tai HostExec + VNC/Proxy | DONE |
| ComputerInfo | `sandbox/v2/types.go` | `ComputerInfo` struct with Kind, Pool, TaiID, System, Capabilities, box-specific fields | DONE | | ComputerInfo | `sandbox/v2/types.go` | `ComputerInfo` struct with Kind, NodeID, TaiID, System, Capabilities, box-specific fields | DONE |
### JSAPI — DONE ### JSAPI — DONE
@ -224,7 +223,7 @@ Manager injects these labels at creation time:
managed-by=yao-sandbox managed-by=yao-sandbox
sandbox-id=<id> sandbox-id=<id>
sandbox-owner=<owner> sandbox-owner=<owner>
sandbox-pool=<pool> sandbox-node-id=<nodeID>
sandbox-policy=<policy> sandbox-policy=<policy>
workspace-id=<workspace-id> (if WorkspaceID set) workspace-id=<workspace-id> (if WorkspaceID set)
``` ```
@ -237,14 +236,14 @@ When `CreateOptions.WorkspaceID` is set:
``` ```
1. NodeForWorkspace(wsID) → node name 1. NodeForWorkspace(wsID) → node name
2. Force pool = node name 2. Force nodeID = node name
3. MountPath(wsID) → hostDir 3. MountPath(wsID) → hostDir
4. Bind: hostDir:/workspace:rw 4. Bind: hostDir:/workspace:rw
``` ```
### Multi-Mode Testing ### Multi-Mode Testing
`testPools()` returns all available pool configurations: `testNodes()` returns all available node configurations:
```go ```go
func testPools() []poolConfig { func testPools() []poolConfig {

View file

@ -426,7 +426,7 @@ func TestRemovePool_InUse(t *testing.T) {
// manager_test.go // manager_test.go
package sandbox_test package sandbox_test
func TestMultiPool(t *testing.T) { func TestMultiNode(t *testing.T) {
skipIfNoDocker(t) skipIfNoDocker(t)
skipIfNoTai(t) skipIfNoTai(t)
cleanup := setupManagerWithRemote(t) cleanup := setupManagerWithRemote(t)

View file

@ -13,7 +13,7 @@ import (
// BenchmarkContainerLifecycle measures the full Create → Exec → Remove cycle. // BenchmarkContainerLifecycle measures the full Create → Exec → Remove cycle.
func BenchmarkContainerLifecycle(b *testing.B) { func BenchmarkContainerLifecycle(b *testing.B) {
for _, pc := range testPools() { for _, pc := range testNodes() {
pc := pc pc := pc
b.Run(pc.Name, func(b *testing.B) { b.Run(pc.Name, func(b *testing.B) {
m := setupManagerForBench(b, &pc) m := setupManagerForBench(b, &pc)
@ -44,7 +44,7 @@ func BenchmarkContainerLifecycle(b *testing.B) {
// BenchmarkCreate measures container creation time only. // BenchmarkCreate measures container creation time only.
func BenchmarkCreate(b *testing.B) { func BenchmarkCreate(b *testing.B) {
for _, pc := range testPools() { for _, pc := range testNodes() {
pc := pc pc := pc
b.Run(pc.Name, func(b *testing.B) { b.Run(pc.Name, func(b *testing.B) {
m := setupManagerForBench(b, &pc) m := setupManagerForBench(b, &pc)
@ -73,7 +73,7 @@ func BenchmarkCreate(b *testing.B) {
// BenchmarkExec measures command execution latency on a pre-created container. // BenchmarkExec measures command execution latency on a pre-created container.
func BenchmarkExec(b *testing.B) { func BenchmarkExec(b *testing.B) {
for _, pc := range testPools() { for _, pc := range testNodes() {
pc := pc pc := pc
b.Run(pc.Name, func(b *testing.B) { b.Run(pc.Name, func(b *testing.B) {
m := setupManagerForBench(b, &pc) m := setupManagerForBench(b, &pc)
@ -95,7 +95,7 @@ func BenchmarkExec(b *testing.B) {
// BenchmarkExecHeavy measures execution of a heavier command (write + read file). // BenchmarkExecHeavy measures execution of a heavier command (write + read file).
func BenchmarkExecHeavy(b *testing.B) { func BenchmarkExecHeavy(b *testing.B) {
for _, pc := range testPools() { for _, pc := range testNodes() {
pc := pc pc := pc
b.Run(pc.Name, func(b *testing.B) { b.Run(pc.Name, func(b *testing.B) {
m := setupManagerForBench(b, &pc) m := setupManagerForBench(b, &pc)
@ -118,7 +118,7 @@ func BenchmarkExecHeavy(b *testing.B) {
// BenchmarkRemove measures container removal time. // BenchmarkRemove measures container removal time.
func BenchmarkRemove(b *testing.B) { func BenchmarkRemove(b *testing.B) {
for _, pc := range testPools() { for _, pc := range testNodes() {
pc := pc pc := pc
b.Run(pc.Name, func(b *testing.B) { b.Run(pc.Name, func(b *testing.B) {
m := setupManagerForBench(b, &pc) m := setupManagerForBench(b, &pc)
@ -148,7 +148,7 @@ func BenchmarkRemove(b *testing.B) {
// BenchmarkInfo measures Info() latency on a running container. // BenchmarkInfo measures Info() latency on a running container.
func BenchmarkInfo(b *testing.B) { func BenchmarkInfo(b *testing.B) {
for _, pc := range testPools() { for _, pc := range testNodes() {
pc := pc pc := pc
b.Run(pc.Name, func(b *testing.B) { b.Run(pc.Name, func(b *testing.B) {
m := setupManagerForBench(b, &pc) m := setupManagerForBench(b, &pc)
@ -167,7 +167,7 @@ func BenchmarkInfo(b *testing.B) {
// BenchmarkStopStart measures Stop → Start cycle time. // BenchmarkStopStart measures Stop → Start cycle time.
func BenchmarkStopStart(b *testing.B) { func BenchmarkStopStart(b *testing.B) {
for _, pc := range testPools() { for _, pc := range testNodes() {
pc := pc pc := pc
b.Run(pc.Name, func(b *testing.B) { b.Run(pc.Name, func(b *testing.B) {
if pc.Name == "k8s" { if pc.Name == "k8s" {
@ -191,7 +191,7 @@ func BenchmarkStopStart(b *testing.B) {
// BenchmarkWorkspaceReadWrite measures workspace file read/write via container Box. // BenchmarkWorkspaceReadWrite measures workspace file read/write via container Box.
func BenchmarkWorkspaceReadWrite(b *testing.B) { func BenchmarkWorkspaceReadWrite(b *testing.B) {
for _, pc := range testPools() { for _, pc := range testNodes() {
pc := pc pc := pc
b.Run(pc.Name, func(b *testing.B) { b.Run(pc.Name, func(b *testing.B) {
m := setupManagerForBench(b, &pc) m := setupManagerForBench(b, &pc)
@ -223,7 +223,7 @@ func BenchmarkWorkspaceReadWrite(b *testing.B) {
// --- helpers --- // --- helpers ---
func setupManagerForBench(b *testing.B, pc *poolConfig) *sandbox.Manager { func setupManagerForBench(b *testing.B, pc *nodeConfig) *sandbox.Manager {
b.Helper() b.Helper()
reg := registry.Global() reg := registry.Global()
if reg == nil { if reg == nil {
@ -240,29 +240,29 @@ func setupManagerForBench(b *testing.B, pc *poolConfig) *sandbox.Manager {
return m return m
} }
func ensureTestImageBench(b *testing.B, m *sandbox.Manager, pool string) { func ensureTestImageBench(b *testing.B, m *sandbox.Manager, nodeID string) {
b.Helper() b.Helper()
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
defer cancel() defer cancel()
if err := m.EnsureImage(ctx, pool, testImage(), sandbox.ImagePullOptions{}); err != nil { if err := m.EnsureImage(ctx, nodeID, testImage(), sandbox.ImagePullOptions{}); err != nil {
b.Fatalf("EnsureImage: %v", err) b.Fatalf("EnsureImage: %v", err)
} }
} }
func createBoxForBench(b *testing.B, m *sandbox.Manager) *sandbox.Box { func createBoxForBench(b *testing.B, m *sandbox.Manager) *sandbox.Box {
b.Helper() b.Helper()
pools := m.Pools() nodes := m.Nodes()
var poolName string var nodeID string
if len(pools) > 0 { if len(nodes) > 0 {
poolName = pools[0].TaiID nodeID = nodes[0].TaiID
ensureTestImageBench(b, m, poolName) ensureTestImageBench(b, m, nodeID)
} }
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
defer cancel() defer cancel()
box, err := m.Create(ctx, sandbox.CreateOptions{ box, err := m.Create(ctx, sandbox.CreateOptions{
Image: testImage(), Image: testImage(),
Owner: "bench", Owner: "bench",
Pool: poolName, NodeID: nodeID,
}) })
if err != nil { if err != nil {
b.Fatalf("Create: %v", err) b.Fatalf("Create: %v", err)

View file

@ -15,7 +15,7 @@ import (
type Box struct { type Box struct {
id string id string
containerID string containerID string
pool string nodeID string
owner string owner string
policy LifecyclePolicy policy LifecyclePolicy
labels map[string]string labels map[string]string
@ -39,13 +39,13 @@ var _ Computer = (*Box)(nil)
func (b *Box) ID() string { return b.id } func (b *Box) ID() string { return b.id }
func (b *Box) Owner() string { return b.owner } func (b *Box) Owner() string { return b.owner }
func (b *Box) ContainerID() string { return b.containerID } func (b *Box) ContainerID() string { return b.containerID }
func (b *Box) Pool() string { return b.pool } func (b *Box) NodeID() string { return b.nodeID }
// ComputerInfo returns identity and registry information for this Box. // ComputerInfo returns identity and registry information for this Box.
func (b *Box) ComputerInfo() ComputerInfo { func (b *Box) ComputerInfo() ComputerInfo {
return ComputerInfo{ return ComputerInfo{
Kind: "box", Kind: "box",
Pool: b.pool, NodeID: b.nodeID,
Status: "online", Status: "online",
BoxID: b.id, BoxID: b.id,
ContainerID: b.containerID, ContainerID: b.containerID,
@ -79,7 +79,7 @@ func (b *Box) Exec(ctx context.Context, cmd []string, opts ...ExecOption) (*Exec
o(cfg) o(cfg)
} }
client, err := b.manager.getPool(b.pool) client, err := b.manager.getNode(b.nodeID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -113,7 +113,7 @@ func (b *Box) Stream(ctx context.Context, cmd []string, opts ...ExecOption) (*Ex
o(cfg) o(cfg)
} }
client, err := b.manager.getPool(b.pool) client, err := b.manager.getNode(b.nodeID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -143,7 +143,7 @@ func (b *Box) Attach(ctx context.Context, port int, opts ...AttachOption) (*Serv
o(cfg) o(cfg)
} }
client, err := b.manager.getPool(b.pool) client, err := b.manager.getNode(b.nodeID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -189,7 +189,7 @@ func (b *Box) Workspace() workspace.FS {
if sessionID == "" { if sessionID == "" {
sessionID = b.id sessionID = b.id
} }
client, err := b.manager.getPool(b.pool) client, err := b.manager.getNode(b.nodeID)
if err != nil { if err != nil {
return nil return nil
} }
@ -203,7 +203,7 @@ func (b *Box) WorkspaceID() string { return b.workspaceID }
// VNC returns the VNC WebSocket URL. // VNC returns the VNC WebSocket URL.
func (b *Box) VNC(ctx context.Context) (string, error) { func (b *Box) VNC(ctx context.Context) (string, error) {
b.touch() b.touch()
client, err := b.manager.getPool(b.pool) client, err := b.manager.getNode(b.nodeID)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -213,7 +213,7 @@ func (b *Box) VNC(ctx context.Context) (string, error) {
// Proxy returns the HTTP URL for a service on the given port inside the sandbox. // Proxy returns the HTTP URL for a service on the given port inside the sandbox.
func (b *Box) Proxy(ctx context.Context, port int, path string) (string, error) { func (b *Box) Proxy(ctx context.Context, port int, path string) (string, error) {
b.touch() b.touch()
client, err := b.manager.getPool(b.pool) client, err := b.manager.getNode(b.nodeID)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -222,7 +222,7 @@ func (b *Box) Proxy(ctx context.Context, port int, path string) (string, error)
// Start starts a stopped sandbox. // Start starts a stopped sandbox.
func (b *Box) Start(ctx context.Context) error { func (b *Box) Start(ctx context.Context) error {
client, err := b.manager.getPool(b.pool) client, err := b.manager.getNode(b.nodeID)
if err != nil { if err != nil {
return err return err
} }
@ -231,7 +231,7 @@ func (b *Box) Start(ctx context.Context) error {
// Stop stops the sandbox without removing it. // Stop stops the sandbox without removing it.
func (b *Box) Stop(ctx context.Context) error { func (b *Box) Stop(ctx context.Context) error {
client, err := b.manager.getPool(b.pool) client, err := b.manager.getNode(b.nodeID)
if err != nil { if err != nil {
return err return err
} }
@ -245,7 +245,7 @@ func (b *Box) Remove(ctx context.Context) error {
// Info returns current sandbox status. // Info returns current sandbox status.
func (b *Box) Info(ctx context.Context) (*BoxInfo, error) { func (b *Box) Info(ctx context.Context) (*BoxInfo, error) {
client, err := b.manager.getPool(b.pool) client, err := b.manager.getNode(b.nodeID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -258,7 +258,7 @@ func (b *Box) Info(ctx context.Context) (*BoxInfo, error) {
return &BoxInfo{ return &BoxInfo{
ID: b.id, ID: b.id,
ContainerID: b.containerID, ContainerID: b.containerID,
Pool: b.pool, NodeID: b.nodeID,
Owner: b.owner, Owner: b.owner,
Status: info.Status, Status: info.Status,
Policy: b.policy, Policy: b.policy,

View file

@ -65,10 +65,10 @@ func TestAttachWS(t *testing.T) {
t.Skip("WebSocket test requires tai-sandbox-test image with ws-echo service") t.Skip("WebSocket test requires tai-sandbox-test image with ws-echo service")
} }
for _, pc := range testPools() { for _, pc := range testNodes() {
pc := pc pc := pc
t.Run(pc.Name, func(t *testing.T) { t.Run(pc.Name, func(t *testing.T) {
m := setupManagerForPool(t, &pc) m := setupManagerForNode(t, &pc)
box := createTestBox(t, m, pc, func(co *sandbox.CreateOptions) { box := createTestBox(t, m, pc, func(co *sandbox.CreateOptions) {
co.Ports = []sandbox.PortMapping{ co.Ports = []sandbox.PortMapping{
{ContainerPort: 9800, HostPort: 0, Protocol: "tcp"}, {ContainerPort: 9800, HostPort: 0, Protocol: "tcp"},
@ -114,10 +114,10 @@ func TestAttachSSE(t *testing.T) {
t.Skip("SSE test requires tai-sandbox-test image with sse-server service") t.Skip("SSE test requires tai-sandbox-test image with sse-server service")
} }
for _, pc := range testPools() { for _, pc := range testNodes() {
pc := pc pc := pc
t.Run(pc.Name, func(t *testing.T) { t.Run(pc.Name, func(t *testing.T) {
m := setupManagerForPool(t, &pc) m := setupManagerForNode(t, &pc)
box := createTestBox(t, m, pc, func(co *sandbox.CreateOptions) { box := createTestBox(t, m, pc, func(co *sandbox.CreateOptions) {
co.Ports = []sandbox.PortMapping{ co.Ports = []sandbox.PortMapping{
{ContainerPort: 9801, HostPort: 0, Protocol: "tcp"}, {ContainerPort: 9801, HostPort: 0, Protocol: "tcp"},
@ -164,10 +164,10 @@ func TestVNCURL(t *testing.T) {
t.Skip("VNC test requires tai-sandbox-test image with VNC desktop") t.Skip("VNC test requires tai-sandbox-test image with VNC desktop")
} }
for _, pc := range testPools() { for _, pc := range testNodes() {
pc := pc pc := pc
t.Run(pc.Name, func(t *testing.T) { t.Run(pc.Name, func(t *testing.T) {
m := setupManagerForPool(t, &pc) m := setupManagerForNode(t, &pc)
box := createTestBox(t, m, pc, func(co *sandbox.CreateOptions) { box := createTestBox(t, m, pc, func(co *sandbox.CreateOptions) {
co.VNC = true co.VNC = true
}) })
@ -195,10 +195,10 @@ func TestVNCConnect(t *testing.T) {
t.Skip("VNC test requires tai-sandbox-test image with VNC desktop") t.Skip("VNC test requires tai-sandbox-test image with VNC desktop")
} }
for _, pc := range testPools() { for _, pc := range testNodes() {
pc := pc pc := pc
t.Run(pc.Name, func(t *testing.T) { t.Run(pc.Name, func(t *testing.T) {
m := setupManagerForPool(t, &pc) m := setupManagerForNode(t, &pc)
box := createTestBox(t, m, pc, func(co *sandbox.CreateOptions) { box := createTestBox(t, m, pc, func(co *sandbox.CreateOptions) {
co.VNC = true co.VNC = true
}) })

View file

@ -11,12 +11,12 @@ import (
) )
func TestImageExists(t *testing.T) { func TestImageExists(t *testing.T) {
for _, pc := range testPools() { for _, pc := range testNodes() {
pc := pc pc := pc
t.Run(pc.Name, func(t *testing.T) { t.Run(pc.Name, func(t *testing.T) {
if pc.Name == "k8s" { if pc.Name == "k8s" {
t.Run("always_true", func(t *testing.T) { t.Run("always_true", func(t *testing.T) {
m := setupManagerForPool(t, &pc) m := setupManagerForNode(t, &pc)
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel() defer cancel()
exists, err := m.ImageExists(ctx, pc.TaiID, "anything:nonexistent") exists, err := m.ImageExists(ctx, pc.TaiID, "anything:nonexistent")
@ -26,7 +26,7 @@ func TestImageExists(t *testing.T) {
return return
} }
m := setupManagerForPool(t, &pc) m := setupManagerForNode(t, &pc)
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel() defer cancel()
@ -46,12 +46,12 @@ func TestImageExists(t *testing.T) {
} }
func TestImagePull(t *testing.T) { func TestImagePull(t *testing.T) {
for _, pc := range testPools() { for _, pc := range testNodes() {
pc := pc pc := pc
t.Run(pc.Name, func(t *testing.T) { t.Run(pc.Name, func(t *testing.T) {
if pc.Name == "k8s" { if pc.Name == "k8s" {
t.Run("noop", func(t *testing.T) { t.Run("noop", func(t *testing.T) {
m := setupManagerForPool(t, &pc) m := setupManagerForNode(t, &pc)
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel() defer cancel()
ch, err := m.PullImage(ctx, pc.TaiID, "alpine:latest", sandbox.ImagePullOptions{}) ch, err := m.PullImage(ctx, pc.TaiID, "alpine:latest", sandbox.ImagePullOptions{})
@ -61,7 +61,7 @@ func TestImagePull(t *testing.T) {
return return
} }
m := setupManagerForPool(t, &pc) m := setupManagerForNode(t, &pc)
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
defer cancel() defer cancel()
@ -84,10 +84,10 @@ func TestImagePull(t *testing.T) {
} }
func TestEnsureImage(t *testing.T) { func TestEnsureImage(t *testing.T) {
for _, pc := range testPools() { for _, pc := range testNodes() {
pc := pc pc := pc
t.Run(pc.Name, func(t *testing.T) { t.Run(pc.Name, func(t *testing.T) {
m := setupManagerForPool(t, &pc) m := setupManagerForNode(t, &pc)
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
defer cancel() defer cancel()
@ -104,13 +104,13 @@ func TestEnsureImage(t *testing.T) {
} }
func TestEnsureImage_BadRef(t *testing.T) { func TestEnsureImage_BadRef(t *testing.T) {
for _, pc := range testPools() { for _, pc := range testNodes() {
pc := pc pc := pc
if pc.Name == "k8s" { if pc.Name == "k8s" {
continue continue
} }
t.Run(pc.Name, func(t *testing.T) { t.Run(pc.Name, func(t *testing.T) {
m := setupManagerForPool(t, &pc) m := setupManagerForNode(t, &pc)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel() defer cancel()

View file

@ -13,10 +13,10 @@ import (
func TestBoxExec(t *testing.T) { func TestBoxExec(t *testing.T) {
skipIfNoDocker(t) skipIfNoDocker(t)
for _, pc := range testPools() { for _, pc := range testNodes() {
pc := pc pc := pc
t.Run(pc.Name, func(t *testing.T) { t.Run(pc.Name, func(t *testing.T) {
m := setupManagerForPool(t, &pc) m := setupManagerForNode(t, &pc)
box := createTestBox(t, m, pc) box := createTestBox(t, m, pc)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
@ -36,10 +36,10 @@ func TestBoxExec(t *testing.T) {
func TestBoxExecWithOptions(t *testing.T) { func TestBoxExecWithOptions(t *testing.T) {
skipIfNoDocker(t) skipIfNoDocker(t)
for _, pc := range testPools() { for _, pc := range testNodes() {
pc := pc pc := pc
t.Run(pc.Name, func(t *testing.T) { t.Run(pc.Name, func(t *testing.T) {
m := setupManagerForPool(t, &pc) m := setupManagerForNode(t, &pc)
box := createTestBox(t, m, pc) box := createTestBox(t, m, pc)
ctx := context.Background() ctx := context.Background()
@ -59,10 +59,10 @@ func TestBoxExecWithOptions(t *testing.T) {
func TestBoxStream(t *testing.T) { func TestBoxStream(t *testing.T) {
skipIfNoDocker(t) skipIfNoDocker(t)
for _, pc := range testPools() { for _, pc := range testNodes() {
pc := pc pc := pc
t.Run(pc.Name, func(t *testing.T) { t.Run(pc.Name, func(t *testing.T) {
m := setupManagerForPool(t, &pc) m := setupManagerForNode(t, &pc)
box := createTestBox(t, m, pc) box := createTestBox(t, m, pc)
ctx := context.Background() ctx := context.Background()
@ -93,10 +93,10 @@ func TestBoxStream(t *testing.T) {
func TestBoxWorkspace(t *testing.T) { func TestBoxWorkspace(t *testing.T) {
skipIfNoDocker(t) skipIfNoDocker(t)
for _, pc := range testPools() { for _, pc := range testNodes() {
pc := pc pc := pc
t.Run(pc.Name, func(t *testing.T) { t.Run(pc.Name, func(t *testing.T) {
m := setupManagerForPool(t, &pc) m := setupManagerForNode(t, &pc)
box := createTestBox(t, m, pc) box := createTestBox(t, m, pc)
ws := box.Workspace() ws := box.Workspace()
@ -135,10 +135,10 @@ func TestBoxWorkspace(t *testing.T) {
func TestBoxInfo(t *testing.T) { func TestBoxInfo(t *testing.T) {
skipIfNoDocker(t) skipIfNoDocker(t)
for _, pc := range testPools() { for _, pc := range testNodes() {
pc := pc pc := pc
t.Run(pc.Name, func(t *testing.T) { t.Run(pc.Name, func(t *testing.T) {
m := setupManagerForPool(t, &pc) m := setupManagerForNode(t, &pc)
box := createTestBox(t, m, pc) box := createTestBox(t, m, pc)
ctx := context.Background() ctx := context.Background()
@ -162,10 +162,10 @@ func TestBoxInfo(t *testing.T) {
func TestBoxStopStart(t *testing.T) { func TestBoxStopStart(t *testing.T) {
skipIfNoDocker(t) skipIfNoDocker(t)
for _, pc := range testPools() { for _, pc := range testNodes() {
pc := pc pc := pc
t.Run(pc.Name, func(t *testing.T) { t.Run(pc.Name, func(t *testing.T) {
m := setupManagerForPool(t, &pc) m := setupManagerForNode(t, &pc)
box := createTestBox(t, m, pc) box := createTestBox(t, m, pc)
ctx := context.Background() ctx := context.Background()
@ -191,16 +191,16 @@ func TestBoxStopStart(t *testing.T) {
func TestBoxGetOrCreate(t *testing.T) { func TestBoxGetOrCreate(t *testing.T) {
skipIfNoDocker(t) skipIfNoDocker(t)
for _, pc := range testPools() { for _, pc := range testNodes() {
pc := pc pc := pc
t.Run(pc.Name, func(t *testing.T) { t.Run(pc.Name, func(t *testing.T) {
m := setupManagerForPool(t, &pc) m := setupManagerForNode(t, &pc)
ctx := context.Background() ctx := context.Background()
box1, err := m.GetOrCreate(ctx, sandbox.CreateOptions{ box1, err := m.GetOrCreate(ctx, sandbox.CreateOptions{
ID: "goc-" + pc.Name, ID: "goc-" + pc.Name,
Image: testImage(), Image: testImage(),
Owner: "test-user", Owner: "test-user",
Pool: pc.TaiID, NodeID: pc.TaiID,
}) })
if err != nil { if err != nil {
t.Fatalf("GetOrCreate first: %v", err) t.Fatalf("GetOrCreate first: %v", err)
@ -211,7 +211,7 @@ func TestBoxGetOrCreate(t *testing.T) {
ID: "goc-" + pc.Name, ID: "goc-" + pc.Name,
Image: testImage(), Image: testImage(),
Owner: "test-user", Owner: "test-user",
Pool: pc.TaiID, NodeID: pc.TaiID,
}) })
if err != nil { if err != nil {
t.Fatalf("GetOrCreate second: %v", err) t.Fatalf("GetOrCreate second: %v", err)

View file

@ -15,7 +15,7 @@ import (
func TestWorkspaceID_Set(t *testing.T) { func TestWorkspaceID_Set(t *testing.T) {
skipIfNoDocker(t) skipIfNoDocker(t)
for _, pc := range testPools() { for _, pc := range testNodes() {
pc := pc pc := pc
t.Run(pc.Name, func(t *testing.T) { t.Run(pc.Name, func(t *testing.T) {
sbm, wsm := setupManagerWithWorkspace(t, &pc) sbm, wsm := setupManagerWithWorkspace(t, &pc)
@ -41,10 +41,10 @@ func TestWorkspaceID_Set(t *testing.T) {
func TestWorkspaceID_Empty(t *testing.T) { func TestWorkspaceID_Empty(t *testing.T) {
skipIfNoDocker(t) skipIfNoDocker(t)
for _, pc := range testPools() { for _, pc := range testNodes() {
pc := pc pc := pc
t.Run(pc.Name, func(t *testing.T) { t.Run(pc.Name, func(t *testing.T) {
m := setupManagerForPool(t, &pc) m := setupManagerForNode(t, &pc)
box := createTestBox(t, m, pc) box := createTestBox(t, m, pc)
assert.Empty(t, box.WorkspaceID()) assert.Empty(t, box.WorkspaceID())
}) })
@ -54,7 +54,7 @@ func TestWorkspaceID_Empty(t *testing.T) {
func TestWorkspace_NodeRouting(t *testing.T) { func TestWorkspace_NodeRouting(t *testing.T) {
skipIfNoDocker(t) skipIfNoDocker(t)
for _, pc := range testPools() { for _, pc := range testNodes() {
pc := pc pc := pc
t.Run(pc.Name, func(t *testing.T) { t.Run(pc.Name, func(t *testing.T) {
sbm, wsm := setupManagerWithWorkspace(t, &pc) sbm, wsm := setupManagerWithWorkspace(t, &pc)
@ -72,7 +72,7 @@ func TestWorkspace_NodeRouting(t *testing.T) {
co.WorkspaceID = ws.ID co.WorkspaceID = ws.ID
}) })
assert.Equal(t, pc.TaiID, box.Pool()) assert.Equal(t, pc.TaiID, box.NodeID())
}) })
} }
} }
@ -80,7 +80,7 @@ func TestWorkspace_NodeRouting(t *testing.T) {
func TestWorkspace_InvalidID(t *testing.T) { func TestWorkspace_InvalidID(t *testing.T) {
skipIfNoDocker(t) skipIfNoDocker(t)
for _, pc := range testPools() { for _, pc := range testNodes() {
pc := pc pc := pc
t.Run(pc.Name, func(t *testing.T) { t.Run(pc.Name, func(t *testing.T) {
sbm, _ := setupManagerWithWorkspace(t, &pc) sbm, _ := setupManagerWithWorkspace(t, &pc)
@ -103,7 +103,7 @@ func TestWorkspace_InvalidID(t *testing.T) {
func TestWorkspace_BindMountLocal(t *testing.T) { func TestWorkspace_BindMountLocal(t *testing.T) {
skipIfNoDocker(t) skipIfNoDocker(t)
pc := poolConfig{Name: "local", Addr: testLocalAddr()} pc := nodeConfig{Name: "local", Addr: testLocalAddr()}
sbm, wsm := setupManagerWithWorkspace(t, &pc) sbm, wsm := setupManagerWithWorkspace(t, &pc)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
@ -129,7 +129,7 @@ func TestWorkspace_BindMountLocal(t *testing.T) {
func TestWorkspace_ContainerWriteBack(t *testing.T) { func TestWorkspace_ContainerWriteBack(t *testing.T) {
skipIfNoDocker(t) skipIfNoDocker(t)
pc := poolConfig{Name: "local", Addr: testLocalAddr()} pc := nodeConfig{Name: "local", Addr: testLocalAddr()}
sbm, wsm := setupManagerWithWorkspace(t, &pc) sbm, wsm := setupManagerWithWorkspace(t, &pc)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
@ -156,7 +156,7 @@ func TestWorkspace_ContainerWriteBack(t *testing.T) {
func TestWorkspace_ReadOnlyMount(t *testing.T) { func TestWorkspace_ReadOnlyMount(t *testing.T) {
skipIfNoDocker(t) skipIfNoDocker(t)
pc := poolConfig{Name: "local", Addr: testLocalAddr()} pc := nodeConfig{Name: "local", Addr: testLocalAddr()}
sbm, wsm := setupManagerWithWorkspace(t, &pc) sbm, wsm := setupManagerWithWorkspace(t, &pc)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
@ -189,7 +189,7 @@ func TestWorkspace_ReadOnlyMount(t *testing.T) {
func TestWorkspace_CustomMountPath(t *testing.T) { func TestWorkspace_CustomMountPath(t *testing.T) {
skipIfNoDocker(t) skipIfNoDocker(t)
pc := poolConfig{Name: "local", Addr: testLocalAddr()} pc := nodeConfig{Name: "local", Addr: testLocalAddr()}
sbm, wsm := setupManagerWithWorkspace(t, &pc) sbm, wsm := setupManagerWithWorkspace(t, &pc)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
@ -216,7 +216,7 @@ func TestWorkspace_CustomMountPath(t *testing.T) {
func TestWorkspace_BoxWorkspaceFS(t *testing.T) { func TestWorkspace_BoxWorkspaceFS(t *testing.T) {
skipIfNoDocker(t) skipIfNoDocker(t)
for _, pc := range testPools() { for _, pc := range testNodes() {
if pc.Name == "local" { if pc.Name == "local" {
continue continue
} }
@ -254,7 +254,7 @@ func TestWorkspace_BoxWorkspaceFS(t *testing.T) {
func TestWorkspace_LabelPersistence(t *testing.T) { func TestWorkspace_LabelPersistence(t *testing.T) {
skipIfNoDocker(t) skipIfNoDocker(t)
for _, pc := range testPools() { for _, pc := range testNodes() {
pc := pc pc := pc
t.Run(pc.Name, func(t *testing.T) { t.Run(pc.Name, func(t *testing.T) {
sbm, wsm := setupManagerWithWorkspace(t, &pc) sbm, wsm := setupManagerWithWorkspace(t, &pc)

View file

@ -2,7 +2,7 @@
Package: `github.com/yaoapp/yao/sandbox/v2` Package: `github.com/yaoapp/yao/sandbox/v2`
Sandbox V2 manages sandboxes through a pool of Tai nodes. Two primary abstractions: Sandbox V2 manages sandboxes through a set of Tai nodes. Two primary abstractions:
- **Box** — a container (Docker or K8s pod). Created via `Manager.Create`. - **Box** — a container (Docker or K8s pod). Created via `Manager.Create`.
- **Host** — the Tai host machine itself. Obtained via `Manager.Host` (no Create needed). - **Host** — the Tai host machine itself. Obtained via `Manager.Host` (no Create needed).
@ -42,10 +42,10 @@ mgr := sandbox.M()
## Node Discovery ## Node Discovery
Sandbox V2 no longer uses a static pool configuration. Nodes are discovered dynamically Sandbox V2 no longer uses static node configuration. Nodes are discovered dynamically
through `tai/registry`. Each Tai node registers itself with a unique **TaiID** (e.g. through `tai/registry`. Each Tai node registers itself with a unique **TaiID** (e.g.
`"192.168.1.10-19100"` for direct mode, `"local"` for Docker). The TaiID is used as the `"192.168.1.10-19100"` for direct mode, `"local"` for Docker). The TaiID is used as the
`Pool` identifier in `CreateOptions`, `ListOptions`, `Host()`, `ImageExists()`, etc. `NodeID` identifier in `CreateOptions`, `ListOptions`, `Host()`, `ImageExists()`, etc.
--- ---
@ -72,7 +72,7 @@ const (
func (m *Manager) Start(ctx context.Context) error func (m *Manager) Start(ctx context.Context) error
``` ```
Recovers existing containers from all pools and starts the background cleanup loop (1 min interval). Recovers existing containers from all nodes and starts the background cleanup loop (1 min interval).
```go ```go
ctx := context.Background() ctx := context.Background()
@ -85,7 +85,7 @@ err := sandbox.M().Start(ctx)
func (m *Manager) Close() error func (m *Manager) Close() error
``` ```
Stops the cleanup loop and closes all pool connections. Stops the cleanup loop and closes all node connections.
### Create ### Create
@ -99,7 +99,7 @@ Creates and starts a new sandbox container. Returns a `Box` handle.
box, err := sandbox.M().Create(ctx, sandbox.CreateOptions{ box, err := sandbox.M().Create(ctx, sandbox.CreateOptions{
Image: "alpine:latest", Image: "alpine:latest",
Owner: "user-123", Owner: "user-123",
Pool: "192.168.1.10-19100", // TaiID from registry NodeID: "192.168.1.10-19100", // TaiID from registry
Policy: sandbox.Session, Policy: sandbox.Session,
WorkDir: "/workspace", WorkDir: "/workspace",
Env: map[string]string{"LANG": "en_US.UTF-8"}, Env: map[string]string{"LANG": "en_US.UTF-8"},
@ -121,13 +121,13 @@ box, err := sandbox.M().Create(ctx, sandbox.CreateOptions{
### Host ### Host
```go ```go
func (m *Manager) Host(ctx context.Context, pool string) (*Host, error) func (m *Manager) Host(ctx context.Context, nodeID string) (*Host, error)
``` ```
Returns a `Host` handle for the given pool (identified by TaiID). Unlike `Create`, no Returns a `Host` handle for the given node (identified by TaiID). Unlike `Create`, no
container is provisioned — the Host is available as long as the Tai server reports container is provisioned — the Host is available as long as the Tai server reports
`host_exec` capability. Returns `ErrPoolNotFound` if the TaiID is not registered, `host_exec` capability. Returns `ErrNodeNotFound` if the TaiID is not registered,
`ErrPoolMissing` if the pool argument is empty, or an error if the node has no `host_exec`. `ErrNodeMissing` if the nodeID argument is empty, or an error if the node has no `host_exec`.
```go ```go
host, err := sandbox.M().Host(ctx, "192.168.1.10-19100") host, err := sandbox.M().Host(ctx, "192.168.1.10-19100")
@ -172,7 +172,7 @@ Returns all sandboxes matching the given filters. Empty fields = no filter.
```go ```go
boxes, err := sandbox.M().List(ctx, sandbox.ListOptions{ boxes, err := sandbox.M().List(ctx, sandbox.ListOptions{
Owner: "user-123", Owner: "user-123",
Pool: "192.168.1.10-19100", NodeID: "192.168.1.10-19100",
Labels: map[string]string{"project": "demo"}, Labels: map[string]string{"project": "demo"},
}) })
``` ```
@ -210,16 +210,16 @@ Updates a sandbox's last-active timestamp. Called by the gRPC heartbeat service.
err := sandbox.M().Heartbeat("sb-12345", true, 3) err := sandbox.M().Heartbeat("sb-12345", true, 3)
``` ```
### Pools ### Nodes
```go ```go
func (m *Manager) Pools() []registry.NodeSnapshot func (m *Manager) Nodes() []registry.NodeSnapshot
``` ```
Returns all registered Tai nodes from the `tai/registry`. Returns all registered Tai nodes from the `tai/registry`.
```go ```go
for _, n := range sandbox.M().Pools() { for _, n := range sandbox.M().Nodes() {
fmt.Printf("tai_id=%s mode=%s addr=%s status=%s\n", fmt.Printf("tai_id=%s mode=%s addr=%s status=%s\n",
n.TaiID, n.Mode, n.Addr, n.Status) n.TaiID, n.Mode, n.Addr, n.Status)
} }
@ -228,11 +228,11 @@ for _, n := range sandbox.M().Pools() {
### ImageExists ### ImageExists
```go ```go
func (m *Manager) ImageExists(ctx context.Context, pool, ref string) (bool, error) func (m *Manager) ImageExists(ctx context.Context, nodeID, ref string) (bool, error)
``` ```
Reports whether the given image ref exists on the target pool node. Reports whether the given image ref exists on the target node.
Returns `(true, nil)` when the pool has no image service (e.g. K8s — kubelet handles pulls). Returns `(true, nil)` when the node has no image service (e.g. K8s — kubelet handles pulls).
```go ```go
exists, err := sandbox.M().ImageExists(ctx, "192.168.1.10-19100", "alpine:latest") exists, err := sandbox.M().ImageExists(ctx, "192.168.1.10-19100", "alpine:latest")
@ -241,11 +241,11 @@ exists, err := sandbox.M().ImageExists(ctx, "192.168.1.10-19100", "alpine:latest
### PullImage ### PullImage
```go ```go
func (m *Manager) PullImage(ctx context.Context, pool, ref string, opts ImagePullOptions) (<-chan taisandbox.PullProgress, error) func (m *Manager) PullImage(ctx context.Context, nodeID, ref string, opts ImagePullOptions) (<-chan taisandbox.PullProgress, error)
``` ```
Pulls an image to the target pool node. Returns a channel of `taisandbox.PullProgress` Pulls an image to the target node. Returns a channel of `taisandbox.PullProgress`
(from `github.com/yaoapp/yao/tai/sandbox`). Returns `(nil, nil)` when the pool has no image (from `github.com/yaoapp/yao/tai/sandbox`). Returns `(nil, nil)` when the node has no image
service (e.g. K8s). service (e.g. K8s).
`PullProgress` fields: `Status string`, `Layer string`, `Current int64`, `Total int64`, `Error string`. `PullProgress` fields: `Status string`, `Layer string`, `Current int64`, `Total int64`, `Error string`.
@ -266,7 +266,7 @@ for p := range ch {
### EnsureImage ### EnsureImage
```go ```go
func (m *Manager) EnsureImage(ctx context.Context, pool, ref string, opts ImagePullOptions) error func (m *Manager) EnsureImage(ctx context.Context, nodeID, ref string, opts ImagePullOptions) error
``` ```
Checks if the image exists; if not, pulls it and blocks until complete. Checks if the image exists; if not, pulls it and blocks until complete.
@ -287,7 +287,7 @@ A `Box` is a handle to a running sandbox container.
func (b *Box) ID() string func (b *Box) ID() string
func (b *Box) Owner() string func (b *Box) Owner() string
func (b *Box) ContainerID() string func (b *Box) ContainerID() string
func (b *Box) Pool() string func (b *Box) NodeID() string
func (b *Box) WorkspaceID() string func (b *Box) WorkspaceID() string
``` ```
@ -423,12 +423,12 @@ fmt.Printf("status=%s processes=%d vnc=%v created=%s\n",
## Host ## Host
A `Host` represents a Tai host machine execution environment, distinct from `Box` (containers). A `Host` represents a Tai host machine execution environment, distinct from `Box` (containers).
No `Create` call is needed — a Host is available as long as the pool's Tai server reports `host_exec`. No `Create` call is needed — a Host is available as long as the node's Tai server reports `host_exec`.
### Accessors ### Accessors
```go ```go
func (h *Host) Pool() string func (h *Host) NodeID() string
``` ```
### Exec ### Exec
@ -539,7 +539,7 @@ type CreateOptions struct {
ID string ID string
Owner string Owner string
Labels map[string]string Labels map[string]string
Pool string // TaiID from registry (required unless WorkspaceID routes to a node) NodeID string // TaiID from registry (required unless WorkspaceID routes to a node)
Image string // required Image string // required
WorkDir string // default "/workspace" WorkDir string // default "/workspace"
User string // container user User string // container user
@ -563,7 +563,7 @@ type CreateOptions struct {
```go ```go
type ListOptions struct { type ListOptions struct {
Owner string Owner string
Pool string NodeID string
Labels map[string]string Labels map[string]string
} }
``` ```
@ -619,7 +619,7 @@ type ServiceConn struct {
type BoxInfo struct { type BoxInfo struct {
ID string ID string
ContainerID string ContainerID string
Pool string NodeID string
Owner string Owner string
Status string // "running", "stopped", etc. Status string // "running", "stopped", etc.
Policy LifecyclePolicy Policy LifecyclePolicy
@ -676,10 +676,10 @@ type HostExecStream struct {
```go ```go
var ( var (
ErrNotAvailable = errors.New("sandbox: not available (no pools configured)") ErrNotAvailable = errors.New("sandbox: not available (no nodes registered)")
ErrNotFound = errors.New("sandbox: not found") ErrNotFound = errors.New("sandbox: not found")
ErrPoolNotFound = errors.New("sandbox: pool not found") ErrNodeNotFound = errors.New("sandbox: node not found")
ErrPoolMissing = errors.New("sandbox: pool name is required") ErrNodeMissing = errors.New("sandbox: node ID is required")
) )
``` ```

View file

@ -3,8 +3,8 @@ package sandbox
import "errors" import "errors"
var ( var (
ErrNotAvailable = errors.New("sandbox: not available (no pools configured)") ErrNotAvailable = errors.New("sandbox: not available (no nodes registered)")
ErrNotFound = errors.New("sandbox: not found") ErrNotFound = errors.New("sandbox: not found")
ErrPoolNotFound = errors.New("sandbox: pool not found") ErrNodeNotFound = errors.New("sandbox: node not found")
ErrPoolMissing = errors.New("sandbox: pool name is required") ErrNodeMissing = errors.New("sandbox: node ID is required")
) )

View file

@ -16,7 +16,7 @@ import (
// //
// Host implements the Computer interface. // Host implements the Computer interface.
type Host struct { type Host struct {
pool string nodeID string
workplaceID string workplaceID string
manager *Manager manager *Manager
} }
@ -25,12 +25,12 @@ type Host struct {
var _ Computer = (*Host)(nil) var _ Computer = (*Host)(nil)
// ComputerInfo returns identity and registry information for the host. // ComputerInfo returns identity and registry information for the host.
// Registry-level details (TaiID, System, etc.) are populated when the pool // Registry-level details (TaiID, System, etc.) are populated when the node
// is backed by a registered Tai node; otherwise only Kind and Pool are set. // is backed by a registered Tai node; otherwise only Kind and NodeID are set.
func (h *Host) ComputerInfo() ComputerInfo { func (h *Host) ComputerInfo() ComputerInfo {
return ComputerInfo{ return ComputerInfo{
Kind: "host", Kind: "host",
Pool: h.pool, NodeID: h.nodeID,
Status: "online", Status: "online",
} }
} }
@ -42,14 +42,14 @@ func (h *Host) Exec(ctx context.Context, cmd []string, opts ...ExecOption) (*Exe
return nil, fmt.Errorf("sandbox: empty command") return nil, fmt.Errorf("sandbox: empty command")
} }
client, err := h.manager.getPool(h.pool) client, err := h.manager.getNode(h.nodeID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
he := client.HostExec() he := client.HostExec()
if he == nil { if he == nil {
return nil, fmt.Errorf("sandbox: host_exec not available on pool %q", h.pool) return nil, fmt.Errorf("sandbox: host_exec not available on node %q", h.nodeID)
} }
cfg := &execConfig{} cfg := &execConfig{}
@ -98,14 +98,14 @@ func (h *Host) Stream(ctx context.Context, cmd []string, opts ...ExecOption) (*E
return nil, fmt.Errorf("sandbox: empty command") return nil, fmt.Errorf("sandbox: empty command")
} }
client, err := h.manager.getPool(h.pool) client, err := h.manager.getNode(h.nodeID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
he := client.HostExec() he := client.HostExec()
if he == nil { if he == nil {
return nil, fmt.Errorf("sandbox: host_exec not available on pool %q", h.pool) return nil, fmt.Errorf("sandbox: host_exec not available on node %q", h.nodeID)
} }
cfg := &execConfig{} cfg := &execConfig{}
@ -187,7 +187,7 @@ func (h *Host) Stream(ctx context.Context, cmd []string, opts ...ExecOption) (*E
// VNC returns the VNC WebSocket URL for the Tai host machine. // VNC returns the VNC WebSocket URL for the Tai host machine.
// Uses the special __host__ identifier to route to localhost:5900 on the Tai server. // Uses the special __host__ identifier to route to localhost:5900 on the Tai server.
func (h *Host) VNC(ctx context.Context) (string, error) { func (h *Host) VNC(ctx context.Context) (string, error) {
client, err := h.manager.getPool(h.pool) client, err := h.manager.getNode(h.nodeID)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -197,7 +197,7 @@ func (h *Host) VNC(ctx context.Context) (string, error) {
// Proxy returns the HTTP URL for a service running on the Tai host machine. // Proxy returns the HTTP URL for a service running on the Tai host machine.
// Uses the special __host__ identifier to route to localhost:{port} on the Tai server. // Uses the special __host__ identifier to route to localhost:{port} on the Tai server.
func (h *Host) Proxy(ctx context.Context, port int, path string) (string, error) { func (h *Host) Proxy(ctx context.Context, port int, path string) (string, error) {
client, err := h.manager.getPool(h.pool) client, err := h.manager.getNode(h.nodeID)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -215,15 +215,15 @@ func (h *Host) Workplace() workspace.FS {
if h.workplaceID == "" { if h.workplaceID == "" {
return nil return nil
} }
client, err := h.manager.getPool(h.pool) client, err := h.manager.getNode(h.nodeID)
if err != nil { if err != nil {
return nil return nil
} }
return client.Workspace(h.workplaceID) return client.Workspace(h.workplaceID)
} }
// Pool returns the pool name this Host belongs to. // NodeID returns the node ID this Host belongs to.
func (h *Host) Pool() string { return h.pool } func (h *Host) NodeID() string { return h.nodeID }
// nopWriteCloser wraps an io.Writer with a no-op Close. // nopWriteCloser wraps an io.Writer with a no-op Close.
type nopWriteCloser struct{ io.Writer } type nopWriteCloser struct{ io.Writer }

View file

@ -15,8 +15,8 @@ import (
func setupHostManager(t *testing.T, tgt *hostExecTarget) *sandbox.Manager { func setupHostManager(t *testing.T, tgt *hostExecTarget) *sandbox.Manager {
t.Helper() t.Helper()
addr := fmt.Sprintf("tai://%s", tgt.Addr) addr := fmt.Sprintf("tai://%s", tgt.Addr)
m, pools := setupManager(t, poolConfig{Name: tgt.Name, Addr: addr}) m, nodes := setupManager(t, nodeConfig{Name: tgt.Name, Addr: addr})
tgt.TaiID = pools[0].TaiID tgt.TaiID = nodes[0].TaiID
return m return m
} }
@ -384,8 +384,8 @@ func TestHost_ComputerInfo(t *testing.T) {
if info.Kind != "host" { if info.Kind != "host" {
t.Errorf("Kind = %q, want 'host'", info.Kind) t.Errorf("Kind = %q, want 'host'", info.Kind)
} }
if info.Pool != tgt.TaiID { if info.NodeID != tgt.TaiID {
t.Errorf("Pool = %q, want %q", info.Pool, tgt.TaiID) t.Errorf("NodeID = %q, want %q", info.NodeID, tgt.TaiID)
} }
}) })
} }
@ -414,7 +414,7 @@ func TestHost_ComputerInterface(t *testing.T) {
} }
} }
func TestHost_CreateRejectsNoContainerPool(t *testing.T) { func TestHost_CreateRejectsNoContainerNode(t *testing.T) {
tgt := findHostExecOnly(t) tgt := findHostExecOnly(t)
if tgt == nil { if tgt == nil {
t.Skip("no host-exec-only target available") t.Skip("no host-exec-only target available")
@ -428,22 +428,22 @@ func TestHost_CreateRejectsNoContainerPool(t *testing.T) {
_, err := m.Create(ctx, sandbox.CreateOptions{ _, err := m.Create(ctx, sandbox.CreateOptions{
Image: "alpine:latest", Image: "alpine:latest",
Owner: "test", Owner: "test",
Pool: tgt.TaiID, NodeID: tgt.TaiID,
}) })
if err == nil { if err == nil {
t.Fatal("expected error for Create on host-exec-only pool, got nil") t.Fatal("expected error for Create on host-exec-only node, got nil")
} }
if !strings.Contains(err.Error(), "no container runtime") { if !strings.Contains(err.Error(), "no container runtime") {
t.Errorf("error = %q, want contains 'no container runtime'", err.Error()) t.Errorf("error = %q, want contains 'no container runtime'", err.Error())
} }
} }
func TestHost_PoolNotFound(t *testing.T) { func TestHost_NodeNotFound(t *testing.T) {
skipIfNoHostExec(t) skipIfNoHostExec(t)
tgt := hostExecTargets()[0] tgt := hostExecTargets()[0]
m := setupHostManager(t, &tgt) m := setupHostManager(t, &tgt)
_, err := m.Host(context.Background(), "nonexistent-pool") _, err := m.Host(context.Background(), "nonexistent-node")
if err == nil { if err == nil {
t.Fatal("expected error, got nil") t.Fatal("expected error, got nil")
} }

View file

@ -31,7 +31,7 @@ Create a new sandbox container. Returns a Computer (`kind = "box"`). If `options
const pc = sandbox.Create({ const pc = sandbox.Create({
image: "node:20", // required — container image image: "node:20", // required — container image
owner: "user-123", // required — owner identifier owner: "user-123", // required — owner identifier
pool: "192.168.1.10-19100", // optional — TaiID from registry (required unless workspace_id routes to a node) node_id: "192.168.1.10-19100", // optional — TaiID from registry (required unless workspace_id routes to a node)
id: "my-sandbox", // optional — if set, uses GetOrCreate id: "my-sandbox", // optional — if set, uses GetOrCreate
workdir: "/app", // optional — working directory workdir: "/app", // optional — working directory
user: "1000:1000", // optional — UID:GID user: "1000:1000", // optional — UID:GID
@ -59,7 +59,7 @@ Get an existing sandbox by ID. Returns a Computer (`kind = "box"`) or `null` if
```javascript ```javascript
const pc = sandbox.Get("my-sandbox") const pc = sandbox.Get("my-sandbox")
if (pc) { if (pc) {
console.log(pc.kind, pc.id, pc.owner, pc.pool) console.log(pc.kind, pc.id, pc.owner, pc.node_id)
} }
``` ```
@ -74,8 +74,8 @@ const all = sandbox.List()
// Filter by owner // Filter by owner
const mine = sandbox.List({ owner: "user-123" }) const mine = sandbox.List({ owner: "user-123" })
// Filter by pool (TaiID) and labels // Filter by node_id (TaiID) and labels
const gpu = sandbox.List({ pool: "10.0.0.5-19100", labels: { team: "ml" } }) const gpu = sandbox.List({ node_id: "10.0.0.5-19100", labels: { team: "ml" } })
``` ```
Each element in the returned array: Each element in the returned array:
@ -84,7 +84,7 @@ Each element in the returned array:
{ {
id: "sb-xxx", id: "sb-xxx",
container_id: "abc123...", container_id: "abc123...",
pool: "192.168.1.10-19100", node_id: "192.168.1.10-19100",
owner: "user-123", owner: "user-123",
status: "running", // "running"|"stopped"|"creating"|... status: "running", // "running"|"stopped"|"creating"|...
image: "node:20", image: "node:20",
@ -105,9 +105,9 @@ Remove a sandbox and its container.
sandbox.Delete("my-sandbox") sandbox.Delete("my-sandbox")
``` ```
### sandbox.Host(pool) → Computer ### sandbox.Host(nodeID?) → Computer
Get a Computer (`kind = "host"`) for executing commands directly on the Tai host machine (no container). Only available when the node's Tai server has `host_exec` capability. The `pool` argument is the TaiID (e.g. `"192.168.1.10-19100"`). Get a Computer (`kind = "host"`) for executing commands directly on the Tai host machine (no container). Only available when the node's Tai server has `host_exec` capability. The `nodeID` argument is the TaiID (e.g. `"192.168.1.10-19100"`).
```javascript ```javascript
const host = sandbox.Host("192.168.1.10-19100") const host = sandbox.Host("192.168.1.10-19100")
@ -131,7 +131,7 @@ List all registered nodes.
```javascript ```javascript
const nodes = sandbox.Nodes() const nodes = sandbox.Nodes()
nodes.forEach(function(n) { nodes.forEach(function(n) {
console.log(n.tai_id, n.status, n.pool, n.system.os) console.log(n.tai_id, n.status, n.display_name, n.system.os)
}) })
``` ```
@ -158,7 +158,7 @@ Use the `kind` property to check the type. Methods marked **box-only** throw an
| `pc.kind` | string | `"box"` or `"host"` | | `pc.kind` | string | `"box"` or `"host"` |
| `pc.id` | string | Sandbox ID (box-only; empty for host) | | `pc.id` | string | Sandbox ID (box-only; empty for host) |
| `pc.owner` | string | Owner identifier (box-only; empty for host) | | `pc.owner` | string | Owner identifier (box-only; empty for host) |
| `pc.pool` | string | TaiID (e.g. `"192.168.1.10-19100"`, `"local"`) | | `pc.node_id` | string | TaiID (e.g. `"192.168.1.10-19100"`, `"local"`) |
### pc.Exec(cmd, options?) → ExecResult ### pc.Exec(cmd, options?) → ExecResult
@ -260,7 +260,7 @@ Get identity and registry information.
```javascript ```javascript
const info = pc.ComputerInfo() const info = pc.ComputerInfo()
console.log(info.kind) // "box" or "host" console.log(info.kind) // "box" or "host"
console.log(info.pool) // TaiID console.log(info.node_id) // TaiID
console.log(info.system.os) // "linux" | "windows" | "darwin" console.log(info.system.os) // "linux" | "windows" | "darwin"
console.log(info.status) // "running" | "stopped" | ... console.log(info.status) // "running" | "stopped" | ...
``` ```
@ -332,7 +332,7 @@ Returned by `pc.ComputerInfo()`. Read-only snapshot of a Computer's identity and
```javascript ```javascript
{ {
kind: "box", // "box" | "host" kind: "box", // "box" | "host"
pool: "192.168.1.10-19100", // TaiID node_id: "192.168.1.10-19100", // TaiID
tai_id: "tai-abc123", tai_id: "tai-abc123",
machine_id: "m-xyz", machine_id: "m-xyz",
version: "1.2.3", version: "1.2.3",
@ -371,7 +371,8 @@ Returned by `sandbox.GetNode()`, `sandbox.Nodes()`, `sandbox.NodesByTeam()`. Rea
mode: "direct", // "direct" | "tunnel" mode: "direct", // "direct" | "tunnel"
addr: "tai://192.168.1.100:19100", addr: "tai://192.168.1.100:19100",
status: "online", // "online" | "offline" | "connecting" status: "online", // "online" | "offline" | "connecting"
pool: "gpu", display_name: "GPU Node", // optional human-readable name for UI
node_id: "gpu",
connected_at: "2026-03-07T08:00:00Z", connected_at: "2026-03-07T08:00:00Z",
last_ping: "2026-03-07T10:05:00Z", last_ping: "2026-03-07T10:05:00Z",
ports: { ports: {
@ -550,7 +551,7 @@ const nodes = sandbox.Nodes()
// Find online GPU nodes // Find online GPU nodes
const gpuNodes = nodes.filter(function(n) { const gpuNodes = nodes.filter(function(n) {
return n.status === "online" && n.pool === "gpu" return n.status === "online" && n.display_name === "gpu" // n.display_name is optional label for UI
}) })
console.log("Available GPU nodes:", gpuNodes.length) console.log("Available GPU nodes:", gpuNodes.length)

View file

@ -117,7 +117,7 @@ func boxInfoToJS(v8ctx *v8go.Context, b *sandbox.BoxInfo) *v8go.Value {
data, _ := json.Marshal(map[string]interface{}{ data, _ := json.Marshal(map[string]interface{}{
"id": b.ID, "id": b.ID,
"container_id": b.ContainerID, "container_id": b.ContainerID,
"pool": b.Pool, "node_id": b.NodeID,
"owner": b.Owner, "owner": b.Owner,
"status": b.Status, "status": b.Status,
"image": b.Image, "image": b.Image,
@ -135,7 +135,7 @@ func boxInfoToJS(v8ctx *v8go.Context, b *sandbox.BoxInfo) *v8go.Value {
func computerInfoToJS(v8ctx *v8go.Context, c sandbox.ComputerInfo) *v8go.Value { func computerInfoToJS(v8ctx *v8go.Context, c sandbox.ComputerInfo) *v8go.Value {
data, _ := json.Marshal(map[string]interface{}{ data, _ := json.Marshal(map[string]interface{}{
"kind": c.Kind, "kind": c.Kind,
"pool": c.Pool, "node_id": c.NodeID,
"tai_id": c.TaiID, "tai_id": c.TaiID,
"machine_id": c.MachineID, "machine_id": c.MachineID,
"version": c.Version, "version": c.Version,
@ -161,7 +161,7 @@ func computerInfoToJS(v8ctx *v8go.Context, c sandbox.ComputerInfo) *v8go.Value {
} }
// getComputer re-fetches a Computer from the Manager by kind + identifier. // getComputer re-fetches a Computer from the Manager by kind + identifier.
// kind="box" → identifier is boxID, kind="host" → identifier is pool name. // kind="box" → identifier is boxID, kind="host" → identifier is node ID.
func getComputer(ctx context.Context, kind, identifier string) (sandbox.Computer, error) { func getComputer(ctx context.Context, kind, identifier string) (sandbox.Computer, error) {
m := sandbox.M() m := sandbox.M()
if kind == "box" { if kind == "box" {
@ -171,24 +171,24 @@ func getComputer(ctx context.Context, kind, identifier string) (sandbox.Computer
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// sbHost — sandbox.Host(pool?) // sbHost — sandbox.Host(nodeID?)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
func sbHost(info *v8go.FunctionCallbackInfo) *v8go.Value { func sbHost(info *v8go.FunctionCallbackInfo) *v8go.Value {
ctx := context.Background() ctx := context.Background()
v8ctx := info.Context() v8ctx := info.Context()
pool := "" nodeID := ""
args := info.Args() args := info.Args()
if len(args) > 0 && args[0].IsString() { if len(args) > 0 && args[0].IsString() {
pool = args[0].String() nodeID = args[0].String()
} }
if _, err := sandbox.M().Host(ctx, pool); err != nil { if _, err := sandbox.M().Host(ctx, nodeID); err != nil {
return throwError(info, err.Error()) return throwError(info, err.Error())
} }
val, err := NewComputerObject(v8ctx, "host", pool) val, err := NewComputerObject(v8ctx, "host", nodeID)
if err != nil { if err != nil {
return throwError(info, err.Error()) return throwError(info, err.Error())
} }
@ -452,20 +452,20 @@ func NewComputerObject(v8ctx *v8go.Context, kind string, identifier string) (*v8
idStr := "" idStr := ""
ownerStr := "" ownerStr := ""
poolStr := identifier nodeIDStr := identifier
if kind == "box" { if kind == "box" {
if comp, err := getComputer(ctx, kind, identifier); err == nil { if comp, err := getComputer(ctx, kind, identifier); err == nil {
box := comp.(*sandbox.Box) box := comp.(*sandbox.Box)
idStr = box.ID() idStr = box.ID()
ownerStr = box.Owner() ownerStr = box.Owner()
poolStr = box.Pool() nodeIDStr = box.NodeID()
} else { } else {
idStr = identifier idStr = identifier
} }
} }
obj.Set("id", idStr) obj.Set("id", idStr)
obj.Set("owner", ownerStr) obj.Set("owner", ownerStr)
obj.Set("pool", poolStr) obj.Set("node_id", nodeIDStr)
return obj.Value, nil return obj.Value, nil
} }

View file

@ -21,7 +21,7 @@
// sandbox.Get(id) → Manager.Get(ctx, id) → Computer (Box) // sandbox.Get(id) → Manager.Get(ctx, id) → Computer (Box)
// sandbox.List(filter?) → Manager.List(ctx, ListOptions) → BoxInfo[] // sandbox.List(filter?) → Manager.List(ctx, ListOptions) → BoxInfo[]
// sandbox.Delete(id) → Manager.Remove(ctx, id) → void // sandbox.Delete(id) → Manager.Remove(ctx, id) → void
// sandbox.Host(pool?) → Manager.Host(ctx, pool) → Computer (Host) // sandbox.Host(nodeID?) → Manager.Host(ctx, nodeID) → Computer (Host)
// sandbox.GetNode(id) → registry.Global().Get(id) → NodeInfo | null // sandbox.GetNode(id) → registry.Global().Get(id) → NodeInfo | null
// sandbox.Nodes() → registry.Global().List() → NodeInfo[] // sandbox.Nodes() → registry.Global().List() → NodeInfo[]
// sandbox.NodesByTeam(t)→ registry.Global().ListByTeam(t) → NodeInfo[] // sandbox.NodesByTeam(t)→ registry.Global().ListByTeam(t) → NodeInfo[]
@ -86,8 +86,8 @@ func sbCreate(info *v8go.FunctionCallbackInfo) *v8go.Value {
if v, ok := raw["owner"].(string); ok { if v, ok := raw["owner"].(string); ok {
opts.Owner = v opts.Owner = v
} }
if v, ok := raw["pool"].(string); ok { if v, ok := raw["node_id"].(string); ok {
opts.Pool = v opts.NodeID = v
} }
if v, ok := raw["image"].(string); ok { if v, ok := raw["image"].(string); ok {
opts.Image = v opts.Image = v
@ -221,8 +221,8 @@ func sbList(info *v8go.FunctionCallbackInfo) *v8go.Value {
if v, ok := raw["owner"].(string); ok { if v, ok := raw["owner"].(string); ok {
opts.Owner = v opts.Owner = v
} }
if v, ok := raw["pool"].(string); ok { if v, ok := raw["node_id"].(string); ok {
opts.Pool = v opts.NodeID = v
} }
if v, ok := raw["labels"].(map[string]interface{}); ok { if v, ok := raw["labels"].(map[string]interface{}); ok {
labels := make(map[string]string, len(v)) labels := make(map[string]string, len(v))
@ -250,7 +250,7 @@ func sbList(info *v8go.FunctionCallbackInfo) *v8go.Value {
items = append(items, map[string]interface{}{ items = append(items, map[string]interface{}{
"id": bi.ID, "id": bi.ID,
"container_id": bi.ContainerID, "container_id": bi.ContainerID,
"pool": bi.Pool, "node_id": bi.NodeID,
"owner": bi.Owner, "owner": bi.Owner,
"status": bi.Status, "status": bi.Status,
"image": bi.Image, "image": bi.Image,

View file

@ -103,7 +103,7 @@ func TestCreate(t *testing.T) {
setupSandbox(t, &m) setupSandbox(t, &m)
img := testImage() img := testImage()
res := runJS(t, fmt.Sprintf(`function TestCreate() { res := runJS(t, fmt.Sprintf(`function TestCreate() {
var pc = sandbox.Create({ image: "%s", owner: "test-user", pool: "%s" }); var pc = sandbox.Create({ image: "%s", owner: "test-user", node_id: "%s" });
if (pc.kind !== "box") throw new Error("kind=" + pc.kind); if (pc.kind !== "box") throw new Error("kind=" + pc.kind);
if (!pc.id) throw new Error("no id"); if (!pc.id) throw new Error("no id");
var id = pc.id; var id = pc.id;
@ -124,7 +124,7 @@ func TestGet(t *testing.T) {
setupSandbox(t, &m) setupSandbox(t, &m)
img := testImage() img := testImage()
res := runJS(t, fmt.Sprintf(`function TestGet() { res := runJS(t, fmt.Sprintf(`function TestGet() {
var pc = sandbox.Create({ image: "%s", owner: "test-user", pool: "%s" }); var pc = sandbox.Create({ image: "%s", owner: "test-user", node_id: "%s" });
var id = pc.id; var id = pc.id;
var got = sandbox.Get(id); var got = sandbox.Get(id);
if (!got) throw new Error("Get returned null"); if (!got) throw new Error("Get returned null");
@ -161,7 +161,7 @@ func TestDelete(t *testing.T) {
setupSandbox(t, &m) setupSandbox(t, &m)
img := testImage() img := testImage()
res := runJS(t, fmt.Sprintf(`function TestDelete() { res := runJS(t, fmt.Sprintf(`function TestDelete() {
var pc = sandbox.Create({ image: "%s", owner: "test-user", pool: "%s" }); var pc = sandbox.Create({ image: "%s", owner: "test-user", node_id: "%s" });
var id = pc.id; var id = pc.id;
sandbox.Delete(id); sandbox.Delete(id);
var got = sandbox.Get(id); var got = sandbox.Get(id);
@ -185,8 +185,8 @@ func TestList(t *testing.T) {
setupSandbox(t, &m) setupSandbox(t, &m)
img := testImage() img := testImage()
res := runJS(t, fmt.Sprintf(`function TestList() { res := runJS(t, fmt.Sprintf(`function TestList() {
var a = sandbox.Create({ image: "%s", owner: "list-user", pool: "%s" }); var a = sandbox.Create({ image: "%s", owner: "list-user", node_id: "%s" });
var b = sandbox.Create({ image: "%s", owner: "list-user", pool: "%s" }); var b = sandbox.Create({ image: "%s", owner: "list-user", node_id: "%s" });
var list = sandbox.List({ owner: "list-user" }); var list = sandbox.List({ owner: "list-user" });
var count = list.length; var count = list.length;
sandbox.Delete(a.id); sandbox.Delete(a.id);
@ -212,7 +212,7 @@ func TestExec(t *testing.T) {
setupSandbox(t, &m) setupSandbox(t, &m)
img := testImage() img := testImage()
res := runJS(t, fmt.Sprintf(`function TestExec() { res := runJS(t, fmt.Sprintf(`function TestExec() {
var pc = sandbox.Create({ image: "%s", owner: "test-user", pool: "%s" }); var pc = sandbox.Create({ image: "%s", owner: "test-user", node_id: "%s" });
var r = pc.Exec(["echo", "hello-jsapi"]); var r = pc.Exec(["echo", "hello-jsapi"]);
sandbox.Delete(pc.id); sandbox.Delete(pc.id);
return r.stdout; return r.stdout;
@ -232,7 +232,7 @@ func TestExecWithOptions(t *testing.T) {
setupSandbox(t, &m) setupSandbox(t, &m)
img := testImage() img := testImage()
res := runJS(t, fmt.Sprintf(`function TestExecWithOptions() { res := runJS(t, fmt.Sprintf(`function TestExecWithOptions() {
var pc = sandbox.Create({ image: "%s", owner: "test-user", pool: "%s" }); var pc = sandbox.Create({ image: "%s", owner: "test-user", node_id: "%s" });
var r = pc.Exec(["pwd"], { workdir: "/tmp" }); var r = pc.Exec(["pwd"], { workdir: "/tmp" });
sandbox.Delete(pc.id); sandbox.Delete(pc.id);
return r.stdout; return r.stdout;
@ -256,7 +256,7 @@ func TestStream(t *testing.T) {
setupSandbox(t, &m) setupSandbox(t, &m)
img := testImage() img := testImage()
res := runJS(t, fmt.Sprintf(`function TestStream() { res := runJS(t, fmt.Sprintf(`function TestStream() {
var pc = sandbox.Create({ image: "%s", owner: "test-user", pool: "%s" }); var pc = sandbox.Create({ image: "%s", owner: "test-user", node_id: "%s" });
var chunks = []; var chunks = [];
var exitCode = -1; var exitCode = -1;
pc.Stream(["echo", "streaming"], function(type, data) { pc.Stream(["echo", "streaming"], function(type, data) {
@ -285,7 +285,7 @@ func TestComputerInfo(t *testing.T) {
setupSandbox(t, &m) setupSandbox(t, &m)
img := testImage() img := testImage()
res := runJS(t, fmt.Sprintf(`function TestComputerInfo() { res := runJS(t, fmt.Sprintf(`function TestComputerInfo() {
var pc = sandbox.Create({ image: "%s", owner: "test-user", pool: "%s" }); var pc = sandbox.Create({ image: "%s", owner: "test-user", node_id: "%s" });
var info = pc.ComputerInfo(); var info = pc.ComputerInfo();
sandbox.Delete(pc.id); sandbox.Delete(pc.id);
return info.kind; return info.kind;
@ -308,7 +308,7 @@ func TestBoxInfo(t *testing.T) {
setupSandbox(t, &m) setupSandbox(t, &m)
img := testImage() img := testImage()
res := runJS(t, fmt.Sprintf(`function TestBoxInfo() { res := runJS(t, fmt.Sprintf(`function TestBoxInfo() {
var pc = sandbox.Create({ image: "%s", owner: "test-user", pool: "%s" }); var pc = sandbox.Create({ image: "%s", owner: "test-user", node_id: "%s" });
var info = pc.Info(); var info = pc.Info();
sandbox.Delete(pc.id); sandbox.Delete(pc.id);
return info.id ? "ok" : "no-id"; return info.id ? "ok" : "no-id";
@ -356,7 +356,7 @@ func TestComputerKind(t *testing.T) {
setupSandbox(t, &m) setupSandbox(t, &m)
img := testImage() img := testImage()
res := runJS(t, fmt.Sprintf(`function TestComputerKind() { res := runJS(t, fmt.Sprintf(`function TestComputerKind() {
var pc = sandbox.Create({ image: "%s", owner: "test-user", pool: "%s" }); var pc = sandbox.Create({ image: "%s", owner: "test-user", node_id: "%s" });
var k = pc.kind; var k = pc.kind;
sandbox.Delete(pc.id); sandbox.Delete(pc.id);
return k; return k;

View file

@ -83,7 +83,7 @@ func snapshotToJS(v8ctx *v8go.Context, snap *registry.NodeSnapshot) (*v8go.Value
"mode": snap.Mode, "mode": snap.Mode,
"addr": snap.Addr, "addr": snap.Addr,
"status": snap.Status, "status": snap.Status,
"pool": snap.PoolName, "display_name": snap.DisplayName,
"connected_at": snap.ConnectedAt.Format(time.RFC3339), "connected_at": snap.ConnectedAt.Format(time.RFC3339),
"last_ping": snap.LastPing.Format(time.RFC3339), "last_ping": snap.LastPing.Format(time.RFC3339),
"ports": ports, "ports": ports,
@ -117,12 +117,13 @@ func snapshotsToJSArray(v8ctx *v8go.Context, snaps []registry.NodeSnapshot) *v8g
} }
items = append(items, map[string]interface{}{ items = append(items, map[string]interface{}{
"tai_id": snap.TaiID, "tai_id": snap.TaiID,
"node_id": snap.TaiID,
"machine_id": snap.MachineID, "machine_id": snap.MachineID,
"version": snap.Version, "version": snap.Version,
"mode": snap.Mode, "mode": snap.Mode,
"addr": snap.Addr, "addr": snap.Addr,
"status": snap.Status, "status": snap.Status,
"pool": snap.PoolName, "display_name": snap.DisplayName,
"connected_at": snap.ConnectedAt.Format(time.RFC3339), "connected_at": snap.ConnectedAt.Format(time.RFC3339),
"last_ping": snap.LastPing.Format(time.RFC3339), "last_ping": snap.LastPing.Format(time.RFC3339),
"ports": ports, "ports": ports,

View file

@ -32,7 +32,7 @@ func (m *Manager) Start(ctx context.Context) error {
} }
for _, snap := range reg.List() { for _, snap := range reg.List() {
client, err := m.getPool(snap.TaiID) client, err := m.getNode(snap.TaiID)
if err != nil { if err != nil {
continue continue
} }
@ -45,8 +45,8 @@ func (m *Manager) Start(ctx context.Context) error {
return nil return nil
} }
// Pools returns the list of registered Tai nodes from the registry. // Nodes returns the list of registered Tai nodes from the registry.
func (m *Manager) Pools() []registry.NodeSnapshot { func (m *Manager) Nodes() []registry.NodeSnapshot {
reg := registry.Global() reg := registry.Global()
if reg == nil { if reg == nil {
return nil return nil
@ -69,21 +69,21 @@ func (m *Manager) Heartbeat(sandboxID string, active bool, processCount int) err
} }
// Host returns a Host handle for executing commands on the Tai host machine. // Host returns a Host handle for executing commands on the Tai host machine.
func (m *Manager) Host(_ context.Context, pool string) (*Host, error) { func (m *Manager) Host(_ context.Context, nodeID string) (*Host, error) {
if pool == "" { if nodeID == "" {
return nil, ErrPoolMissing return nil, ErrNodeMissing
} }
client, err := m.getPool(pool) client, err := m.getNode(nodeID)
if err != nil { if err != nil {
return nil, fmt.Errorf("sandbox: connect pool %q: %w", pool, err) return nil, fmt.Errorf("sandbox: connect node %q: %w", nodeID, err)
} }
if client.HostExec() == nil { if client.HostExec() == nil {
return nil, fmt.Errorf("sandbox: pool %q has no host_exec capability", pool) return nil, fmt.Errorf("sandbox: node %q has no host_exec capability", nodeID)
} }
return &Host{pool: pool, manager: m}, nil return &Host{nodeID: nodeID, manager: m}, nil
} }
// Create creates and starts a new sandbox. // Create creates and starts a new sandbox.
@ -92,7 +92,7 @@ func (m *Manager) Create(ctx context.Context, opts CreateOptions) (*Box, error)
return nil, fmt.Errorf("sandbox: image is required") return nil, fmt.Errorf("sandbox: image is required")
} }
poolName := opts.Pool nodeID := opts.NodeID
if opts.WorkspaceID != "" { if opts.WorkspaceID != "" {
if wsm := workspace.M(); wsm != nil { if wsm := workspace.M(); wsm != nil {
@ -100,12 +100,12 @@ func (m *Manager) Create(ctx context.Context, opts CreateOptions) (*Box, error)
if err != nil { if err != nil {
return nil, fmt.Errorf("sandbox: resolve workspace %q: %w", opts.WorkspaceID, err) return nil, fmt.Errorf("sandbox: resolve workspace %q: %w", opts.WorkspaceID, err)
} }
poolName = node nodeID = node
} }
} }
if poolName == "" { if nodeID == "" {
return nil, ErrPoolMissing return nil, ErrNodeMissing
} }
id := opts.ID id := opts.ID
@ -113,16 +113,16 @@ func (m *Manager) Create(ctx context.Context, opts CreateOptions) (*Box, error)
id = fmt.Sprintf("sb-%d", time.Now().UnixNano()) id = fmt.Sprintf("sb-%d", time.Now().UnixNano())
} }
client, err := m.getPool(poolName) client, err := m.getNode(nodeID)
if err != nil { if err != nil {
return nil, fmt.Errorf("sandbox: connect pool %q: %w", poolName, err) return nil, fmt.Errorf("sandbox: connect node %q: %w", nodeID, err)
} }
if client.Sandbox() == nil { if client.Sandbox() == nil {
return nil, fmt.Errorf("sandbox: pool %q has no container runtime", poolName) return nil, fmt.Errorf("sandbox: node %q has no container runtime", nodeID)
} }
taiOpts := m.buildTaiCreateOptions(opts, poolName, id) taiOpts := m.buildTaiCreateOptions(opts, nodeID, id)
containerID, err := client.Sandbox().Create(ctx, taiOpts) containerID, err := client.Sandbox().Create(ctx, taiOpts)
if err != nil { if err != nil {
@ -142,7 +142,7 @@ func (m *Manager) Create(ctx context.Context, opts CreateOptions) (*Box, error)
box := &Box{ box := &Box{
id: id, id: id,
containerID: containerID, containerID: containerID,
pool: poolName, nodeID: nodeID,
owner: opts.Owner, owner: opts.Owner,
policy: policy, policy: policy,
labels: opts.Labels, labels: opts.Labels,
@ -188,7 +188,7 @@ func (m *Manager) List(_ context.Context, opts ListOptions) ([]*Box, error) {
if opts.Owner != "" && b.owner != opts.Owner { if opts.Owner != "" && b.owner != opts.Owner {
return true return true
} }
if opts.Pool != "" && b.pool != opts.Pool { if opts.NodeID != "" && b.nodeID != opts.NodeID {
return true return true
} }
if len(opts.Labels) > 0 { if len(opts.Labels) > 0 {
@ -212,7 +212,7 @@ func (m *Manager) Remove(ctx context.Context, id string) error {
} }
b := v.(*Box) b := v.(*Box)
client, err := m.getPool(b.pool) client, err := m.getNode(b.nodeID)
if err == nil && client.Sandbox() != nil { if err == nil && client.Sandbox() != nil {
client.Sandbox().Remove(ctx, b.containerID, true) client.Sandbox().Remove(ctx, b.containerID, true)
} }
@ -237,7 +237,7 @@ func (m *Manager) Cleanup(ctx context.Context) error {
} }
case LongRunning: case LongRunning:
if timeout := b.idleTimeout(); timeout > 0 && idle > timeout { if timeout := b.idleTimeout(); timeout > 0 && idle > timeout {
if client, err := m.getPool(b.pool); err == nil && client.Sandbox() != nil { if client, err := m.getNode(b.nodeID); err == nil && client.Sandbox() != nil {
client.Sandbox().Stop(ctx, b.containerID, b.stopTimeout()) client.Sandbox().Stop(ctx, b.containerID, b.stopTimeout())
} }
} }
@ -273,20 +273,20 @@ func (m *Manager) cleanupLoop(ctx context.Context) {
} }
} }
func (m *Manager) getPool(name string) (*tai.Client, error) { func (m *Manager) getNode(name string) (*tai.Client, error) {
client, ok := tai.GetClient(name) client, ok := tai.GetClient(name)
if !ok { if !ok {
return nil, ErrPoolNotFound return nil, ErrNodeNotFound
} }
return client, nil return client, nil
} }
func (m *Manager) buildTaiCreateOptions(opts CreateOptions, poolName, sandboxID string) taisandbox.CreateOptions { func (m *Manager) buildTaiCreateOptions(opts CreateOptions, nodeID, sandboxID string) taisandbox.CreateOptions {
env := make(map[string]string) env := make(map[string]string)
reg := registry.Global() reg := registry.Global()
if reg != nil { if reg != nil {
if snap, ok := reg.Get(poolName); ok { if snap, ok := reg.Get(nodeID); ok {
grpcEnv := BuildGRPCEnv(snap.Mode, snap.Addr, sandboxID) grpcEnv := BuildGRPCEnv(snap.Mode, snap.Addr, sandboxID)
for k, v := range grpcEnv { for k, v := range grpcEnv {
env[k] = v env[k] = v
@ -302,7 +302,7 @@ func (m *Manager) buildTaiCreateOptions(opts CreateOptions, poolName, sandboxID
"managed-by": "yao-sandbox", "managed-by": "yao-sandbox",
"sandbox-id": sandboxID, "sandbox-id": sandboxID,
"sandbox-owner": opts.Owner, "sandbox-owner": opts.Owner,
"sandbox-pool": poolName, "sandbox-node-id": nodeID,
"sandbox-policy": string(opts.Policy), "sandbox-policy": string(opts.Policy),
} }
if opts.WorkspaceID != "" { if opts.WorkspaceID != "" {
@ -363,7 +363,7 @@ func (m *Manager) buildTaiCreateOptions(opts CreateOptions, poolName, sandboxID
} }
} }
func (m *Manager) recoverBoxes(ctx context.Context, poolName string, client *tai.Client) { func (m *Manager) recoverBoxes(ctx context.Context, nodeID string, client *tai.Client) {
if client.Sandbox() == nil { if client.Sandbox() == nil {
return return
} }
@ -391,7 +391,7 @@ func (m *Manager) recoverBoxes(ctx context.Context, poolName string, client *tai
box := &Box{ box := &Box{
id: sandboxID, id: sandboxID,
containerID: cid, containerID: cid,
pool: c.Labels["sandbox-pool"], nodeID: c.Labels["sandbox-node-id"],
owner: c.Labels["sandbox-owner"], owner: c.Labels["sandbox-owner"],
policy: LifecyclePolicy(c.Labels["sandbox-policy"]), policy: LifecyclePolicy(c.Labels["sandbox-policy"]),
labels: c.Labels, labels: c.Labels,
@ -405,9 +405,9 @@ func (m *Manager) recoverBoxes(ctx context.Context, poolName string, client *tai
} }
} }
// ImageExists reports whether the given image ref exists on the target pool node. // ImageExists reports whether the given image ref exists on the target node.
func (m *Manager) ImageExists(ctx context.Context, pool, ref string) (bool, error) { func (m *Manager) ImageExists(ctx context.Context, nodeID, ref string) (bool, error) {
client, err := m.getPool(pool) client, err := m.getNode(nodeID)
if err != nil { if err != nil {
return false, err return false, err
} }
@ -418,10 +418,10 @@ func (m *Manager) ImageExists(ctx context.Context, pool, ref string) (bool, erro
return img.Exists(ctx, ref) return img.Exists(ctx, ref)
} }
// PullImage pulls an image to the target pool node, returning a channel of // PullImage pulls an image to the target node, returning a channel of
// real-time progress events. // real-time progress events.
func (m *Manager) PullImage(ctx context.Context, pool, ref string, opts ImagePullOptions) (<-chan taisandbox.PullProgress, error) { func (m *Manager) PullImage(ctx context.Context, nodeID, ref string, opts ImagePullOptions) (<-chan taisandbox.PullProgress, error) {
client, err := m.getPool(pool) client, err := m.getNode(nodeID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -440,10 +440,10 @@ func (m *Manager) PullImage(ctx context.Context, pool, ref string, opts ImagePul
return img.Pull(ctx, ref, pullOpts) return img.Pull(ctx, ref, pullOpts)
} }
// EnsureImage checks whether the image exists on the pool node; if not, it // EnsureImage checks whether the image exists on the node; if not, it
// pulls the image and blocks until the pull completes. // pulls the image and blocks until the pull completes.
func (m *Manager) EnsureImage(ctx context.Context, pool, ref string, opts ImagePullOptions) error { func (m *Manager) EnsureImage(ctx context.Context, nodeID, ref string, opts ImagePullOptions) error {
exists, err := m.ImageExists(ctx, pool, ref) exists, err := m.ImageExists(ctx, nodeID, ref)
if err != nil { if err != nil {
return fmt.Errorf("image exists check: %w", err) return fmt.Errorf("image exists check: %w", err)
} }
@ -451,7 +451,7 @@ func (m *Manager) EnsureImage(ctx context.Context, pool, ref string, opts ImageP
return nil return nil
} }
ch, err := m.PullImage(ctx, pool, ref, opts) ch, err := m.PullImage(ctx, nodeID, ref, opts)
if err != nil { if err != nil {
return fmt.Errorf("image pull: %w", err) return fmt.Errorf("image pull: %w", err)
} }

View file

@ -11,10 +11,10 @@ import (
func TestHeartbeatUpdates(t *testing.T) { func TestHeartbeatUpdates(t *testing.T) {
skipIfNoDocker(t) skipIfNoDocker(t)
for _, pc := range testPools() { for _, pc := range testNodes() {
pc := pc pc := pc
t.Run(pc.Name, func(t *testing.T) { t.Run(pc.Name, func(t *testing.T) {
m := setupManagerForPool(t, &pc) m := setupManagerForNode(t, &pc)
box := createTestBox(t, m, pc) box := createTestBox(t, m, pc)
err := m.Heartbeat(box.ID(), true, 5) err := m.Heartbeat(box.ID(), true, 5)
@ -34,10 +34,10 @@ func TestHeartbeatUpdates(t *testing.T) {
} }
func TestHeartbeatUnknownBox(t *testing.T) { func TestHeartbeatUnknownBox(t *testing.T) {
for _, pc := range testPools() { for _, pc := range testNodes() {
pc := pc pc := pc
t.Run(pc.Name, func(t *testing.T) { t.Run(pc.Name, func(t *testing.T) {
m := setupManagerForPool(t, &pc) m := setupManagerForNode(t, &pc)
err := m.Heartbeat("nonexistent", true, 1) err := m.Heartbeat("nonexistent", true, 1)
if err != sandbox.ErrNotFound { if err != sandbox.ErrNotFound {
t.Errorf("err = %v, want ErrNotFound", err) t.Errorf("err = %v, want ErrNotFound", err)
@ -49,17 +49,17 @@ func TestHeartbeatUnknownBox(t *testing.T) {
func TestIdleCleanup(t *testing.T) { func TestIdleCleanup(t *testing.T) {
skipIfNoDocker(t) skipIfNoDocker(t)
for _, pc := range testPools() { for _, pc := range testNodes() {
pc := pc pc := pc
t.Run(pc.Name, func(t *testing.T) { t.Run(pc.Name, func(t *testing.T) {
m := setupManagerForPool(t, &pc) m := setupManagerForNode(t, &pc)
ensureTestImage(t, m, pc.TaiID) ensureTestImage(t, m, pc.TaiID)
ctx := context.Background() ctx := context.Background()
box, err := m.Create(ctx, sandbox.CreateOptions{ box, err := m.Create(ctx, sandbox.CreateOptions{
Image: testImage(), Image: testImage(),
Owner: "test-user", Owner: "test-user",
Pool: pc.TaiID, NodeID: pc.TaiID,
Policy: sandbox.Session, Policy: sandbox.Session,
IdleTimeout: 1 * time.Second, IdleTimeout: 1 * time.Second,
}) })
@ -85,10 +85,10 @@ func TestIdleCleanup(t *testing.T) {
func TestStartRecovery(t *testing.T) { func TestStartRecovery(t *testing.T) {
skipIfNoDocker(t) skipIfNoDocker(t)
for _, pc := range testPools() { for _, pc := range testNodes() {
pc := pc pc := pc
t.Run(pc.Name, func(t *testing.T) { t.Run(pc.Name, func(t *testing.T) {
m1 := setupManagerForPool(t, &pc) m1 := setupManagerForNode(t, &pc)
box := createTestBox(t, m1, pc) box := createTestBox(t, m1, pc)
boxID := box.ID() boxID := box.ID()
@ -117,10 +117,10 @@ func TestStartRecovery(t *testing.T) {
func TestPersistentNotCleaned(t *testing.T) { func TestPersistentNotCleaned(t *testing.T) {
skipIfNoDocker(t) skipIfNoDocker(t)
for _, pc := range testPools() { for _, pc := range testNodes() {
pc := pc pc := pc
t.Run(pc.Name, func(t *testing.T) { t.Run(pc.Name, func(t *testing.T) {
m := setupManagerForPool(t, &pc) m := setupManagerForNode(t, &pc)
box := createTestBox(t, m, pc, func(co *sandbox.CreateOptions) { box := createTestBox(t, m, pc, func(co *sandbox.CreateOptions) {
co.Policy = sandbox.Persistent co.Policy = sandbox.Persistent

View file

@ -11,10 +11,10 @@ import (
func TestCreateAndExec(t *testing.T) { func TestCreateAndExec(t *testing.T) {
skipIfNoDocker(t) skipIfNoDocker(t)
for _, pc := range testPools() { for _, pc := range testNodes() {
pc := pc pc := pc
t.Run(pc.Name, func(t *testing.T) { t.Run(pc.Name, func(t *testing.T) {
m := setupManagerForPool(t, &pc) m := setupManagerForNode(t, &pc)
box := createTestBox(t, m, pc) box := createTestBox(t, m, pc)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
@ -37,10 +37,10 @@ func TestCreateAndExec(t *testing.T) {
func TestCreateWithLabels(t *testing.T) { func TestCreateWithLabels(t *testing.T) {
skipIfNoDocker(t) skipIfNoDocker(t)
for _, pc := range testPools() { for _, pc := range testNodes() {
pc := pc pc := pc
t.Run(pc.Name, func(t *testing.T) { t.Run(pc.Name, func(t *testing.T) {
m := setupManagerForPool(t, &pc) m := setupManagerForNode(t, &pc)
box := createTestBox(t, m, pc, func(co *sandbox.CreateOptions) { box := createTestBox(t, m, pc, func(co *sandbox.CreateOptions) {
co.Labels = map[string]string{"app": "test-app"} co.Labels = map[string]string{"app": "test-app"}
}) })
@ -60,10 +60,10 @@ func TestCreateWithLabels(t *testing.T) {
func TestGet(t *testing.T) { func TestGet(t *testing.T) {
skipIfNoDocker(t) skipIfNoDocker(t)
for _, pc := range testPools() { for _, pc := range testNodes() {
pc := pc pc := pc
t.Run(pc.Name, func(t *testing.T) { t.Run(pc.Name, func(t *testing.T) {
m := setupManagerForPool(t, &pc) m := setupManagerForNode(t, &pc)
box := createTestBox(t, m, pc) box := createTestBox(t, m, pc)
got, err := m.Get(context.Background(), box.ID()) got, err := m.Get(context.Background(), box.ID())
@ -78,10 +78,10 @@ func TestGet(t *testing.T) {
} }
func TestGetNotFound(t *testing.T) { func TestGetNotFound(t *testing.T) {
for _, pc := range testPools() { for _, pc := range testNodes() {
pc := pc pc := pc
t.Run(pc.Name, func(t *testing.T) { t.Run(pc.Name, func(t *testing.T) {
m := setupManagerForPool(t, &pc) m := setupManagerForNode(t, &pc)
_, err := m.Get(context.Background(), "nonexistent") _, err := m.Get(context.Background(), "nonexistent")
if err != sandbox.ErrNotFound { if err != sandbox.ErrNotFound {
t.Errorf("err = %v, want ErrNotFound", err) t.Errorf("err = %v, want ErrNotFound", err)
@ -93,10 +93,10 @@ func TestGetNotFound(t *testing.T) {
func TestList(t *testing.T) { func TestList(t *testing.T) {
skipIfNoDocker(t) skipIfNoDocker(t)
for _, pc := range testPools() { for _, pc := range testNodes() {
pc := pc pc := pc
t.Run(pc.Name, func(t *testing.T) { t.Run(pc.Name, func(t *testing.T) {
m := setupManagerForPool(t, &pc) m := setupManagerForNode(t, &pc)
box := createTestBox(t, m, pc, func(co *sandbox.CreateOptions) { box := createTestBox(t, m, pc, func(co *sandbox.CreateOptions) {
co.Owner = "user-list" co.Owner = "user-list"
}) })
@ -129,16 +129,16 @@ func TestList(t *testing.T) {
func TestRemove(t *testing.T) { func TestRemove(t *testing.T) {
skipIfNoDocker(t) skipIfNoDocker(t)
for _, pc := range testPools() { for _, pc := range testNodes() {
pc := pc pc := pc
t.Run(pc.Name, func(t *testing.T) { t.Run(pc.Name, func(t *testing.T) {
m := setupManagerForPool(t, &pc) m := setupManagerForNode(t, &pc)
ensureTestImage(t, m, pc.TaiID) ensureTestImage(t, m, pc.TaiID)
ctx := context.Background() ctx := context.Background()
box, err := m.Create(ctx, sandbox.CreateOptions{ box, err := m.Create(ctx, sandbox.CreateOptions{
Image: testImage(), Image: testImage(),
Owner: "test-user", Owner: "test-user",
Pool: pc.TaiID, NodeID: pc.TaiID,
}) })
if err != nil { if err != nil {
t.Fatalf("Create: %v", err) t.Fatalf("Create: %v", err)
@ -157,38 +157,38 @@ func TestRemove(t *testing.T) {
} }
func TestCreateNoImage(t *testing.T) { func TestCreateNoImage(t *testing.T) {
m, pools := setupManager(t, poolConfig{Name: "local", Addr: testLocalAddr()}) m, nodes := setupManager(t, nodeConfig{Name: "local", Addr: testLocalAddr()})
_, err := m.Create(context.Background(), sandbox.CreateOptions{ _, err := m.Create(context.Background(), sandbox.CreateOptions{
Owner: "test", Owner: "test",
Pool: pools[0].TaiID, NodeID: nodes[0].TaiID,
}) })
if err == nil { if err == nil {
t.Error("expected error for missing image") t.Error("expected error for missing image")
} }
} }
func TestCreateNoPool(t *testing.T) { func TestCreateNoNodeID(t *testing.T) {
m, _ := setupManager(t, poolConfig{Name: "local", Addr: testLocalAddr()}) m, _ := setupManager(t, nodeConfig{Name: "local", Addr: testLocalAddr()})
_, err := m.Create(context.Background(), sandbox.CreateOptions{ _, err := m.Create(context.Background(), sandbox.CreateOptions{
Image: testImage(), Image: testImage(),
}) })
if err != sandbox.ErrPoolMissing { if err != sandbox.ErrNodeMissing {
t.Errorf("err = %v, want ErrPoolMissing", err) t.Errorf("err = %v, want ErrNodeMissing", err)
} }
} }
func TestMultiPool(t *testing.T) { func TestMultiNode(t *testing.T) {
skipIfNoDocker(t) skipIfNoDocker(t)
skipIfNoTai(t) skipIfNoTai(t)
pools := testPools() nodes := testNodes()
if len(pools) < 2 { if len(nodes) < 2 {
t.Skip("need at least 2 pools (local + remote) for multi-pool test") t.Skip("need at least 2 nodes (local + remote) for multi-node test")
} }
m, registered := setupManager(t, pools...) m, registered := setupManager(t, nodes...)
for _, pc := range registered { for _, pc := range registered {
ensureTestImage(t, m, pc.TaiID) ensureTestImage(t, m, pc.TaiID)
@ -200,7 +200,7 @@ func TestMultiPool(t *testing.T) {
localBox, err := m.Create(ctx, sandbox.CreateOptions{ localBox, err := m.Create(ctx, sandbox.CreateOptions{
Image: testImage(), Image: testImage(),
Owner: "test-user", Owner: "test-user",
Pool: registered[0].TaiID, NodeID: registered[0].TaiID,
}) })
if err != nil { if err != nil {
t.Fatalf("Create on local: %v", err) t.Fatalf("Create on local: %v", err)
@ -210,7 +210,7 @@ func TestMultiPool(t *testing.T) {
remoteBox, err := m.Create(ctx, sandbox.CreateOptions{ remoteBox, err := m.Create(ctx, sandbox.CreateOptions{
Image: testImage(), Image: testImage(),
Owner: "test-user", Owner: "test-user",
Pool: registered[1].TaiID, NodeID: registered[1].TaiID,
}) })
if err != nil { if err != nil {
t.Fatalf("Create on remote: %v", err) t.Fatalf("Create on remote: %v", err)

View file

@ -31,7 +31,7 @@ func TestMain(m *testing.M) {
} }
// purgeStaleContainers removes leftover sb-* containers/pods from previous // purgeStaleContainers removes leftover sb-* containers/pods from previous
// test runs across all configured pools (Docker + K8s). // test runs across all configured nodes (Docker + K8s).
func purgeStaleContainers() { func purgeStaleContainers() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel() defer cancel()
@ -98,30 +98,30 @@ func purgeStaleContainers() {
} }
} }
type poolConfig struct { type nodeConfig struct {
Name string // human-readable label for t.Run (e.g. "remote", "k8s") Name string // human-readable label for t.Run (e.g. "remote", "k8s")
Addr string Addr string
TaiID string // actual registry key, filled after tai.New TaiID string // actual registry key, filled after tai.New
Options []tai.Option Options []tai.Option
} }
// testPools returns all available pool configurations for multi-mode testing. // testNodes returns all available node configurations for multi-mode testing.
func testPools() []poolConfig { func testNodes() []nodeConfig {
pools := []poolConfig{ nodes := []nodeConfig{
{Name: "local", Addr: testLocalAddr()}, {Name: "local", Addr: testLocalAddr()},
} }
if addr := os.Getenv("SANDBOX_TEST_REMOTE_ADDR"); addr != "" { if addr := os.Getenv("SANDBOX_TEST_REMOTE_ADDR"); addr != "" {
pools = append(pools, poolConfig{Name: "remote", Addr: addr}) nodes = append(nodes, nodeConfig{Name: "remote", Addr: addr})
} }
if host := os.Getenv("TAI_TEST_CONTAINERIZED_HOST"); host != "" { if host := os.Getenv("TAI_TEST_CONTAINERIZED_HOST"); host != "" {
grpcPort := envPort("TAI_TEST_CONTAINERIZED_GRPC_PORT", 9200) grpcPort := envPort("TAI_TEST_CONTAINERIZED_GRPC_PORT", 9200)
addr := fmt.Sprintf("tai://%s:%d", host, grpcPort) addr := fmt.Sprintf("tai://%s:%d", host, grpcPort)
pools = append(pools, poolConfig{Name: "containerized", Addr: addr}) nodes = append(nodes, nodeConfig{Name: "containerized", Addr: addr})
} }
if host := os.Getenv("TAI_TEST_K8S_HOST"); host != "" { if host := os.Getenv("TAI_TEST_K8S_HOST"); host != "" {
kubeconfig := os.Getenv("TAI_TEST_KUBECONFIG") kubeconfig := os.Getenv("TAI_TEST_KUBECONFIG")
if kubeconfig == "" { if kubeconfig == "" {
return pools return nodes
} }
grpcPort := envPort("TAI_TEST_K8S_GRPC_PORT", envPort("TAI_TEST_GRPC_PORT", 19100)) grpcPort := envPort("TAI_TEST_K8S_GRPC_PORT", envPort("TAI_TEST_GRPC_PORT", 19100))
addr := fmt.Sprintf("tai://%s:%d", host, grpcPort) addr := fmt.Sprintf("tai://%s:%d", host, grpcPort)
@ -136,9 +136,9 @@ func testPools() []poolConfig {
if ns := os.Getenv("TAI_TEST_K8S_NAMESPACE"); ns != "" { if ns := os.Getenv("TAI_TEST_K8S_NAMESPACE"); ns != "" {
opts = append(opts, tai.WithNamespace(ns)) opts = append(opts, tai.WithNamespace(ns))
} }
pools = append(pools, poolConfig{Name: "k8s", Addr: addr, Options: opts}) nodes = append(nodes, nodeConfig{Name: "k8s", Addr: addr, Options: opts})
} }
return pools return nodes
} }
func skipIfNoDocker(t *testing.T) { func skipIfNoDocker(t *testing.T) {
@ -237,9 +237,9 @@ func envPort(key string, fallback int) int {
return fallback return fallback
} }
// registerPool creates a tai.Client and registers it in the global registry. // registerNode creates a tai.Client and registers it in the global registry.
// It fills pc.TaiID with the actual registry key returned by tai.New. // It fills pc.TaiID with the actual registry key returned by tai.New.
func registerPool(t *testing.T, pc *poolConfig) { func registerNode(t *testing.T, pc *nodeConfig) {
t.Helper() t.Helper()
reg := registry.Global() reg := registry.Global()
@ -255,7 +255,7 @@ func registerPool(t *testing.T, pc *poolConfig) {
t.Cleanup(func() { client.Close() }) t.Cleanup(func() { client.Close() })
} }
func setupManager(t *testing.T, pools ...poolConfig) (*sandbox.Manager, []poolConfig) { func setupManager(t *testing.T, nodes ...nodeConfig) (*sandbox.Manager, []nodeConfig) {
t.Helper() t.Helper()
reg := registry.Global() reg := registry.Global()
@ -264,8 +264,8 @@ func setupManager(t *testing.T, pools ...poolConfig) (*sandbox.Manager, []poolCo
} }
_ = reg _ = reg
out := make([]poolConfig, len(pools)) out := make([]nodeConfig, len(nodes))
copy(out, pools) copy(out, nodes)
for i := range out { for i := range out {
client, err := tai.New(out[i].Addr, out[i].Options...) client, err := tai.New(out[i].Addr, out[i].Options...)
if err != nil { if err != nil {
@ -280,7 +280,7 @@ func setupManager(t *testing.T, pools ...poolConfig) (*sandbox.Manager, []poolCo
return m, out return m, out
} }
func setupManagerForPool(t *testing.T, pc *poolConfig) *sandbox.Manager { func setupManagerForNode(t *testing.T, pc *nodeConfig) *sandbox.Manager {
t.Helper() t.Helper()
m, registered := setupManager(t, *pc) m, registered := setupManager(t, *pc)
*pc = registered[0] *pc = registered[0]
@ -289,38 +289,38 @@ func setupManagerForPool(t *testing.T, pc *poolConfig) *sandbox.Manager {
// setupManagerWithWorkspace creates a sandbox Manager and returns // setupManagerWithWorkspace creates a sandbox Manager and returns
// the global workspace.Manager (which uses the registry for client lookups). // the global workspace.Manager (which uses the registry for client lookups).
func setupManagerWithWorkspace(t *testing.T, pc *poolConfig) (*sandbox.Manager, *workspace.Manager) { func setupManagerWithWorkspace(t *testing.T, pc *nodeConfig) (*sandbox.Manager, *workspace.Manager) {
t.Helper() t.Helper()
sbm := setupManagerForPool(t, pc) sbm := setupManagerForNode(t, pc)
return sbm, workspace.M() return sbm, workspace.M()
} }
func ensureTestImage(t *testing.T, m *sandbox.Manager, pool string) { func ensureTestImage(t *testing.T, m *sandbox.Manager, nodeID string) {
t.Helper() t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
defer cancel() defer cancel()
if err := m.EnsureImage(ctx, pool, testImage(), sandbox.ImagePullOptions{}); err != nil { if err := m.EnsureImage(ctx, nodeID, testImage(), sandbox.ImagePullOptions{}); err != nil {
t.Fatalf("EnsureImage(%s, %s): %v", pool, testImage(), err) t.Fatalf("EnsureImage(%s, %s): %v", nodeID, testImage(), err)
} }
} }
func createTestBox(t *testing.T, m *sandbox.Manager, pc poolConfig, opts ...func(*sandbox.CreateOptions)) *sandbox.Box { func createTestBox(t *testing.T, m *sandbox.Manager, pc nodeConfig, opts ...func(*sandbox.CreateOptions)) *sandbox.Box {
t.Helper() t.Helper()
co := sandbox.CreateOptions{ co := sandbox.CreateOptions{
Image: testImage(), Image: testImage(),
Owner: "test-user", Owner: "test-user",
Pool: pc.TaiID, NodeID: pc.TaiID,
} }
for _, fn := range opts { for _, fn := range opts {
fn(&co) fn(&co)
} }
pool := co.Pool nodeID := co.NodeID
if pool == "" { if nodeID == "" {
pools := m.Pools() nodes := m.Nodes()
if len(pools) > 0 { if len(nodes) > 0 {
pool = pools[0].TaiID nodeID = nodes[0].TaiID
co.Pool = pool co.NodeID = nodeID
} }
} }
@ -332,12 +332,12 @@ func createTestBox(t *testing.T, m *sandbox.Manager, pc poolConfig, opts ...func
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel() defer cancel()
if pool != "" { if nodeID != "" {
if err := m.EnsureImage(ctx, pool, co.Image, sandbox.ImagePullOptions{}); err != nil { if err := m.EnsureImage(ctx, nodeID, co.Image, sandbox.ImagePullOptions{}); err != nil {
if isK8s { if isK8s {
<-k8sSem <-k8sSem
} }
t.Fatalf("EnsureImage(%s, %s): %v", pool, co.Image, err) t.Fatalf("EnsureImage(%s, %s): %v", nodeID, co.Image, err)
} }
} }

View file

@ -27,7 +27,7 @@ type Computer interface {
// ComputerInfo holds identity and registry information for a Computer. // ComputerInfo holds identity and registry information for a Computer.
type ComputerInfo struct { type ComputerInfo struct {
Kind string // "box" | "host" Kind string // "box" | "host"
Pool string NodeID string
TaiID string TaiID string
MachineID string MachineID string
Version string Version string
@ -84,7 +84,7 @@ type CreateOptions struct {
ID string ID string
Owner string Owner string
Labels map[string]string Labels map[string]string
Pool string NodeID string
Image string Image string
WorkDir string WorkDir string
User string User string
@ -105,7 +105,7 @@ type CreateOptions struct {
type ListOptions struct { type ListOptions struct {
Owner string Owner string
Pool string NodeID string
Labels map[string]string Labels map[string]string
} }
@ -211,7 +211,7 @@ type ServiceConn struct {
type BoxInfo struct { type BoxInfo struct {
ID string ID string
ContainerID string ContainerID string
Pool string NodeID string
Owner string Owner string
Status string Status string
Policy LifecyclePolicy Policy LifecyclePolicy

View file

@ -30,7 +30,7 @@ type TaiNode struct {
Status string // "online" | "offline" | "connecting" Status string // "online" | "offline" | "connecting"
ConnectedAt time.Time ConnectedAt time.Time
LastPing time.Time LastPing time.Time
PoolName string DisplayName string
} }
``` ```
@ -48,7 +48,7 @@ type NodeSnapshot struct {
Capabilities map[string]bool Capabilities map[string]bool
Status string Status string
ConnectedAt, LastPing time.Time ConnectedAt, LastPing time.Time
PoolName string DisplayName string
} }
``` ```

View file

@ -42,7 +42,7 @@ type TaiNode struct {
Status string // "online" | "offline" | "connecting" Status string // "online" | "offline" | "connecting"
ConnectedAt time.Time ConnectedAt time.Time
LastPing time.Time LastPing time.Time
PoolName string DisplayName string // optional human-readable name for UI
client any // *tai.Client; stored as any to avoid import cycle client any // *tai.Client; stored as any to avoid import cycle
@ -64,7 +64,7 @@ type NodeSnapshot struct {
Status string Status string
ConnectedAt time.Time ConnectedAt time.Time
LastPing time.Time LastPing time.Time
PoolName string DisplayName string
client any client any
} }
@ -83,7 +83,7 @@ func (n *TaiNode) snapshot() NodeSnapshot {
Mode: n.Mode, Addr: n.Addr, YaoBase: n.YaoBase, Mode: n.Mode, Addr: n.Addr, YaoBase: n.YaoBase,
Ports: ports, Capabilities: caps, Ports: ports, Capabilities: caps,
Status: n.Status, ConnectedAt: n.ConnectedAt, LastPing: n.LastPing, Status: n.Status, ConnectedAt: n.ConnectedAt, LastPing: n.LastPing,
PoolName: n.PoolName, DisplayName: n.DisplayName,
client: n.client, client: n.client,
} }
} }