Skip to content

Commit 490e1d1

Browse files
authored
feat(sdks): ossfs support (opensandbox-group#563)
1 parent 49f6c6b commit 490e1d1

9 files changed

Lines changed: 393 additions & 10 deletions

File tree

sdks/sandbox/csharp/src/OpenSandbox/Models/Sandboxes.cs

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,9 +137,51 @@ public class PVC
137137
public required string ClaimName { get; set; }
138138
}
139139

140+
/// <summary>
141+
/// Alibaba Cloud OSS mount backend via ossfs.
142+
/// </summary>
143+
public class OSSFS
144+
{
145+
/// <summary>
146+
/// Gets or sets the OSS bucket name.
147+
/// </summary>
148+
[JsonPropertyName("bucket")]
149+
public required string Bucket { get; set; }
150+
151+
/// <summary>
152+
/// Gets or sets the OSS endpoint.
153+
/// </summary>
154+
[JsonPropertyName("endpoint")]
155+
public required string Endpoint { get; set; }
156+
157+
/// <summary>
158+
/// Gets or sets the OSS access key ID for inline credentials mode.
159+
/// </summary>
160+
[JsonPropertyName("accessKeyId")]
161+
public required string AccessKeyId { get; set; }
162+
163+
/// <summary>
164+
/// Gets or sets the OSS access key secret for inline credentials mode.
165+
/// </summary>
166+
[JsonPropertyName("accessKeySecret")]
167+
public required string AccessKeySecret { get; set; }
168+
169+
/// <summary>
170+
/// Gets or sets the ossfs major version used by runtime mount integration. Defaults to "2.0".
171+
/// </summary>
172+
[JsonPropertyName("version")]
173+
public string Version { get; set; } = "2.0";
174+
175+
/// <summary>
176+
/// Gets or sets additional ossfs mount options.
177+
/// </summary>
178+
[JsonPropertyName("options")]
179+
public IReadOnlyList<string>? Options { get; set; }
180+
}
181+
140182
/// <summary>
141183
/// Storage mount definition for sandbox creation.
142-
/// Exactly one backend (Host or PVC) should be provided per volume.
184+
/// Exactly one backend (Host, PVC, or OSSFS) should be provided per volume.
143185
/// </summary>
144186
public class Volume
145187
{
@@ -161,6 +203,12 @@ public class Volume
161203
[JsonPropertyName("pvc")]
162204
public PVC? Pvc { get; set; }
163205

206+
/// <summary>
207+
/// Gets or sets the OSSFS backend configuration.
208+
/// </summary>
209+
[JsonPropertyName("ossfs")]
210+
public OSSFS? Ossfs { get; set; }
211+
164212
/// <summary>
165213
/// Gets or sets the absolute mount path inside the container.
166214
/// </summary>

sdks/sandbox/csharp/tests/OpenSandbox.Tests/ModelsTests.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
using System.Text.Json;
1516
using FluentAssertions;
1617
using OpenSandbox.Models;
1718
using Xunit;
@@ -208,6 +209,43 @@ public void NetworkPolicy_ShouldStoreRules()
208209
policy.Egress[0].Target.Should().Be("example.com");
209210
}
210211

