Skip to content

Commit df78219

Browse files
committed
fix: escape registry credentials and validate REST options
1 parent 3520061 commit df78219

7 files changed

Lines changed: 35 additions & 76 deletions

File tree

‎README.md‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,7 @@ pod = runpod.create_pod("test", "runpod/stack", instance_id="cpu3c-2-4")
297297
runpod.stop_pod(pod["id"])
298298

299299
# resume the pod
300-
runpod.resume_pod(pod["id"], 1)
300+
runpod.resume_pod(pod["id"])
301301

302302
# terminate the pod
303303
runpod.terminate_pod(pod["id"])
@@ -314,6 +314,8 @@ runpod.terminate_pod(pod["id"])
314314
requires `network_volume_id`.
315315
- For GPU pods, `min_memory_in_gb` and `min_vcpu_count` specify minimum host RAM
316316
and vCPUs **per GPU**, not GPU VRAM or totals for the pod.
317+
- CPU `instance_id` must use `<cpu-flavor>-<vcpu-count>-<memory>`, with positive
318+
integer vCPU and memory values, for example `cpu3c-4-8`.
317319
- REST v2 cannot require a public-IP-capable host. `support_public_ip=False` is
318320
the default and does not disable public networking. `True` raises `ValueError`
319321
rather than silently ignoring that requirement.
@@ -326,6 +328,8 @@ runpod.terminate_pod(pod["id"])
326328
- `create_endpoint` accepts only `QUEUE_DELAY` and `REQUEST_COUNT` scaling.
327329
`idle_timeout` applies to `QUEUE_DELAY` (default: 5 seconds); explicitly setting
328330
it with `REQUEST_COUNT` raises `ValueError`.
331+
- `resume_pod(pod_id)` keeps the existing GPU allocation. REST v2 does not support
332+
resizing on resume, so providing `gpu_count` raises `ValueError`.
329333

330334
## 📁 | Directory
331335

‎docs/api/queries.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ for gpu in gpus:
1414
### get_gpus output
1515

1616
```python
17-
{
17+
[{
1818
"id": "NVIDIA GeForce RTX 4090",
1919
"name": "RTX 4090",
2020
"pool": "ADA_24",
@@ -24,7 +24,7 @@ for gpu in gpus:
2424
"community": True,
2525
"price": {"secure": 0.44, "community": 0.31, "serverless": 1.1},
2626
"maxCount": {"secure": 8, "community": 4},
27-
}
27+
}]
2828
```
2929

3030
## get_gpu

‎examples/rest_wrapper.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
print("Waiting for pod to be stopped...")
2929
time.sleep(10)
3030

31-
pod = runpod.resume_pod(pod["id"], 1)
31+
pod = runpod.resume_pod(pod["id"])
3232
print(pod)
3333

3434
print("Waiting for pod to be resumed...")

‎runpod/api/ctl_commands.py‎

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
# pylint: disable=too-many-arguments,too-many-locals
44

5+
import re
56
from typing import Any, Iterable, Optional
67
from urllib.parse import quote
78

@@ -31,21 +32,17 @@ def _environment(env: Optional[dict]) -> dict[str, str]:
3132
return {str(key): str(value) for key, value in (env or {}).items()}
3233

3334

34-
def _cpu_config(instance_id: Optional[str], min_vcpu_count: int) -> dict[str, Any]:
35+
def _cpu_config(instance_id: Optional[str]) -> dict[str, Any]:
3536
if not instance_id:
3637
raise ValueError("instance_id must be provided for CPU pods")
3738

38-
parts = instance_id.split("-")
39-
vcpu_count = max(2, min_vcpu_count)
40-
if len(parts) > 1:
41-
try:
42-
vcpu_count = int(parts[1])
43-
except ValueError as exc:
44-
raise ValueError(
45-
"instance_id must use the format <cpu-flavor>-<vcpu-count>-<memory>"
46-
) from exc
39+
match = re.fullmatch(r"([^-\s]+)-([1-9][0-9]*)-([1-9][0-9]*)", instance_id)
40+
if match is None:
41+
raise ValueError(
42+
"instance_id must use the format <cpu-flavor>-<vcpu-count>-<memory>"
43+
)
4744

48-
return {"id": parts[0], "vcpuCount": vcpu_count}
45+
return {"id": match[1], "vcpuCount": int(match[2])}
4946

5047

5148
def get_user(api_key: Optional[str] = None) -> dict:
@@ -212,7 +209,7 @@ def create_pod(
212209
gpu["allowedCudaVersions"] = _split_values(allowed_cuda_versions)
213210
body["gpu"] = gpu
214211
else:
215-
body["cpu"] = _cpu_config(instance_id, min_vcpu_count)
212+
body["cpu"] = _cpu_config(instance_id)
216213

217214
return run_rest_request("POST", "/v2/pods", json=body)
218215

@@ -226,9 +223,10 @@ def stop_pod(pod_id: str) -> dict:
226223
)
227224

228225

