Enhance test utility for multi-mode configurations
- Update the `testPools` function to include a new "containerized" pool configuration, allowing for testing with Tai running in a containerized environment. - Revise comments to clarify the conditions under which each pool configuration is available, improving documentation for future reference. These changes improve the flexibility of the testing framework by accommodating additional deployment scenarios for the Tai service.
This commit is contained in:
parent
5b54bb2429
commit
c797fcf74d
7 changed files with 294 additions and 3 deletions
52
sandbox/v2/jsapi/box.go
Normal file
52
sandbox/v2/jsapi/box.go
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
package jsapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"rogchap.com/v8go"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewBoxObject creates a JS Box object with the following methods:
|
||||||
|
//
|
||||||
|
// box.ID() → string // sandbox ID
|
||||||
|
// box.Owner() → string // owner
|
||||||
|
// box.ContainerID() → string // underlying container/pod ID
|
||||||
|
// box.Pool() → string // pool name
|
||||||
|
// box.WorkspaceID() → string // mounted workspace ID (empty if none)
|
||||||
|
//
|
||||||
|
// box.Exec(cmd, options?) → ExecResult // run command, wait for completion
|
||||||
|
// cmd: string[] // command + args
|
||||||
|
// options: { workdir, env, timeout }
|
||||||
|
// returns: { exit_code: number, stdout: string, stderr: string }
|
||||||
|
//
|
||||||
|
// box.Stream(cmd, options?) → ExecStream // streaming I/O
|
||||||
|
// returns: { stdout: ReadableStream, stderr: ReadableStream,
|
||||||
|
// stdin: WritableStream, wait: ()=>number, cancel: ()=>void }
|
||||||
|
//
|
||||||
|
// box.Attach(port, options?) → ServiceConn // WebSocket/SSE attach
|
||||||
|
// port: number // container port
|
||||||
|
// options: { protocol, path, headers }
|
||||||
|
// returns: { url: string, close: ()=>void }
|
||||||
|
//
|
||||||
|
// box.VNC() → string // VNC WebSocket URL
|
||||||
|
// box.Proxy(port, path?) → string // HTTP proxy URL
|
||||||
|
//
|
||||||
|
// box.Workspace() → WorkspaceFS // workspace file system
|
||||||
|
// returns WorkspaceFS object (see workspace/jsapi)
|
||||||
|
//
|
||||||
|
// box.Info() → BoxInfo // container status
|
||||||
|
// returns: { id, container_id, pool, owner, status, policy,
|
||||||
|
// labels, image, created_at, last_active, process_count, vnc }
|
||||||
|
//
|
||||||
|
// box.Start() → void // start stopped box
|
||||||
|
// box.Stop() → void // stop running box
|
||||||
|
// box.Remove() → void // remove box permanently
|
||||||
|
// box.Release() → void // release JS bridge ref
|
||||||
|
func NewBoxObject(v8ctx *v8go.Context /* , box *sandbox.Box */) (*v8go.Value, error) {
|
||||||
|
// TODO: Phase 2 implementation
|
||||||
|
// 1. Create ObjectTemplate with InternalFieldCount(1)
|
||||||
|
// 2. Register box in bridge
|
||||||
|
// 3. Bind property accessors: ID, Owner, ContainerID, Pool, WorkspaceID
|
||||||
|
// 4. Bind methods: Exec, Stream, Attach, VNC, Proxy, Workspace,
|
||||||
|
// Info, Start, Stop, Remove, Release
|
||||||
|
// 5. Create instance, set internal field
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
50
sandbox/v2/jsapi/jsapi.go
Normal file
50
sandbox/v2/jsapi/jsapi.go
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
// Package jsapi registers the Sandbox() constructor into the Yao V8 runtime.
|
||||||
|
//
|
||||||
|
// # JavaScript API
|
||||||
|
//
|
||||||
|
// const sb = new Sandbox({ pool: "default", image: "node:20", owner: "user1" })
|
||||||
|
// const box = sb.Create({ workdir: "/app", env: { NODE_ENV: "dev" } })
|
||||||
|
// const result = box.Exec(["node", "-e", "console.log('hi')"])
|
||||||
|
// box.Remove()
|
||||||
|
//
|
||||||
|
// The constructor returns a SandboxManager object; Create/GetOrCreate returns
|
||||||
|
// a Box object with Exec/Stream/Attach/VNC/Proxy/Workspace/Info/Stop/Start/Remove.
|
||||||
|
//
|
||||||
|
// Registration happens via init() — import with:
|
||||||
|
//
|
||||||
|
// _ "github.com/yaoapp/yao/sandbox/v2/jsapi"
|
||||||
|
package jsapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
v8 "github.com/yaoapp/gou/runtime/v8"
|
||||||
|
"rogchap.com/v8go"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
v8.RegisterFunction("Sandbox", ExportFunction)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExportFunction exports the Sandbox constructor to V8.
|
||||||
|
func ExportFunction(iso *v8go.Isolate) *v8go.FunctionTemplate {
|
||||||
|
return v8go.NewFunctionTemplate(iso, sandboxConstructor)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sandboxConstructor is called when JS executes `new Sandbox(options)`.
|
||||||
|
//
|
||||||
|
// Options:
|
||||||
|
//
|
||||||
|
// {
|
||||||
|
// pool: string // pool name (required)
|
||||||
|
// image: string // container image (required)
|
||||||
|
// owner: string // owner ID (required)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Returns a SandboxManager JS object.
|
||||||
|
func sandboxConstructor(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
||||||
|
// TODO: Phase 2 implementation
|
||||||
|
// 1. Parse options from args[0]
|
||||||
|
// 2. Validate required fields (pool, image, owner)
|
||||||
|
// 3. Get sandbox.M() singleton
|
||||||
|
// 4. Return NewManagerObject(v8ctx, manager, options)
|
||||||
|
return v8go.Undefined(info.Context().Isolate())
|
||||||
|
}
|
||||||
45
sandbox/v2/jsapi/manager.go
Normal file
45
sandbox/v2/jsapi/manager.go
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
package jsapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"rogchap.com/v8go"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewManagerObject creates a JS SandboxManager object with the following methods:
|
||||||
|
//
|
||||||
|
// manager.Create(options?) → Box // create a new sandbox box
|
||||||
|
// manager.GetOrCreate(opts) → Box // get existing or create
|
||||||
|
// manager.Get(id) → Box|null // get by sandbox ID
|
||||||
|
// manager.List(options?) → Box[] // list boxes
|
||||||
|
// manager.Remove(id) → void // remove a box
|
||||||
|
// manager.EnsureImage(ref) → void // pull image if missing
|
||||||
|
// manager.ImageExists(ref) → boolean // check image presence
|
||||||
|
// manager.Pools() → PoolInfo[] // list pool info
|
||||||
|
// manager.Release() → void // release JS bridge ref
|
||||||
|
//
|
||||||
|
// Create options (merged with constructor defaults):
|
||||||
|
//
|
||||||
|
// {
|
||||||
|
// id: string // explicit sandbox ID (optional)
|
||||||
|
// workdir: string // container working directory
|
||||||
|
// user: string // container user (e.g. "1000:1000")
|
||||||
|
// env: object // environment variables
|
||||||
|
// memory: number // memory limit in bytes
|
||||||
|
// cpus: number // CPU limit (e.g. 1.5)
|
||||||
|
// vnc: boolean // enable VNC
|
||||||
|
// ports: array // port mappings [{container: 8080, host: 0}]
|
||||||
|
// policy: string // "oneshot"|"session"|"longrunning"|"persistent"
|
||||||
|
// idle_timeout: number // idle timeout in ms
|
||||||
|
// stop_timeout: number // stop timeout in ms
|
||||||
|
// workspace_id: string // workspace to mount
|
||||||
|
// mount_mode: string // "rw"|"ro"
|
||||||
|
// mount_path: string // mount target in container
|
||||||
|
// }
|
||||||
|
func NewManagerObject(v8ctx *v8go.Context /* manager *sandbox.Manager, defaults CreateDefaults */) (*v8go.Value, error) {
|
||||||
|
// TODO: Phase 2 implementation
|
||||||
|
// 1. Create ObjectTemplate with InternalFieldCount(1)
|
||||||
|
// 2. Register manager in bridge
|
||||||
|
// 3. Bind methods: Create, GetOrCreate, Get, List, Remove,
|
||||||
|
// EnsureImage, ImageExists, Pools, Release
|
||||||
|
// 4. Create instance, set internal field
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
@ -22,8 +22,9 @@ type poolConfig struct {
|
||||||
|
|
||||||
// testPools returns all available pool configurations for multi-mode testing.
|
// testPools returns all available pool configurations for multi-mode testing.
|
||||||
// - local: always present (direct Docker daemon)
|
// - local: always present (direct Docker daemon)
|
||||||
// - remote: when SANDBOX_TEST_REMOTE_ADDR is set (Tai proxy → Docker)
|
// - remote: when SANDBOX_TEST_REMOTE_ADDR is set (Tai on host → Docker)
|
||||||
// - k8s: when TAI_TEST_K8S_HOST + TAI_TEST_KUBECONFIG are set (Tai proxy → K8s)
|
// - containerized: when TAI_TEST_CONTAINERIZED_HOST is set (Tai in container → Docker)
|
||||||
|
// - k8s: when TAI_TEST_K8S_HOST + TAI_TEST_KUBECONFIG are set (Tai → K8s)
|
||||||
func testPools() []poolConfig {
|
func testPools() []poolConfig {
|
||||||
pools := []poolConfig{
|
pools := []poolConfig{
|
||||||
{Name: "local", Addr: testLocalAddr()},
|
{Name: "local", Addr: testLocalAddr()},
|
||||||
|
|
@ -31,6 +32,13 @@ func testPools() []poolConfig {
|
||||||
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})
|
pools = append(pools, poolConfig{Name: "remote", Addr: addr})
|
||||||
}
|
}
|
||||||
|
if host := os.Getenv("TAI_TEST_CONTAINERIZED_HOST"); host != "" {
|
||||||
|
grpcPort := envPort("TAI_TEST_CONTAINERIZED_GRPC_PORT", 9200)
|
||||||
|
addr := fmt.Sprintf("tai://%s:%d", host, grpcPort)
|
||||||
|
// No WithPorts for HTTP/VNC — Tai self-inspects its container
|
||||||
|
// and returns host-mapped ports via ServerInfo automatically.
|
||||||
|
pools = append(pools, poolConfig{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 == "" {
|
||||||
|
|
|
||||||
36
workspace/jsapi/fs.go
Normal file
36
workspace/jsapi/fs.go
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
package jsapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"rogchap.com/v8go"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewFSObject creates a JS WorkspaceFS object implementing a file system interface.
|
||||||
|
//
|
||||||
|
// This is the object returned by both:
|
||||||
|
// - WorkspaceManager.FS(id) (standalone workspace access)
|
||||||
|
// - Box.Workspace() (sandbox-mounted workspace access)
|
||||||
|
//
|
||||||
|
// Methods:
|
||||||
|
//
|
||||||
|
// fs.ReadFile(path) → string // read UTF-8 content
|
||||||
|
// fs.ReadFileBytes(path) → ArrayBuffer // read binary content
|
||||||
|
// fs.WriteFile(path, data) → void // write string or ArrayBuffer
|
||||||
|
// fs.Stat(path) → FileInfo // file metadata
|
||||||
|
// returns: { name, size, mode, mod_time, is_dir }
|
||||||
|
// fs.ReadDir(path?) → DirEntry[] // list directory (default ".")
|
||||||
|
// returns: [{ name, is_dir, size }]
|
||||||
|
// fs.MkdirAll(path) → void // create directory tree
|
||||||
|
// fs.Remove(path) → void // remove single file/empty dir
|
||||||
|
// fs.RemoveAll(path) → void // remove recursively
|
||||||
|
// fs.Rename(from, to) → void // rename/move
|
||||||
|
// fs.Close() → void // close FS handle
|
||||||
|
// fs.Release() → void // release JS bridge ref
|
||||||
|
func NewFSObject(v8ctx *v8go.Context /* , wfs taiworkspace.FS */) (*v8go.Value, error) {
|
||||||
|
// TODO: Phase 2 implementation
|
||||||
|
// 1. Create ObjectTemplate with InternalFieldCount(1)
|
||||||
|
// 2. Register FS in bridge
|
||||||
|
// 3. Bind methods: ReadFile, ReadFileBytes, WriteFile,
|
||||||
|
// Stat, ReadDir, MkdirAll, Remove, RemoveAll, Rename, Close, Release
|
||||||
|
// 4. Create instance, set internal field
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
47
workspace/jsapi/jsapi.go
Normal file
47
workspace/jsapi/jsapi.go
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
// Package jsapi registers the Workspace() constructor into the Yao V8 runtime.
|
||||||
|
//
|
||||||
|
// # JavaScript API
|
||||||
|
//
|
||||||
|
// const ws = new Workspace({ node: "tai-1" })
|
||||||
|
// const info = ws.Create({ name: "my-project", owner: "user1" })
|
||||||
|
// const file = ws.ReadFile(info.id, "/README.md")
|
||||||
|
// ws.WriteFile(info.id, "/app.ts", content)
|
||||||
|
//
|
||||||
|
// The constructor returns a WorkspaceManager object; individual workspace
|
||||||
|
// files are accessed through ReadFile/WriteFile/ListDir or the FS() handle.
|
||||||
|
//
|
||||||
|
// Registration happens via init() — import with:
|
||||||
|
//
|
||||||
|
// _ "github.com/yaoapp/yao/workspace/jsapi"
|
||||||
|
package jsapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
v8 "github.com/yaoapp/gou/runtime/v8"
|
||||||
|
"rogchap.com/v8go"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
v8.RegisterFunction("Workspace", ExportFunction)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExportFunction exports the Workspace constructor to V8.
|
||||||
|
func ExportFunction(iso *v8go.Isolate) *v8go.FunctionTemplate {
|
||||||
|
return v8go.NewFunctionTemplate(iso, workspaceConstructor)
|
||||||
|
}
|
||||||
|
|
||||||
|
// workspaceConstructor is called when JS executes `new Workspace(options?)`.
|
||||||
|
//
|
||||||
|
// Options (all optional — uses global workspace.Manager if omitted):
|
||||||
|
//
|
||||||
|
// {
|
||||||
|
// node: string // default target node for Create (optional)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Returns a WorkspaceManager JS object.
|
||||||
|
func workspaceConstructor(info *v8go.FunctionCallbackInfo) *v8go.Value {
|
||||||
|
// TODO: Phase 2 implementation
|
||||||
|
// 1. Parse optional options from args[0]
|
||||||
|
// 2. Get workspace manager instance
|
||||||
|
// 3. Return NewManagerObject(v8ctx, manager, defaults)
|
||||||
|
return v8go.Undefined(info.Context().Isolate())
|
||||||
|
}
|
||||||
53
workspace/jsapi/manager.go
Normal file
53
workspace/jsapi/manager.go
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
package jsapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"rogchap.com/v8go"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewManagerObject creates a JS WorkspaceManager object with the following methods:
|
||||||
|
//
|
||||||
|
// wm.Create(options) → WorkspaceInfo // create workspace
|
||||||
|
// options: {
|
||||||
|
// id: string // explicit ID (optional, auto uuid)
|
||||||
|
// name: string // display name (required)
|
||||||
|
// owner: string // owner user ID (required)
|
||||||
|
// node: string // target Tai node (required, or use constructor default)
|
||||||
|
// labels: object // metadata key-value pairs
|
||||||
|
// }
|
||||||
|
// returns: { id, name, owner, node, labels, created_at, updated_at }
|
||||||
|
//
|
||||||
|
// wm.Get(id) → WorkspaceInfo|null
|
||||||
|
// wm.List(options?) → WorkspaceInfo[]
|
||||||
|
// options: { owner: string, node: string }
|
||||||
|
//
|
||||||
|
// wm.Update(id, options) → WorkspaceInfo
|
||||||
|
// options: { name: string, labels: object }
|
||||||
|
//
|
||||||
|
// wm.Delete(id, force?) → void
|
||||||
|
// force: boolean // delete even if has active mounts
|
||||||
|
//
|
||||||
|
// wm.ReadFile(id, path) → string // read file content (UTF-8)
|
||||||
|
// wm.ReadFileBytes(id, path) → ArrayBuffer // read file content (binary)
|
||||||
|
// wm.WriteFile(id, path, data) → void // write file (string or ArrayBuffer)
|
||||||
|
// wm.ListDir(id, path?) → DirEntry[] // list directory
|
||||||
|
// returns: [{ name, is_dir, size }]
|
||||||
|
// wm.Remove(id, path) → void // remove file or dir
|
||||||
|
// wm.MkdirAll(id, path) → void // create directory tree
|
||||||
|
// wm.Rename(id, from, to) → void // rename/move file
|
||||||
|
//
|
||||||
|
// wm.FS(id) → WorkspaceFS // get full FS handle
|
||||||
|
// wm.MountPath(id) → string // host mount path
|
||||||
|
// wm.Nodes() → NodeInfo[] // list available nodes
|
||||||
|
// returns: [{ name, addr, online }]
|
||||||
|
//
|
||||||
|
// wm.Release() → void // release JS bridge ref
|
||||||
|
func NewManagerObject(v8ctx *v8go.Context /* , manager *workspace.Manager, defaults ManagerDefaults */) (*v8go.Value, error) {
|
||||||
|
// TODO: Phase 2 implementation
|
||||||
|
// 1. Create ObjectTemplate with InternalFieldCount(1)
|
||||||
|
// 2. Register manager in bridge
|
||||||
|
// 3. Bind methods: Create, Get, List, Update, Delete,
|
||||||
|
// ReadFile, ReadFileBytes, WriteFile, ListDir, Remove,
|
||||||
|
// MkdirAll, Rename, FS, MountPath, Nodes, Release
|
||||||
|
// 4. Create instance, set internal field
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue