Files
wehub-resource-sync e0e362d700
SDK Tests / changes (push) Successful in 2m29s
Real E2E Tests / changes (push) Successful in 2m29s
Deploy Docs Pages / build (push) Has been cancelled
Deploy Docs Pages / deploy (push) Has been cancelled
Real E2E Tests / JavaScript E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Python E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Java E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / C# E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Go E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Real E2E CI (push) Has been cancelled
SDK Tests / SDK CI (push) Has been cancelled
SDK Tests / CLI Tests (push) Has been cancelled
SDK Tests / Python SDK Quality (code-interpreter) (push) Has been cancelled
SDK Tests / Python SDK Quality (sandbox) (push) Has been cancelled
SDK Tests / Python SDK Tests (code-interpreter) (push) Has been cancelled
SDK Tests / JavaScript SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / JavaScript SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Python SDK Tests (sandbox) (push) Has been cancelled
SDK Tests / CLI Quality (push) Has been cancelled
SDK Tests / Kotlin SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Kotlin SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / C# SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / C# SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Go SDK Quality And Tests (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:39:33 +08:00

354 lines
12 KiB
Python

# Copyright 2025 Alibaba Group Holding Ltd.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Docker PVC (Named Volume) Mount Example
========================================
Demonstrates how to mount Docker named volumes into sandbox containers using
the OpenSandbox ``pvc`` backend. In Docker runtime the ``pvc`` backend maps
``claimName`` to a Docker named volume -- providing a more convenient and
secure alternative to host-path bind mounts for sharing data across sandboxes.
Four scenarios are demonstrated:
1. **Read-write mount** - Mount a named volume for bidirectional file I/O.
2. **Read-only mount** - Mount a named volume as read-only.
3. **Cross-sandbox sharing** - Two sandboxes share data through the same named
volume without exposing any host path.
4. **SubPath mount** - Mount only a subdirectory of a named volume,
keeping the same API as Kubernetes PVC subPath.
Prerequisites:
- OpenSandbox server running with Docker runtime
- Docker named volume created before running this script (see README.md)
"""
import asyncio
import os
import subprocess
from datetime import timedelta
from opensandbox import Sandbox
from opensandbox.config import ConnectionConfig
try:
from opensandbox.models.sandboxes import PVC, Volume
except ImportError:
print(
"ERROR: Your installed opensandbox SDK does not include Volume/PVC models.\n"
" Volume support requires the latest SDK from source.\n"
" Please install from the local repository:\n"
"\n"
" pip install -e sdks/sandbox/python\n"
"\n"
" See README.md for details."
)
raise SystemExit(1)
VOLUME_NAME = "opensandbox-pvc-demo"
async def print_exec(sandbox: Sandbox, command: str) -> str | None:
"""Run a command in the sandbox and print/return stdout."""
result = await sandbox.commands.run(command)
if result.error:
print(f" [error] {result.error.name}: {result.error.value}")
return None
text = "\n".join(msg.text for msg in result.logs.stdout)
if text:
print(f" {text}")
return text
def ensure_named_volume() -> None:
"""Create the Docker named volume and seed it with test data."""
print(f" Ensuring Docker named volume '{VOLUME_NAME}' exists...")
subprocess.run(
["docker", "volume", "rm", VOLUME_NAME],
capture_output=True,
)
subprocess.run(
["docker", "volume", "create", VOLUME_NAME],
check=True,
capture_output=True,
)
# Seed the volume with a marker file and subpath test data
subprocess.run(
[
"docker", "run", "--rm",
"-v", f"{VOLUME_NAME}:/data",
"alpine",
"sh", "-c",
"echo 'hello-from-named-volume' > /data/marker.txt && "
"mkdir -p /data/datasets/train && "
"echo 'id,value' > /data/datasets/train/data.csv && "
"echo '1,100' >> /data/datasets/train/data.csv && "
"echo '2,200' >> /data/datasets/train/data.csv",
],
check=True,
capture_output=True,
)
print(f" Created volume '{VOLUME_NAME}' with marker.txt and datasets/train/")
async def demo_readwrite_mount(config: ConnectionConfig, image: str) -> None:
"""
Scenario 1: Read-write named volume mount.
Mount a Docker named volume into the sandbox at /mnt/data.
Write a file inside the sandbox, then read it back to verify.
"""
print("\n" + "=" * 60)
print("Scenario 1: Read-Write PVC (Named Volume) Mount")
print("=" * 60)
print(f" Volume name: {VOLUME_NAME}")
print(f" Mount path : /mnt/data")
sandbox = await Sandbox.create(
image=image,
connection_config=config,
timeout=timedelta(minutes=2),
volumes=[
Volume(
name="demo-data",
pvc=PVC(claimName=VOLUME_NAME),
mountPath="/mnt/data",
readOnly=False,
),
],
)
async with sandbox:
try:
# Read the seeded marker file
print("\n [1] Reading marker file from named volume:")
await print_exec(sandbox, "cat /mnt/data/marker.txt")
# Write a new file
print("\n [2] Writing a file from inside the sandbox:")
await print_exec(
sandbox,
"echo 'written-by-sandbox' > /mnt/data/sandbox-output.txt",
)
print(" -> Written: /mnt/data/sandbox-output.txt")
# Read it back
print("\n [3] Reading back the written file:")
await print_exec(sandbox, "cat /mnt/data/sandbox-output.txt")
# List all files
print("\n [4] Listing volume contents:")
await print_exec(sandbox, "ls -la /mnt/data/")
finally:
await sandbox.kill()
print("\n Scenario 1 completed.")
async def demo_readonly_mount(config: ConnectionConfig, image: str) -> None:
"""
Scenario 2: Read-only named volume mount.
Mount the same named volume as read-only. Verify reads succeed but
writes are rejected by the container runtime.
"""
print("\n" + "=" * 60)
print("Scenario 2: Read-Only PVC (Named Volume) Mount")
print("=" * 60)
print(f" Volume name: {VOLUME_NAME}")
print(f" Mount path : /mnt/readonly")
sandbox = await Sandbox.create(
image=image,
connection_config=config,
timeout=timedelta(minutes=2),
volumes=[
Volume(
name="readonly-vol",
pvc=PVC(claimName=VOLUME_NAME),
mountPath="/mnt/readonly",
readOnly=True,
),
],
)
async with sandbox:
try:
# Read the marker file
print("\n [1] Reading marker.txt from read-only mount:")
await print_exec(sandbox, "cat /mnt/readonly/marker.txt")
# Attempt to write (should fail)
print("\n [2] Attempting to write (should fail):")
result = await sandbox.commands.run(
"touch /mnt/readonly/should-fail.txt 2>&1 || echo 'Write denied (expected)'"
)
for msg in result.logs.stdout:
print(f" {msg.text}")
for msg in result.logs.stderr:
print(f" {msg.text}")
finally:
await sandbox.kill()
print("\n Scenario 2 completed.")
async def demo_cross_sandbox_sharing(config: ConnectionConfig, image: str) -> None:
"""
Scenario 3: Cross-sandbox data sharing via named volume.
Two sandboxes mount the same named volume. Sandbox A writes a file,
then Sandbox B reads it -- demonstrating data sharing without any host
path exposure.
"""
print("\n" + "=" * 60)
print("Scenario 3: Cross-Sandbox Sharing via PVC (Named Volume)")
print("=" * 60)
print(f" Volume name: {VOLUME_NAME}")
volume_spec = Volume(
name="shared-vol",
pvc=PVC(claimName=VOLUME_NAME),
mountPath="/mnt/shared",
readOnly=False,
)
# --- Sandbox A: write ---
print("\n [Sandbox A] Creating sandbox and writing data...")
sandbox_a = await Sandbox.create(
image=image,
connection_config=config,
timeout=timedelta(minutes=2),
volumes=[volume_spec],
)
async with sandbox_a:
try:
await print_exec(
sandbox_a,
"echo 'message-from-sandbox-a' > /mnt/shared/cross-sandbox.txt",
)
print(" [Sandbox A] Wrote /mnt/shared/cross-sandbox.txt")
finally:
await sandbox_a.kill()
# --- Sandbox B: read ---
print("\n [Sandbox B] Creating sandbox and reading data...")
sandbox_b = await Sandbox.create(
image=image,
connection_config=config,
timeout=timedelta(minutes=2),
volumes=[volume_spec],
)
async with sandbox_b:
try:
print(" [Sandbox B] Reading file written by Sandbox A:")
text = await print_exec(sandbox_b, "cat /mnt/shared/cross-sandbox.txt")
if text and "message-from-sandbox-a" in text:
print("\n Cross-sandbox data sharing verified!")
finally:
await sandbox_b.kill()
print("\n Scenario 3 completed.")
async def demo_subpath_mount(config: ConnectionConfig, image: str) -> None:
"""
Scenario 4: SubPath mount on a named volume.
Mount only a subdirectory (datasets/train) of the named volume. The server
resolves the volume's host-side Mountpoint via ``docker volume inspect`` and
appends the subPath, producing a standard bind mount. This keeps the API
consistent with Kubernetes PVC subPath semantics.
"""
print("\n" + "=" * 60)
print("Scenario 4: SubPath PVC (Named Volume) Mount")
print("=" * 60)
print(f" Volume name: {VOLUME_NAME}")
print(f" SubPath : datasets/train")
print(f" Mount path : /mnt/training-data")
sandbox = await Sandbox.create(
image=image,
connection_config=config,
timeout=timedelta(minutes=2),
volumes=[
Volume(
name="train-data",
pvc=PVC(claimName=VOLUME_NAME),
mountPath="/mnt/training-data",
readOnly=True,
subPath="datasets/train",
),
],
)
async with sandbox:
try:
# List contents -- should only show the subpath
print("\n [1] Listing mounted subpath content:")
await print_exec(sandbox, "ls -la /mnt/training-data/")
# Read the CSV data
print("\n [2] Reading data.csv:")
await print_exec(sandbox, "cat /mnt/training-data/data.csv")
# Verify the root marker.txt is NOT visible (we're inside datasets/train)
print("\n [3] Verifying volume root is NOT visible:")
result = await sandbox.commands.run("test -f /mnt/training-data/marker.txt && echo FOUND || echo NOT-FOUND")
text = "\n".join(msg.text for msg in result.logs.stdout)
print(f" marker.txt at mount root: {text}")
if "NOT-FOUND" in text:
print(" -> Confirmed: subPath isolation is working correctly")
finally:
await sandbox.kill()
print("\n Scenario 4 completed.")
async def main() -> None:
domain = os.getenv("SANDBOX_DOMAIN", "localhost:8080")
api_key = os.getenv("SANDBOX_API_KEY")
image = os.getenv("SANDBOX_IMAGE", "ubuntu")
config = ConnectionConfig(
domain=domain,
api_key=api_key,
request_timeout=timedelta(minutes=3),
)
print(f"OpenSandbox server : {config.domain}")
print(f"Sandbox image : {image}")
print(f"Docker volume : {VOLUME_NAME}")
# Ensure the named volume exists with seed data
ensure_named_volume()
await demo_readwrite_mount(config, image)
await demo_readonly_mount(config, image)
await demo_cross_sandbox_sharing(config, image)
await demo_subpath_mount(config, image)
print("\n" + "=" * 60)
print("All scenarios completed successfully!")
print("=" * 60)
if __name__ == "__main__":
asyncio.run(main())