Skip to content

Commit 49f6c6b

Browse files
authored
Merge pull request opensandbox-group#515 from perhapzz/fix/js-sdk-ossfs-volume-support
feat(sdk/js): add OSSFS volume backend support
2 parents 17b1689 + e26d200 commit 49f6c6b

4 files changed

Lines changed: 146 additions & 6 deletions

File tree

sdks/sandbox/javascript/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export type {
3939
NetworkPolicy,
4040
NetworkRule,
4141
NetworkRuleAction,
42+
OSSFS,
4243
PVC,
4344
RenewSandboxExpirationRequest,
4445
RenewSandboxExpirationResponse,

sdks/sandbox/javascript/src/models/sandboxes.ts

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,12 +92,51 @@ export interface PVC extends Record<string, unknown> {
9292
claimName: string;
9393
}
9494

95+
/**
96+
* Alibaba Cloud OSS mount backend via ossfs.
97+
*
98+
* The runtime mounts a host-side OSS path under `storage.ossfs_mount_root`
99+
* so the container sees the bucket contents at the specified mount path.
100+
*
101+
* In Docker runtime, OSSFS backend requires OpenSandbox Server to run on a Linux host with FUSE support.
102+
*/
103+
export interface OSSFS extends Record<string, unknown> {
104+
/**
105+
* OSS bucket name.
106+
*/
107+
bucket: string;
108+
/**
109+
* OSS endpoint (e.g., "oss-cn-hangzhou.aliyuncs.com").
110+
*/
111+
endpoint: string;
112+
/**
113+
* ossfs major version used by runtime mount integration.
114+
* @default "2.0"
115+
*/
116+
version?: "1.0" | "2.0";
117+
/**
118+
* Additional ossfs mount options.
119+
*
120+
* - `1.0`: mounts with `ossfs ... -o <option>`
121+
* - `2.0`: mounts with `ossfs2 mount ... -c <config-file>` and encodes options as `--<option>` lines in the config file
122+
*/
123+
options?: string[];
124+
/**
125+
* OSS access key ID for inline credentials mode.
126+
*/
127+
accessKeyId: string;
128+
/**
129+
* OSS access key secret for inline credentials mode.
130+
*/
131+
accessKeySecret: string;
132+
}
133+
95134
/**
96135
* Storage mount definition for a sandbox.
97136
*
98137
* Each volume entry contains:
99138
* - A unique name identifier
100-
* - Exactly one backend (host, pvc) with backend-specific fields
139+
* - Exactly one backend (host, pvc, ossfs) with backend-specific fields
101140
* - Common mount settings (mountPath, readOnly, subPath)
102141
*/
103142
export interface Volume extends Record<string, unknown> {
@@ -106,13 +145,17 @@ export interface Volume extends Record<string, unknown> {
106145
*/
107146
name: string;
108147
/**
109-
* Host path bind mount backend (mutually exclusive with pvc).
148+
* Host path bind mount backend (mutually exclusive with pvc, ossfs).
110149
*/
111150
host?: Host;
112151
/**
113-
* Kubernetes PVC mount backend (mutually exclusive with host).
152+
* Kubernetes PVC mount backend (mutually exclusive with host, ossfs).
114153
*/
115154
pvc?: PVC;
155+
/**
156+
* Alibaba Cloud OSSFS mount backend (mutually exclusive with host, pvc).
157+
*/
158+
ossfs?: OSSFS;
116159
/**
117160
* Absolute path inside the container where the volume is mounted.
118161
*/

sdks/sandbox/javascript/src/sandbox.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -246,15 +246,15 @@ export class Sandbox {
246246
// Validate volumes: exactly one backend must be specified per volume
247247
if (opts.volumes) {
248248
for (const vol of opts.volumes) {
249-
const backendsSpecified = [vol.host, vol.pvc].filter((b) => b !== undefined).length;
249+
const backendsSpecified = [vol.host, vol.pvc, vol.ossfs].filter((b) => b != null).length;
250250
if (backendsSpecified === 0) {
251251
throw new Error(
252-
`Volume '${vol.name}' must specify exactly one backend (host, pvc), but none was provided.`
252+
`Volume '${vol.name}' must specify exactly one backend (host, pvc, ossfs), but none was provided.`
253253
);
254254
}
255255
if (backendsSpecified > 1) {
256256
throw new Error(
257-
`Volume '${vol.name}' must specify exactly one backend (host, pvc), but multiple were provided.`
257+
`Volume '${vol.name}' must specify exactly one backend (host, pvc, ossfs), but multiple were provided.`
258258
);
259259
}
260260
}

sdks/sandbox/javascript/tests/sandbox.create.test.mjs

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,3 +143,99 @@ test("Sandbox creates and reuses egress service during sandbox lifecycle", async
143143
assert.equal(egressStackCalls[0].egressBaseUrl, `http://127.0.0.1:${DEFAULT_EGRESS_PORT}`);
144144
assert.deepEqual(egressStackCalls[0].endpointHeaders, { "x-port": String(DEFAULT_EGRESS_PORT) });
145145
});
146+
147+
test("Sandbox.create passes OSSFS volume to request", async () => {
148+
const { adapterFactory, recordedRequests } = createAdapterFactory();
149+
150+
await Sandbox.create({
151+
adapterFactory,
152+
connectionConfig: { domain: "http://127.0.0.1:8080" },
153+
image: "python:3.12",
154+
skipHealthCheck: true,
155+
volumes: [
156+
{
157+
name: "oss-data",
158+
ossfs: {
159+
bucket: "my-bucket",
160+
endpoint: "oss-cn-hangzhou.aliyuncs.com",
161+
version: "2.0",
162+
accessKeyId: "ak-id",
163+
accessKeySecret: "ak-secret",
164+
},
165+
mountPath: "/data",
166+
readOnly: false,
167+
},
168+
],
169+
});
170+
171+
assert.equal(recordedRequests.length, 1);
172+
assert.equal(recordedRequests[0].volumes.length, 1);
173+
assert.equal(recordedRequests[0].volumes[0].name, "oss-data");
174+
assert.equal(recordedRequests[0].volumes[0].ossfs.bucket, "my-bucket");
175+
assert.equal(recordedRequests[0].volumes[0].ossfs.endpoint, "oss-cn-hangzhou.aliyuncs.com");
176+
});
177+
178+
test("Sandbox.create rejects volume with no backend", async () => {
179+
const { adapterFactory } = createAdapterFactory();
180+
181+
await assert.rejects(
182+
Sandbox.create({
183+
adapterFactory,
184+
connectionConfig: { domain: "http://127.0.0.1:8080" },
185+
image: "python:3.12",
186+
skipHealthCheck: true,
187+
volumes: [{ name: "empty", mountPath: "/mnt" }],
188+
}),
189+
/must specify exactly one backend \(host, pvc, ossfs\)/
190+
);
191+
});
192+
193+
test("Sandbox.create rejects volume with multiple backends", async () => {
194+
const { adapterFactory } = createAdapterFactory();
195+
196+
await assert.rejects(
197+
Sandbox.create({
198+
adapterFactory,
199+
connectionConfig: { domain: "http://127.0.0.1:8080" },
200+
image: "python:3.12",
201+
skipHealthCheck: true,
202+
volumes: [
203+
{
204+
name: "conflicting",
205+
host: { path: "/tmp" },
206+
ossfs: {
207+
bucket: "b",
208+
endpoint: "e",
209+
accessKeyId: "id",
210+
accessKeySecret: "secret",
211+
},
212+
mountPath: "/mnt",
213+
},
214+
],
215+
}),
216+
/must specify exactly one backend \(host, pvc, ossfs\)/
217+
);
218+
});
219+
220+
test("Sandbox.create treats null backends as absent", async () => {
221+
const { adapterFactory, recordedRequests } = createAdapterFactory();
222+
223+
await Sandbox.create({
224+
adapterFactory,
225+
connectionConfig: { domain: "http://127.0.0.1:8080" },
226+
image: "python:3.12",
227+
skipHealthCheck: true,
228+
volumes: [
229+
{
230+
name: "host-with-null-ossfs",
231+
host: { path: "/tmp" },
232+
ossfs: null,
233+
pvc: undefined,
234+
mountPath: "/mnt",
235+
},
236+
],
237+
});
238+
239+
assert.equal(recordedRequests.length, 1);
240+
assert.equal(recordedRequests[0].volumes[0].host.path, "/tmp");
241+
});

0 commit comments

Comments
 (0)