212+
[Fact]
213+
public void Volume_WithOssfs_ShouldSerializeExpectedPayload()
214+
{
215+
var request = new CreateSandboxRequest
216+
{
217+
Image = new ImageSpec { Uri = "python:3.11" },
218+
ResourceLimits = new Dictionary<string, string>(),
219+
Entrypoint = new List<string> { "python" },
220+
Volumes = new List<Volume>
221+
{
222+
new()
223+
{
224+
Name = "oss-data",
225+
MountPath = "/mnt/oss",
226+
SubPath = "prefix",
227+
Ossfs = new OSSFS
228+
{
229+
Bucket = "bucket-a",
230+
Endpoint = "oss-cn-hangzhou.aliyuncs.com",
231+
AccessKeyId = "ak",
232+
AccessKeySecret = "sk",
233+
Options = new List<string> { "allow_other" }
234+
}
235+
}
236+
}
237+
};
238+
239+
string json = JsonSerializer.Serialize(request);
240+
241+
json.Should().Contain("\"ossfs\":");
242+
json.Should().Contain("\"bucket\":\"bucket-a\"");
243+
json.Should().Contain("\"endpoint\":\"oss-cn-hangzhou.aliyuncs.com\"");
244+
json.Should().Contain("\"accessKeyId\":\"ak\"");
245+
json.Should().Contain("\"accessKeySecret\":\"sk\"");
246+
json.Should().Contain("\"version\":\"2.0\"");
247+
}
248+
211249
[Fact]
212250
public void SandboxMetrics_ShouldStoreProperties()
213251
{

sdks/sandbox/javascript/README.md

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,32 @@ const { endpoint } = await sandbox.getEndpoint(44772);
165165
const url = await sandbox.getEndpointUrl(44772);
166166
```
167167

168-
### 6. Sandbox Management (Admin)
168+
### 6. Volume Mounts
169+
170+
`volumes` supports `host`, `pvc`, and `ossfs` backends. Each volume must specify exactly one backend.
171+
172+
```ts
173+
const sandbox = await Sandbox.create({
174+
connectionConfig: config,
175+
image: "ubuntu",
176+
volumes: [
177+
{
178+
name: "oss-data",
179+
ossfs: {
180+
bucket: "bucket-a",
181+
endpoint: "oss-cn-hangzhou.aliyuncs.com",
182+
accessKeyId: process.env.OSS_ACCESS_KEY_ID!,
183+
accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET!,
184+
version: "2.0",
185+
},
186+
mountPath: "/mnt/oss",
187+
subPath: "prefix",
188+
},
189+
],
190+
});
191+
```
192+
193+
### 7. Sandbox Management (Admin)
169194

170195
Use `SandboxManager` for administrative tasks and finding existing sandboxes.
171196

sdks/sandbox/javascript/README_zh.md

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,32 @@ const { endpoint } = await sandbox.getEndpoint(44772);
166166
const url = await sandbox.getEndpointUrl(44772);
167167
```
168168

169-
### 6. 沙箱管理(Admin)
169+
### 6. Volume 挂载
170+
171+
`volumes` 现在支持 `host``pvc``ossfs` 三种 backend。每个 volume 必须且只能指定其中一种。
172+
173+
```ts
174+
const sandbox = await Sandbox.create({
175+
connectionConfig: config,
176+
image: "ubuntu",
177+
volumes: [
178+
{
179+
name: "oss-data",
180+
ossfs: {
181+
bucket: "bucket-a",
182+
endpoint: "oss-cn-hangzhou.aliyuncs.com",
183+
accessKeyId: process.env.OSS_ACCESS_KEY_ID!,
184+
accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET!,
185+
version: "2.0",
186+
},
187+
mountPath: "/mnt/oss",
188+
subPath: "prefix",
189+
},
190+
],
191+
});
192+
```
193+
194+
### 7. 沙箱管理(Admin)
170195

171196
使用 `SandboxManager` 进行管理操作,如查询现有沙箱列表。
172197

sdks/sandbox/javascript/src/sandbox.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ export interface SandboxCreateOptions {
7979
networkPolicy?: NetworkPolicy;
8080
/**
8181
* Optional list of volume mounts for persistent storage.
82-
* Each volume specifies a backend (host path or PVC) and mount configuration.
82+
* Each volume specifies a backend (host path, PVC, or OSSFS) and mount configuration.
8383
*/
8484
volumes?: Volume[];
8585
/**

sdks/sandbox/kotlin/sandbox/src/main/kotlin/com/alibaba/opensandbox/sandbox/domain/models/sandboxes/SandboxModels.kt

Lines changed: 113 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -386,12 +386,106 @@ class PVC private constructor(
386386
}
387387
}
388388

389+
/**
390+
* Alibaba Cloud OSS mount backend via ossfs.
391+
*
392+
* @property bucket OSS bucket name
393+
* @property endpoint OSS endpoint (for example, `oss-cn-hangzhou.aliyuncs.com`)
394+
* @property accessKeyId OSS access key ID for inline credentials mode
395+
* @property accessKeySecret OSS access key secret for inline credentials mode
396+
* @property version ossfs major version used by runtime mount integration
397+
* @property options Additional ossfs mount options
398+
*/
399+
class OSSFS private constructor(
400+
val bucket: String,
401+
val endpoint: String,
402+
val accessKeyId: String,
403+
val accessKeySecret: String,
404+
val version: String,
405+
val options: List<String>?,
406+
) {
407+
companion object {
408+
const val VERSION_1_0 = "1.0"
409+
const val VERSION_2_0 = "2.0"
410+
411+
@JvmStatic
412+
fun builder(): Builder = Builder()
413+
}
414+
415+
class Builder {
416+
private var bucket: String? = null
417+
private var endpoint: String? = null
418+
private var accessKeyId: String? = null
419+
private var accessKeySecret: String? = null
420+
private var version: String = VERSION_2_0
421+
private var options: List<String>? = null
422+
423+
fun bucket(bucket: String): Builder {
424+
require(bucket.isNotBlank()) { "Bucket cannot be blank" }
425+
this.bucket = bucket
426+
return this
427+
}
428+
429+
fun endpoint(endpoint: String): Builder {
430+
require(endpoint.isNotBlank()) { "Endpoint cannot be blank" }
431+
this.endpoint = endpoint
432+
return this
433+
}
434+
435+
fun accessKeyId(accessKeyId: String): Builder {
436+
require(accessKeyId.isNotBlank()) { "Access key ID cannot be blank" }
437+
this.accessKeyId = accessKeyId
438+
return this
439+
}
440+
441+
fun accessKeySecret(accessKeySecret: String): Builder {
442+
require(accessKeySecret.isNotBlank()) { "Access key secret cannot be blank" }
443+
this.accessKeySecret = accessKeySecret
444+
return this
445+
}
446+
447+
fun version(version: String): Builder {
448+
require(version == VERSION_1_0 || version == VERSION_2_0) {
449+
"OSSFS version must be one of: 1.0, 2.0"
450+
}
451+
this.version = version
452+
return this
453+
}
454+
455+
fun options(options: List<String>?): Builder {
456+
this.options = options
457+
return this
458+
}
459+
460+
fun options(vararg options: String): Builder {
461+
this.options = options.toList()
462+
return this
463+
}
464+
465+
fun build(): OSSFS {
466+
val bucketValue = bucket ?: throw IllegalArgumentException("Bucket must be specified")
467+
val endpointValue = endpoint ?: throw IllegalArgumentException("Endpoint must be specified")
468+
val accessKeyIdValue = accessKeyId ?: throw IllegalArgumentException("Access key ID must be specified")
469+
val accessKeySecretValue =
470+
accessKeySecret ?: throw IllegalArgumentException("Access key secret must be specified")
471+
return OSSFS(
472+
bucket = bucketValue,
473+
endpoint = endpointValue,
474+
accessKeyId = accessKeyIdValue,
475+
accessKeySecret = accessKeySecretValue,
476+
version = version,
477+
options = options,
478+
)
479+
}
480+
}
481+
}
482+
389483
/**
390484
* Storage mount definition for a sandbox.
391485
*
392486
* Each volume entry contains:
393487
* - A unique name identifier
394-
* - Exactly one backend (host, pvc) with backend-specific fields
488+
* - Exactly one backend (host, pvc, ossfs) with backend-specific fields
395489
* - Common mount settings (mountPath, readOnly, subPath)
396490
*
397491
* Example usage:
@@ -413,8 +507,9 @@ class PVC private constructor(
413507
* ```
414508
*
415509
* @property name Unique identifier for the volume within the sandbox
416-
* @property host Host path bind mount backend (mutually exclusive with pvc)
417-
* @property pvc Kubernetes PVC mount backend (mutually exclusive with host)
510+
* @property host Host path bind mount backend (mutually exclusive with pvc/ossfs)
511+
* @property pvc Kubernetes PVC mount backend (mutually exclusive with host/ossfs)
512+
* @property ossfs OSSFS mount backend (mutually exclusive with host/pvc)
418513
* @property mountPath Absolute path inside the container where the volume is mounted
419514
* @property readOnly If true, the volume is mounted as read-only. Defaults to false (read-write).
420515
* @property subPath Optional subdirectory under the backend path to mount
@@ -423,6 +518,7 @@ class Volume private constructor(
423518
val name: String,
424519
val host: Host?,
425520
val pvc: PVC?,
521+
val ossfs: OSSFS?,
426522
val mountPath: String,
427523
val readOnly: Boolean,
428524
val subPath: String?,
@@ -436,6 +532,7 @@ class Volume private constructor(
436532
private var name: String? = null
437533
private var host: Host? = null
438534
private var pvc: PVC? = null
535+
private var ossfs: OSSFS? = null
439536
private var mountPath: String? = null
440537
private var readOnly: Boolean = false
441538
private var subPath: String? = null
@@ -456,6 +553,11 @@ class Volume private constructor(
456553
return this
457554
}
458555

556+
fun ossfs(ossfs: OSSFS): Builder {
557+
this.ossfs = ossfs
558+
return this
559+
}
560+
459561
fun mountPath(mountPath: String): Builder {
460562
require(mountPath.startsWith("/")) { "Mount path must be an absolute path starting with '/'" }
461563
this.mountPath = mountPath
@@ -477,18 +579,23 @@ class Volume private constructor(
477579
val mountPathValue = mountPath ?: throw IllegalArgumentException("Mount path must be specified")
478580

479581
// Validate exactly one backend is specified
480-
val backendsSpecified = listOfNotNull(host, pvc).size
582+
val backendsSpecified = listOfNotNull(host, pvc, ossfs).size
481583
if (backendsSpecified == 0) {
482-
throw IllegalArgumentException("Exactly one backend (host, pvc) must be specified, but none was provided")
584+
throw IllegalArgumentException(
585+
"Exactly one backend (host, pvc, ossfs) must be specified, but none was provided",
586+
)
483587
}
484588
if (backendsSpecified > 1) {
485-
throw IllegalArgumentException("Exactly one backend (host, pvc) must be specified, but multiple were provided")
589+
throw IllegalArgumentException(
590+
"Exactly one backend (host, pvc, ossfs) must be specified, but multiple were provided",
591+
)
486592
}
487593

488594
return Volume(
489595
name = nameValue,
490596
host = host,
491597
pvc = pvc,
598+
ossfs = ossfs,
492599
mountPath = mountPathValue,
493600
readOnly = readOnly,
494601
subPath = subPath,

0 commit comments

Comments
 (0)