main.py
1 # Copyright 2025 Alibaba Group Holding Ltd. 2 # 3 # Licensed under the Apache License, Version 2.0 (the "License"); 4 # you may not use this file except in compliance with the License. 5 # You may obtain a copy of the License at 6 # 7 # http://www.apache.org/licenses/LICENSE-2.0 8 # 9 # Unless required by applicable law or agreed to in writing, software 10 # distributed under the License is distributed on an "AS IS" BASIS, 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 # See the License for the specific language governing permissions and 13 # limitations under the License. 14 15 """ 16 Docker PVC (Named Volume) Mount Example 17 ======================================== 18 19 Demonstrates how to mount Docker named volumes into sandbox containers using 20 the OpenSandbox ``pvc`` backend. In Docker runtime the ``pvc`` backend maps 21 ``claimName`` to a Docker named volume -- providing a more convenient and 22 secure alternative to host-path bind mounts for sharing data across sandboxes. 23 24 Four scenarios are demonstrated: 25 26 1. **Read-write mount** - Mount a named volume for bidirectional file I/O. 27 2. **Read-only mount** - Mount a named volume as read-only. 28 3. **Cross-sandbox sharing** - Two sandboxes share data through the same named 29 volume without exposing any host path. 30 4. **SubPath mount** - Mount only a subdirectory of a named volume, 31 keeping the same API as Kubernetes PVC subPath. 32 33 Prerequisites: 34 - OpenSandbox server running with Docker runtime 35 - Docker named volume created before running this script (see README.md) 36 """ 37 38 import asyncio 39 import os 40 import subprocess 41 from datetime import timedelta 42 43 from opensandbox import Sandbox 44 from opensandbox.config import ConnectionConfig 45 46 try: 47 from opensandbox.models.sandboxes import PVC, Volume 48 except ImportError: 49 print( 50 "ERROR: Your installed opensandbox SDK does not include Volume/PVC models.\n" 51 " Volume support requires the latest SDK from source.\n" 52 " Please install from the local repository:\n" 53 "\n" 54 " pip install -e sdks/sandbox/python\n" 55 "\n" 56 " See README.md for details." 57 ) 58 raise SystemExit(1) 59 60 61 VOLUME_NAME = "opensandbox-pvc-demo" 62 63 64 async def print_exec(sandbox: Sandbox, command: str) -> str | None: 65 """Run a command in the sandbox and print/return stdout.""" 66 result = await sandbox.commands.run(command) 67 if result.error: 68 print(f" [error] {result.error.name}: {result.error.value}") 69 return None 70 text = "\n".join(msg.text for msg in result.logs.stdout) 71 if text: 72 print(f" {text}") 73 return text 74 75 76 def ensure_named_volume() -> None: 77 """Create the Docker named volume and seed it with test data.""" 78 print(f" Ensuring Docker named volume '{VOLUME_NAME}' exists...") 79 subprocess.run( 80 ["docker", "volume", "rm", VOLUME_NAME], 81 capture_output=True, 82 ) 83 subprocess.run( 84 ["docker", "volume", "create", VOLUME_NAME], 85 check=True, 86 capture_output=True, 87 ) 88 # Seed the volume with a marker file and subpath test data 89 subprocess.run( 90 [ 91 "docker", "run", "--rm", 92 "-v", f"{VOLUME_NAME}:/data", 93 "alpine", 94 "sh", "-c", 95 "echo 'hello-from-named-volume' > /data/marker.txt && " 96 "mkdir -p /data/datasets/train && " 97 "echo 'id,value' > /data/datasets/train/data.csv && " 98 "echo '1,100' >> /data/datasets/train/data.csv && " 99 "echo '2,200' >> /data/datasets/train/data.csv", 100 ], 101 check=True, 102 capture_output=True, 103 ) 104 print(f" Created volume '{VOLUME_NAME}' with marker.txt and datasets/train/") 105 106 107 async def demo_readwrite_mount(config: ConnectionConfig, image: str) -> None: 108 """ 109 Scenario 1: Read-write named volume mount. 110 111 Mount a Docker named volume into the sandbox at /mnt/data. 112 Write a file inside the sandbox, then read it back to verify. 113 """ 114 print("\n" + "=" * 60) 115 print("Scenario 1: Read-Write PVC (Named Volume) Mount") 116 print("=" * 60) 117 print(f" Volume name: {VOLUME_NAME}") 118 print(f" Mount path : /mnt/data") 119 120 sandbox = await Sandbox.create( 121 image=image, 122 connection_config=config, 123 timeout=timedelta(minutes=2), 124 volumes=[ 125 Volume( 126 name="demo-data", 127 pvc=PVC(claimName=VOLUME_NAME), 128 mountPath="/mnt/data", 129 readOnly=False, 130 ), 131 ], 132 ) 133 134 async with sandbox: 135 try: 136 # Read the seeded marker file 137 print("\n [1] Reading marker file from named volume:") 138 await print_exec(sandbox, "cat /mnt/data/marker.txt") 139 140 # Write a new file 141 print("\n [2] Writing a file from inside the sandbox:") 142 await print_exec( 143 sandbox, 144 "echo 'written-by-sandbox' > /mnt/data/sandbox-output.txt", 145 ) 146 print(" -> Written: /mnt/data/sandbox-output.txt") 147 148 # Read it back 149 print("\n [3] Reading back the written file:") 150 await print_exec(sandbox, "cat /mnt/data/sandbox-output.txt") 151 152 # List all files 153 print("\n [4] Listing volume contents:") 154 await print_exec(sandbox, "ls -la /mnt/data/") 155 156 finally: 157 await sandbox.kill() 158 159 print("\n Scenario 1 completed.") 160 161 162 async def demo_readonly_mount(config: ConnectionConfig, image: str) -> None: 163 """ 164 Scenario 2: Read-only named volume mount. 165 166 Mount the same named volume as read-only. Verify reads succeed but 167 writes are rejected by the container runtime. 168 """ 169 print("\n" + "=" * 60) 170 print("Scenario 2: Read-Only PVC (Named Volume) Mount") 171 print("=" * 60) 172 print(f" Volume name: {VOLUME_NAME}") 173 print(f" Mount path : /mnt/readonly") 174 175 sandbox = await Sandbox.create( 176 image=image, 177 connection_config=config, 178 timeout=timedelta(minutes=2), 179 volumes=[ 180 Volume( 181 name="readonly-vol", 182 pvc=PVC(claimName=VOLUME_NAME), 183 mountPath="/mnt/readonly", 184 readOnly=True, 185 ), 186 ], 187 ) 188 189 async with sandbox: 190 try: 191 # Read the marker file 192 print("\n [1] Reading marker.txt from read-only mount:") 193 await print_exec(sandbox, "cat /mnt/readonly/marker.txt") 194 195 # Attempt to write (should fail) 196 print("\n [2] Attempting to write (should fail):") 197 result = await sandbox.commands.run( 198 "touch /mnt/readonly/should-fail.txt 2>&1 || echo 'Write denied (expected)'" 199 ) 200 for msg in result.logs.stdout: 201 print(f" {msg.text}") 202 for msg in result.logs.stderr: 203 print(f" {msg.text}") 204 205 finally: 206 await sandbox.kill() 207 208 print("\n Scenario 2 completed.") 209 210 211 async def demo_cross_sandbox_sharing(config: ConnectionConfig, image: str) -> None: 212 """ 213 Scenario 3: Cross-sandbox data sharing via named volume. 214 215 Two sandboxes mount the same named volume. Sandbox A writes a file, 216 then Sandbox B reads it -- demonstrating data sharing without any host 217 path exposure. 218 """ 219 print("\n" + "=" * 60) 220 print("Scenario 3: Cross-Sandbox Sharing via PVC (Named Volume)") 221 print("=" * 60) 222 print(f" Volume name: {VOLUME_NAME}") 223 224 volume_spec = Volume( 225 name="shared-vol", 226 pvc=PVC(claimName=VOLUME_NAME), 227 mountPath="/mnt/shared", 228 readOnly=False, 229 ) 230 231 # --- Sandbox A: write --- 232 print("\n [Sandbox A] Creating sandbox and writing data...") 233 sandbox_a = await Sandbox.create( 234 image=image, 235 connection_config=config, 236 timeout=timedelta(minutes=2), 237 volumes=[volume_spec], 238 ) 239 async with sandbox_a: 240 try: 241 await print_exec( 242 sandbox_a, 243 "echo 'message-from-sandbox-a' > /mnt/shared/cross-sandbox.txt", 244 ) 245 print(" [Sandbox A] Wrote /mnt/shared/cross-sandbox.txt") 246 finally: 247 await sandbox_a.kill() 248 249 # --- Sandbox B: read --- 250 print("\n [Sandbox B] Creating sandbox and reading data...") 251 sandbox_b = await Sandbox.create( 252 image=image, 253 connection_config=config, 254 timeout=timedelta(minutes=2), 255 volumes=[volume_spec], 256 ) 257 async with sandbox_b: 258 try: 259 print(" [Sandbox B] Reading file written by Sandbox A:") 260 text = await print_exec(sandbox_b, "cat /mnt/shared/cross-sandbox.txt") 261 if text and "message-from-sandbox-a" in text: 262 print("\n Cross-sandbox data sharing verified!") 263 finally: 264 await sandbox_b.kill() 265 266 print("\n Scenario 3 completed.") 267 268 269 async def demo_subpath_mount(config: ConnectionConfig, image: str) -> None: 270 """ 271 Scenario 4: SubPath mount on a named volume. 272 273 Mount only a subdirectory (datasets/train) of the named volume. The server 274 resolves the volume's host-side Mountpoint via ``docker volume inspect`` and 275 appends the subPath, producing a standard bind mount. This keeps the API 276 consistent with Kubernetes PVC subPath semantics. 277 """ 278 print("\n" + "=" * 60) 279 print("Scenario 4: SubPath PVC (Named Volume) Mount") 280 print("=" * 60) 281 print(f" Volume name: {VOLUME_NAME}") 282 print(f" SubPath : datasets/train") 283 print(f" Mount path : /mnt/training-data") 284 285 sandbox = await Sandbox.create( 286 image=image, 287 connection_config=config, 288 timeout=timedelta(minutes=2), 289 volumes=[ 290 Volume( 291 name="train-data", 292 pvc=PVC(claimName=VOLUME_NAME), 293 mountPath="/mnt/training-data", 294 readOnly=True, 295 subPath="datasets/train", 296 ), 297 ], 298 ) 299 300 async with sandbox: 301 try: 302 # List contents -- should only show the subpath 303 print("\n [1] Listing mounted subpath content:") 304 await print_exec(sandbox, "ls -la /mnt/training-data/") 305 306 # Read the CSV data 307 print("\n [2] Reading data.csv:") 308 await print_exec(sandbox, "cat /mnt/training-data/data.csv") 309 310 # Verify the root marker.txt is NOT visible (we're inside datasets/train) 311 print("\n [3] Verifying volume root is NOT visible:") 312 result = await sandbox.commands.run("test -f /mnt/training-data/marker.txt && echo FOUND || echo NOT-FOUND") 313 text = "\n".join(msg.text for msg in result.logs.stdout) 314 print(f" marker.txt at mount root: {text}") 315 if "NOT-FOUND" in text: 316 print(" -> Confirmed: subPath isolation is working correctly") 317 318 finally: 319 await sandbox.kill() 320 321 print("\n Scenario 4 completed.") 322 323 324 async def main() -> None: 325 domain = os.getenv("SANDBOX_DOMAIN", "localhost:8080") 326 api_key = os.getenv("SANDBOX_API_KEY") 327 image = os.getenv("SANDBOX_IMAGE", "ubuntu") 328 329 config = ConnectionConfig( 330 domain=domain, 331 api_key=api_key, 332 request_timeout=timedelta(minutes=3), 333 ) 334 335 print(f"OpenSandbox server : {config.domain}") 336 print(f"Sandbox image : {image}") 337 print(f"Docker volume : {VOLUME_NAME}") 338 339 # Ensure the named volume exists with seed data 340 ensure_named_volume() 341 342 await demo_readwrite_mount(config, image) 343 await demo_readonly_mount(config, image) 344 await demo_cross_sandbox_sharing(config, image) 345 await demo_subpath_mount(config, image) 346 347 print("\n" + "=" * 60) 348 print("All scenarios completed successfully!") 349 print("=" * 60) 350 351 352 if __name__ == "__main__": 353 asyncio.run(main())