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
354 lines
12 KiB
Python
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())
|