Skip to content

Commit 13bbd37

Browse files
committed
Add default block volume for persistent storage
1 parent 075fece commit 13bbd37

File tree

3 files changed

+149
-3
lines changed

3 files changed

+149
-3
lines changed

src/snowflake/cli/_plugins/remote/constants.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ class ComputeResources:
5151
USER_VSCODE_DATA_VOLUME_NAME = "user-vscode-data"
5252
USER_VSCODE_DATA_VOLUME_MOUNT_PATH = "/root/.vscode-server/data"
5353

54+
# Block storage constants
55+
BLOCK_STORAGE_VOLUME_NAME = "block-storage"
56+
DEFAULT_BLOCK_STORAGE_SIZE = "10Gi" # Default size for block storage volumes
57+
5458
# Service naming constants
5559
SERVICE_NAME_PREFIX = "SNOW_REMOTE"
5660

src/snowflake/cli/_plugins/remote/container_spec.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
import yaml
2222
from snowflake import snowpark
2323
from snowflake.cli._plugins.remote.constants import (
24+
BLOCK_STORAGE_VOLUME_NAME,
25+
DEFAULT_BLOCK_STORAGE_SIZE,
2426
DEFAULT_CONTAINER_NAME,
2527
DEFAULT_IMAGE_CPU,
2628
DEFAULT_IMAGE_GPU,
@@ -97,7 +99,8 @@ def generate_service_spec(
9799
Args:
98100
session: Snowflake session
99101
compute_pool: Compute pool for service execution
100-
stage: Optional internal Snowflake stage to mount (e.g., @my_stage)
102+
stage: Optional internal Snowflake stage to mount (e.g., @my_stage).
103+
If not provided, a block storage volume will be created for persistent storage.
101104
image: Optional custom image (can be full path like 'repo/image:tag' or just tag like '1.7.1')
102105
ssh_public_key: Optional SSH public key to inject for secure authentication
103106
@@ -168,7 +171,7 @@ def generate_service_spec(
168171
}
169172
)
170173

171-
# Mount user stage as volume if provided
174+
# Mount user stage as volume if provided, otherwise use block storage
172175
if stage:
173176
# Mount user workspace volume
174177
user_workspace_mount = PurePath(USER_WORKSPACE_VOLUME_MOUNT_PATH)
@@ -207,6 +210,25 @@ def generate_service_spec(
207210
"source": vscode_data_source,
208211
}
209212
)
213+
else:
214+
# Use block storage for persistent storage when no stage is provided
215+
# Mount user workspace volume using block storage
216+
user_workspace_mount = PurePath(USER_WORKSPACE_VOLUME_MOUNT_PATH)
217+
volume_mounts.append(
218+
{
219+
"name": BLOCK_STORAGE_VOLUME_NAME,
220+
"mountPath": user_workspace_mount.as_posix(),
221+
}
222+
)
223+
224+
# Define block storage volume
225+
volumes.append(
226+
{
227+
"name": BLOCK_STORAGE_VOLUME_NAME,
228+
"source": "block",
229+
"size": DEFAULT_BLOCK_STORAGE_SIZE,
230+
}
231+
)
210232

