Skip to content

Commit 204d763

Browse files
committed
feat: add support for encrypted pod volumes via encrypt_volume param
1 parent 0a52c70 commit 204d763

10 files changed

Lines changed: 245 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
# Change Log
22

3+
## Release 1.6.3 (TBD)
4+
5+
### Added
6+
7+
- Support for volume encryption via `encrypt_volume` parameter in `create_pod()` function and CLI commands.
8+
- Add `encrypt_volume: bool = False` parameter to pod creation API and GraphQL mutations.
9+
- Add `--encrypt-volume/--no-encrypt-volume` CLI option for `runpod pod create` command.
10+
- Add `encrypt_volume` configuration support in project `runpod.toml` files for development pods.
11+
12+
---
13+
314
## Release 1.6.2 (2/12/24)
415

516
### Fixed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,9 +140,15 @@ pod = runpod.get_pod(pod.id)
140140
# Create a pod with GPU
141141
pod = runpod.create_pod("test", "runpod/stack", "NVIDIA GeForce RTX 3070")
142142

143+
# Create a pod with GPU and encrypted volume
144+
pod = runpod.create_pod("test", "runpod/stack", "NVIDIA GeForce RTX 3070", encrypt_volume=True)
145+
143146
# Create a pod with CPU
144147
pod = runpod.create_pod("test", "runpod/stack", instance_id="cpu3c-2-4")
145148

149+
# Create a pod with CPU and encrypted volume
150+
pod = runpod.create_pod("test", "runpod/stack", instance_id="cpu3c-2-4", encrypt_volume=True)
151+
146152
# Stop the pod
147153
runpod.stop_pod(pod.id)
148154