229-
def resume_pod(pod_id: str, gpu_count: int) -> dict:
230-
"""Start a stopped pod."""
231-
_ = gpu_count
226+
def resume_pod(pod_id: str, gpu_count: Optional[int] = None) -> dict:
227+
"""Start a stopped pod without changing its GPU allocation."""
228+
if gpu_count is not None:
229+
raise ValueError("REST API v2 does not support gpu_count when resuming a pod")
232230
return run_rest_request(
233231
"POST",
234232
f"/v2/pods/{_path_segment(pod_id)}/action",

‎runpod/api/mutations/container_register_auth.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""GraphQL operations for container registry credentials."""
22

3+
import json
4+
35

46
def update_container_registry_auth(
57
registry_auth_id: str, username: str, password: str
@@ -11,7 +13,7 @@ def update_container_registry_auth(
1113
"password": password,
1214
}
1315
input_str = ", ".join(
14-
f'{key}: "{value}"' for key, value in input_dict.items()
16+
f"{key}: {json.dumps(value)}" for key, value in input_dict.items()
1517
)
1618

1719
return f"""

‎tests/test_api/test_ctl_commands.py‎

Lines changed: 11 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -264,26 +264,17 @@ def allocate(method, path, *, json):
264264
assert pod["machineId"] == "at-boundary"
265265

266266

267-
def test_create_cpu_pod_translates_instance_id():
268-
with patch(
269-
"runpod.api.ctl_commands.run_rest_request", return_value={"id": "pod"}
270-
) as request:
271-
ctl_commands.create_pod("cpu-pod", "python:3.11", instance_id="cpu3c-4-8")
272-
273-
assert request.call_args.kwargs["json"]["cpu"] == {
274-
"id": "cpu3c",
275-
"vcpuCount": 4,
276-
}
277-
278-
279267
def test_create_cpu_pod_requires_instance_id():
280268
with pytest.raises(ValueError, match="instance_id"):
281269
ctl_commands.create_pod("cpu-pod", "python:3.11")
282270

283271

284-
def test_create_cpu_pod_validates_instance_id():
272+
@pytest.mark.parametrize(
273+
"instance_id", ["cpu3c-invalid", "cpu3c", "cpu3c-4-not-a-number"]
274+
)
275+
def test_create_cpu_pod_validates_instance_id(template_backend, instance_id):
285276
with pytest.raises(ValueError, match="format"):
286-
ctl_commands.create_pod("cpu-pod", "python:3.11", instance_id="cpu3c-invalid")
277+
ctl_commands.create_pod("cpu-pod", "python:3.11", instance_id=instance_id)
287278

288279

289280
def test_create_pod_validates_image_and_cloud():
@@ -310,19 +301,12 @@ def test_create_gpu_pod_rejects_unsupported_constraints(template_backend, kwargs
310301
ctl_commands.create_pod("pod", "image", gpu_type_id="NVIDIA A100", **kwargs)
311302

312303

313-
def test_stop_and_resume_pod_use_actions():
314-
with patch(
315-
"runpod.api.ctl_commands.run_rest_request", return_value={"id": "pod"}
316-
) as request:
317-
assert ctl_commands.stop_pod("pod") == {"id": "pod"}
318-
assert ctl_commands.resume_pod("pod", 8) == {"id": "pod"}
319-
320-
assert request.call_args_list[0].args == (
321-
"POST",
322-
"/v2/pods/pod/action",
323-
)
324-
assert request.call_args_list[0].kwargs == {"json": {"action": "stop"}}
325-
assert request.call_args_list[1].kwargs == {"json": {"action": "start"}}
304+
def test_resume_pod_rejects_gpu_count():
305+
with (
306+
patch("runpod.api.ctl_commands.run_rest_request", return_value={"id": "pod"}),
307+
pytest.raises(ValueError, match="gpu_count"),
308+
):
309+
ctl_commands.resume_pod("pod", 8)
326310

327311

328312
def test_terminate_pod_deletes_resource():
@@ -461,21 +445,6 @@ def test_create_container_registry_auth_uses_rest():
461445
)
462446

463447

464-
def test_update_container_registry_auth_uses_graphql():
465-
with patch(
466-
"runpod.api.ctl_commands.run_graphql_query",
467-
return_value={"data": {"updateRegistryAuth": {"id": "registry"}}},
468-
) as request:
469-
result = ctl_commands.update_container_registry_auth(
470-
"registry", "user", "password"
471-
)
472-
473-
assert result == {"id": "registry"}
474-
mutation = request.call_args.args[0]
475-
assert "mutation UpdateRegistryAuth" in mutation
476-
assert 'id: "registry"' in mutation
477-
478-
479448
def test_delete_container_registry_auth_uses_rest():
480449
with patch(
481450
"runpod.api.ctl_commands.run_rest_request", return_value=None

‎tests/test_api/test_mutation_container_registry_auth.py‎

Lines changed: 0 additions & 14 deletions
This file was deleted.

0 commit comments

Comments
 (0)