211233
# Setup environment variables
212234
env_vars = {
@@ -294,7 +316,8 @@ def generate_service_spec_yaml(
294316
Args:
295317
session: Snowflake session
296318
compute_pool: Compute pool for service execution
297-
stage: Optional internal Snowflake stage to mount (e.g., @my_stage)
319+
stage: Optional internal Snowflake stage to mount (e.g., @my_stage).
320+
If not provided, a block storage volume will be created for persistent storage.
298321
image: Optional custom image (can be full path like 'repo/image:tag' or just tag like '1.7.1')
299322
ssh_public_key: Optional SSH public key to inject for secure authentication
300323

tests/remote/test_container_spec.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,12 @@
1717
import pytest
1818
from snowflake import snowpark
1919
from snowflake.cli._plugins.remote.constants import (
20+
BLOCK_STORAGE_VOLUME_NAME,
21+
DEFAULT_BLOCK_STORAGE_SIZE,
2022
DEFAULT_IMAGE_TAG,
2123
RAY_DASHBOARD_ENDPOINT_NAME,
2224
SERVER_UI_ENDPOINT_NAME,
25+
USER_WORKSPACE_VOLUME_MOUNT_PATH,
2326
WEBSOCKET_SSH_ENDPOINT_NAME,
2427
)
2528
from snowflake.cli._plugins.remote.container_spec import (
@@ -418,3 +421,119 @@ def test_generate_spec_with_gpu_resources(self, mock_session):
418421
assert resources["limits"]["cpu"] == "8000m"
419422
assert resources["requests"]["memory"] == "32Gi"
420423
assert resources["limits"]["memory"] == "32Gi"
424+
425+
def test_generate_spec_without_stage_uses_block_storage(self, mock_session):
426+
"""Test that block storage is created when no stage is provided."""
427+
spec = generate_service_spec(session=mock_session, compute_pool="test_pool")
428+
429+
# Check volumes
430+
volumes = spec["spec"]["volumes"]
431+
block_volume = next(
432+
(v for v in volumes if v["name"] == BLOCK_STORAGE_VOLUME_NAME), None
433+
)
434+
435+
# Block storage volume should exist
436+
assert block_volume is not None
437+
assert block_volume["source"] == "block"
438+
assert block_volume["size"] == DEFAULT_BLOCK_STORAGE_SIZE
439+
440+
# Check volume mounts
441+
container = spec["spec"]["containers"][0]
442+
volume_mounts = container["volumeMounts"]
443+
block_mount = next(
444+
(vm for vm in volume_mounts if vm["name"] == BLOCK_STORAGE_VOLUME_NAME),
445+
None,
446+
)
447+
448+
# Block storage should be mounted at user workspace path
449+
assert block_mount is not None
450+
assert block_mount["mountPath"] == USER_WORKSPACE_VOLUME_MOUNT_PATH
451+
452+
def test_generate_spec_with_stage_no_block_storage(self, mock_session):
453+
"""Test that block storage is NOT created when stage is provided."""
454+
spec = generate_service_spec(
455+
session=mock_session, compute_pool="test_pool", stage="@my_stage"
456+
)
457+
458+
# Check volumes
459+
volumes = spec["spec"]["volumes"]
460+
block_volume = next(
461+
(v for v in volumes if v["name"] == BLOCK_STORAGE_VOLUME_NAME), None
462+
)
463+
464+
# Block storage volume should NOT exist when stage is provided
465+
assert block_volume is None
466+
467+
# Check that stage volumes are present instead
468+
workspace_volume = next(v for v in volumes if v["name"] == "user-workspace")
469+
assert workspace_volume["source"] == "@my_stage/user-default"
470+
471+
def test_generate_spec_block_storage_yaml_format(self, mock_session):
472+
"""Test that block storage is correctly formatted in YAML output."""
473+
from snowflake.cli._plugins.remote.container_spec import (
474+
generate_service_spec_yaml,
475+
)
476+
477+
yaml_output = generate_service_spec_yaml(
478+
session=mock_session, compute_pool="test_pool"
479+
)
480+
481+
# Verify it's valid YAML
482+
import yaml
483+
484+
parsed_spec = yaml.safe_load(yaml_output)
485+
486+
# Check block storage volume exists
487+
volumes = parsed_spec["spec"]["volumes"]
488+
block_volume = next(
489+
(v for v in volumes if v["name"] == BLOCK_STORAGE_VOLUME_NAME), None
490+
)
491+
492+
assert block_volume is not None
493+
assert block_volume["source"] == "block"
494+
assert block_volume["size"] == DEFAULT_BLOCK_STORAGE_SIZE
495+
496+
# Check that YAML string contains block storage references
497+
assert BLOCK_STORAGE_VOLUME_NAME in yaml_output
498+
assert "source: block" in yaml_output
499+
assert DEFAULT_BLOCK_STORAGE_SIZE in yaml_output
500+
501+
def test_generate_spec_block_storage_size(self, mock_session):
502+
"""Test that block storage has correct default size."""
503+
spec = generate_service_spec(session=mock_session, compute_pool="test_pool")
504+
505+
volumes = spec["spec"]["volumes"]
506+
block_volume = next(
507+
v for v in volumes if v["name"] == BLOCK_STORAGE_VOLUME_NAME
508+
)
509+
510+
# Verify the default size is set correctly (10Gi as per constant)
511+
assert block_volume["size"] == "10Gi"
512+
513+
def test_generate_spec_block_storage_only_one_workspace_mount(self, mock_session):
514+
"""Test that only block storage OR stage volumes are present, not both."""
515+
# Test without stage - should have block storage
516+
spec_no_stage = generate_service_spec(
517+
session=mock_session, compute_pool="test_pool"
518+
)
519+
520+
volumes_no_stage = spec_no_stage["spec"]["volumes"]
521+
volume_names_no_stage = [v["name"] for v in volumes_no_stage]
522+
523+
# Should have block storage, but not stage-based volumes
524+
assert BLOCK_STORAGE_VOLUME_NAME in volume_names_no_stage
525+
assert "user-workspace" not in volume_names_no_stage
526+
assert "user-vscode-data" not in volume_names_no_stage
527+
528+
# Test with stage - should have stage volumes
529+
spec_with_stage = generate_service_spec(
530+
session=mock_session, compute_pool="test_pool", stage="@my_stage"
531+
)
532+
533+
volumes_with_stage = spec_with_stage["spec"]["volumes"]
534+
volume_names_with_stage = [v["name"] for v in volumes_with_stage]
535+
536+
# Should have stage volumes, but not block storage
537+
assert "user-workspace" in volume_names_with_stage
538+
assert "user-vscode-data" in volume_names_with_stage
539+
assert BLOCK_STORAGE_VOLUME_NAME not in volume_names_with_stage

0 commit comments

Comments
 (0)