runpod/api/ctl_commands.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ def create_pod(
110110
min_download = None,
111111
min_upload = None,
112112
instance_id: Optional[str] = None,
113+
encrypt_volume: bool = False,
113114
) -> dict:
114115
"""
115116
Create a pod
@@ -131,6 +132,7 @@ def create_pod(
131132
:param min_download: minimum download speed in Mbps
132133
:param min_upload: minimum upload speed in Mbps
133134
:param instance_id: the id of a specific instance to deploy to (for CPU pods)
135+
:param encrypt_volume: whether to encrypt the volume
134136
:example:
135137
136138
>>> # Create GPU pod
@@ -181,6 +183,7 @@ def create_pod(
181183
min_download,
182184
min_upload,
183185
instance_id,
186+
encrypt_volume,
184187
)
185188
)
186189

runpod/api/mutations/pods.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ def generate_pod_deployment_mutation(
3131
min_download: Optional[int] = None,
3232
min_upload: Optional[int] = None,
3333
instance_id: Optional[str] = None,
34+
encrypt_volume: bool = False,
3435
) -> str:
3536
"""
3637
Generates a mutation to deploy a pod on demand.
@@ -59,6 +60,7 @@ def generate_pod_deployment_mutation(
5960
min_download: Minimum download speed in Mbps
6061
min_upload: Minimum upload speed in Mbps
6162
instance_id: Instance ID for CPU pods
63+
encrypt_volume: Whether to encrypt the volume
6264
6365
Returns:
6466
str: GraphQL mutation string
@@ -125,6 +127,8 @@ def generate_pod_deployment_mutation(
125127
input_fields.append(f'minDownload: {min_download}')
126128
if min_upload is not None:
127129
input_fields.append(f'minUpload: {min_upload}')
130+
if encrypt_volume:
131+
input_fields.append("encryptVolume: true")
128132

129133
mutation_type = "podFindAndDeployOnDemand" if gpu_type_id else "deployCpuPod"
130134
input_string = ", ".join(input_fields)

runpod/cli/groups/pod/commands.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,19 @@ def list_pods():
3838
@click.option(
3939
"--support-public-ip", default=True, help="Whether or not to support a public IP."
4040
)
41+
@click.option(
42+
"--encrypt-volume/--no-encrypt-volume", default=False, help="Whether or not to encrypt the volume."
43+
)
4144
def create_new_pod(
42-
name, image, gpu_type, gpu_count, support_public_ip
45+
name, image, gpu_type, gpu_count, support_public_ip, encrypt_volume
4346
): # pylint: disable=too-many-arguments
4447
"""
4548
Creates a pod.
4649
"""
4750
kwargs = {
4851
"gpu_count": gpu_count,
4952
"support_public_ip": support_public_ip,
53+
"encrypt_volume": encrypt_volume,
5054
}
5155

5256
if not name:

runpod/cli/groups/project/functions.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ def _launch_dev_pod():
4949
selected_gpu_types.append(config["project"]["gpu"])
5050

5151
# Attempt to launch a pod with the given configuration
52-
new_pod = attempt_pod_launch(config, environment_variables)
52+
encrypt_volume = config["project"].get("encrypt_volume", False)
53+
new_pod = attempt_pod_launch(config, environment_variables, encrypt_volume)
5354
if new_pod is None:
5455
print(
5556
"Selected GPU types unavailable, try again later or use a different type."
@@ -144,6 +145,7 @@ def create_new_project(
144145
project_table.add("volume_mount_path", "/runpod-volume")
145146
project_table.add("ports", "8080/http, 22/tcp")
146147
project_table.add("container_disk_size_gb", 10)
148+
project_table.add("encrypt_volume", False)
147149
project_table.add("env_vars", ENV_VARS)
148150
toml_config.add("project", project_table)
149151

runpod/cli/groups/project/helpers.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ def copy_template_files(template_dir, destination):
5757
shutil.copy2(source_item, destination_item)
5858

5959

60-
def attempt_pod_launch(config, environment_variables):
60+
def attempt_pod_launch(config, environment_variables, encrypt_volume=False):
6161
"""Attempt to launch a pod with the given configuration."""
6262
for gpu_type in config["project"].get("gpu_types", []):
6363
print(f"Trying to get a pod with {gpu_type}... ", end="")
@@ -73,6 +73,7 @@ def attempt_pod_launch(config, environment_variables):
7373
volume_mount_path=f'{config["project"]["volume_mount_path"]}',
7474
container_disk_in_gb=int(config["project"]["container_disk_size_gb"]),
7575
env=environment_variables,
76+
encrypt_volume=encrypt_volume,
7677
)
7778
print("Success!")
7879
return created_pod

tests/test_api/test_ctl_commands.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,76 @@ def test_create_pod(self):
136136
"cloud_type must be one of ALL, COMMUNITY or SECURE",
137137
)
138138

139+
def test_create_pod_with_encrypt_volume(self):
140+
"""
141+
Tests create_pod with encrypt_volume parameter
142+
"""
143+
with patch("runpod.api.graphql.requests.post") as patch_request, patch(
144+
"runpod.api.ctl_commands.get_gpu"
145+
) as patch_get_gpu, patch("runpod.api.ctl_commands.get_user") as patch_get_user:
146+
patch_request.return_value.json.return_value = {
147+
"data": {"podFindAndDeployOnDemand": {"id": "POD_ID_ENCRYPTED"}}
148+
}
149+
150+
patch_get_gpu.return_value = None
151+
patch_get_user.return_value = {
152+
"networkVolumes": [
153+
{"id": "NETWORK_VOLUME_ID", "dataCenterId": "us-east-1"}
154+
]
155+
}
156+
157+
# Test with encryption enabled
158+
pod = ctl_commands.create_pod(
159+
name="POD_NAME_ENCRYPTED",
160+
image_name="IMAGE_NAME",
161+
gpu_type_id="NVIDIA A100 80GB PCIe",
162+
network_volume_id="NETWORK_VOLUME_ID",
163+
encrypt_volume=True
164+
)
165+
166+
self.assertEqual(pod["id"], "POD_ID_ENCRYPTED")
167+
168+
# Verify that the GraphQL mutation was called with encryptVolume: true
169+
called_mutation = patch_request.call_args[1]['data']
170+
self.assertIn("encryptVolume: true", called_mutation)
171+
172+
# Test with encryption explicitly disabled
173+
patch_request.return_value.json.return_value = {
174+
"data": {"podFindAndDeployOnDemand": {"id": "POD_ID_NO_ENCRYPTION"}}
175+
}
176+
177+
pod = ctl_commands.create_pod(
178+
name="POD_NAME_NO_ENCRYPTION",
179+
image_name="IMAGE_NAME",
180+
gpu_type_id="NVIDIA A100 80GB PCIe",
181+
network_volume_id="NETWORK_VOLUME_ID",
182+
encrypt_volume=False
183+
)
184+
185+
self.assertEqual(pod["id"], "POD_ID_NO_ENCRYPTION")
186+
187+
# Verify that the GraphQL mutation was not called with encryptVolume
188+
called_mutation = patch_request.call_args[1]['data']
189+
self.assertNotIn("encryptVolume", called_mutation)
190+
191+
# Test with default value (should not include encryptVolume)
192+
patch_request.return_value.json.return_value = {
193+
"data": {"podFindAndDeployOnDemand": {"id": "POD_ID_DEFAULT"}}
194+
}
195+
196+
pod = ctl_commands.create_pod(
197+
name="POD_NAME_DEFAULT",
198+
image_name="IMAGE_NAME",
199+
gpu_type_id="NVIDIA A100 80GB PCIe",
200+
network_volume_id="NETWORK_VOLUME_ID"
201+
)
202+
203+
self.assertEqual(pod["id"], "POD_ID_DEFAULT")
204+
205+
# Verify that the GraphQL mutation was not called with encryptVolume (default)
206+
called_mutation = patch_request.call_args[1]['data']
207+
self.assertNotIn("encryptVolume", called_mutation)
208+
139209
def test_stop_pod(self):
140210
"""
141211
Test stop_pod

tests/test_api/test_mutations_pods.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,50 @@ def test_generate_pod_deployment_mutation(self):
6060
self.assertIn("mutation", cpu_result)
6161
self.assertIn("deployCpuPod", cpu_result)
6262

63+
def test_generate_pod_deployment_mutation_with_encrypt_volume(self):
64+
"""
65+
Test generate_pod_deployment_mutation with encrypt_volume parameter
66+
"""
67+
# Test GPU pod deployment with encryption enabled
68+
gpu_result_encrypted = pods.generate_pod_deployment_mutation(
69+
name="test-encrypted",
70+
image_name="test_image",
71+
gpu_type_id="1",
72+
cloud_type="cloud",
73+
encrypt_volume=True
74+
)
75+
76+
# Test CPU pod deployment with encryption enabled
77+
cpu_result_encrypted = pods.generate_pod_deployment_mutation(
78+
name="test-cpu-encrypted",
79+
image_name="test_image",
80+
cloud_type="cloud",
81+
instance_id="cpu3c-2-4",
82+
encrypt_volume=True
83+
)
84+
85+
# Test GPU pod deployment with encryption explicitly disabled
86+
gpu_result_no_encryption = pods.generate_pod_deployment_mutation(
87+
name="test-no-encryption",
88+
image_name="test_image",
89+
gpu_type_id="1",
90+
cloud_type="cloud",
91+
encrypt_volume=False
92+
)
93+
94+
# Check that encrypted mutations contain encryptVolume: true
95+
self.assertIn("encryptVolume: true", gpu_result_encrypted)
96+
self.assertIn("encryptVolume: true", cpu_result_encrypted)
97+
98+
# Check that non-encrypted mutations do not contain encryptVolume
99+
self.assertNotIn("encryptVolume", gpu_result_no_encryption)
100+
101+
# Check basic mutation structure is preserved
102+
self.assertIn("mutation", gpu_result_encrypted)
103+
self.assertIn("podFindAndDeployOnDemand", gpu_result_encrypted)
104+
self.assertIn("mutation", cpu_result_encrypted)
105+
self.assertIn("deployCpuPod", cpu_result_encrypted)
106+
63107
def test_generate_pod_stop_mutation(self):
64108
"""
65109
Test generate_pod_stop_mutation

tests/test_cli/test_cli_groups/test_pod_commands.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,106 @@ def test_create_new_pod(
7575
gpu_count=1,
7676
support_public_ip=True,
7777
ports="22/tcp",
78+
encrypt_volume=False, # Accept the new default argument
7879
)
7980
mock_echo.assert_called_with("Pod sample_id has been created.")
8081

82+
@patch("runpod.cli.groups.pod.commands.click.prompt")
83+
@patch("runpod.cli.groups.pod.commands.click.confirm")
84+
@patch("runpod.cli.groups.pod.commands.click.echo")
85+
@patch("runpod.cli.groups.pod.commands.create_pod")
86+
def test_create_new_pod_with_encrypt_volume(
87+
self, mock_create_pod, mock_echo, mock_confirm, mock_prompt
88+
): # pylint: disable=too-many-arguments,line-too-long
89+
"""
90+
Test create_new_pod with --encrypt-volume option
91+
"""
92+
# Mock values
93+
mock_confirm.return_value = True # for the quick_launch option
94+
mock_prompt.return_value = "RunPod-CLI-Pod-Encrypted"
95+
mock_create_pod.return_value = {"id": "encrypted_pod_id"}
96+
97+
runner = CliRunner()
98+
result = runner.invoke(runpod_cli, ["pod", "create", "--encrypt-volume"])
99+
100+
# Assertions
101+
assert result.exit_code == 0, result.exception
102+
mock_prompt.assert_called_once_with("Enter pod name", default="RunPod-CLI-Pod")
103+
mock_echo.assert_called_with("Pod encrypted_pod_id has been created.")
104+
mock_create_pod.assert_called_with(
105+
"RunPod-CLI-Pod-Encrypted",
106+
"runpod/base:0.0.0",
107+
"NVIDIA GeForce RTX 3090",
108+
gpu_count=1,
109+
support_public_ip=True,
110+
ports="22/tcp",
111+
encrypt_volume=True, # This is the key assertion
112+
)
113+
114+
@patch("runpod.cli.groups.pod.commands.click.prompt")
115+
@patch("runpod.cli.groups.pod.commands.click.confirm")
116+
@patch("runpod.cli.groups.pod.commands.click.echo")
117+
@patch("runpod.cli.groups.pod.commands.create_pod")
118+
def test_create_new_pod_with_no_encrypt_volume(
119+
self, mock_create_pod, mock_echo, mock_confirm, mock_prompt
120+
): # pylint: disable=too-many-arguments,line-too-long
121+
"""
122+
Test create_new_pod with --no-encrypt-volume option
123+
"""
124+
# Mock values
125+
mock_confirm.return_value = True # for the quick_launch option
126+
mock_prompt.return_value = "RunPod-CLI-Pod-No-Encryption"
127+
mock_create_pod.return_value = {"id": "no_encryption_pod_id"}
128+
129+
runner = CliRunner()
130+
result = runner.invoke(runpod_cli, ["pod", "create", "--no-encrypt-volume"])
131+
132+
# Assertions
133+
assert result.exit_code == 0, result.exception
134+
mock_prompt.assert_called_once_with("Enter pod name", default="RunPod-CLI-Pod")
135+
mock_echo.assert_called_with("Pod no_encryption_pod_id has been created.")
136+
mock_create_pod.assert_called_with(
137+
"RunPod-CLI-Pod-No-Encryption",
138+
"runpod/base:0.0.0",
139+
"NVIDIA GeForce RTX 3090",
140+
gpu_count=1,
141+
support_public_ip=True,
142+
ports="22/tcp",
143+
encrypt_volume=False, # This is the key assertion
144+
)
145+
146+
@patch("runpod.cli.groups.pod.commands.click.prompt")
147+
@patch("runpod.cli.groups.pod.commands.click.confirm")
148+
@patch("runpod.cli.groups.pod.commands.click.echo")
149+
@patch("runpod.cli.groups.pod.commands.create_pod")
150+
def test_create_new_pod_default_encrypt_volume(
151+
self, mock_create_pod, mock_echo, mock_confirm, mock_prompt
152+
): # pylint: disable=too-many-arguments,line-too-long
153+
"""
154+
Test create_new_pod with default encrypt_volume (should be False)
155+
"""
156+
# Mock values
157+
mock_confirm.return_value = True # for the quick_launch option
158+
mock_prompt.return_value = "RunPod-CLI-Pod-Default"
159+
mock_create_pod.return_value = {"id": "default_pod_id"}
160+
161+
runner = CliRunner()
162+
result = runner.invoke(runpod_cli, ["pod", "create"])
163+
164+
# Assertions
165+
assert result.exit_code == 0, result.exception
166+
mock_prompt.assert_called_once_with("Enter pod name", default="RunPod-CLI-Pod")
167+
mock_echo.assert_called_with("Pod default_pod_id has been created.")
168+
mock_create_pod.assert_called_with(
169+
"RunPod-CLI-Pod-Default",
170+
"runpod/base:0.0.0",
171+
"NVIDIA GeForce RTX 3090",
172+
gpu_count=1,
173+
support_public_ip=True,
174+
ports="22/tcp",
175+
encrypt_volume=False, # Default should be False
176+
)
177+
81178
@patch("runpod.cli.groups.pod.commands.click.echo")
82179
@patch("runpod.cli.groups.pod.commands.ssh_cmd.SSHConnection")
83180
def test_connect_to_pod(self, mock_ssh_connection, mock_echo):

0 commit comments

Comments
 (0)