|
1 | 1 | import json |
| 2 | +from dataclasses import replace |
2 | 3 |
|
3 | 4 | import pytest |
4 | 5 | import responses # https://github.com/getsentry/responses |
5 | 6 |
|
6 | 7 | from verda.containers import ComputeResource, Container, ContainerRegistrySettings |
| 8 | +from verda.containers._containers import ( |
| 9 | + GeneralStorageMount, |
| 10 | + MemoryMount, |
| 11 | + SecretMount, |
| 12 | + SharedFileSystemMount, |
| 13 | +) |
7 | 14 | from verda.exceptions import APIException |
8 | 15 | from verda.job_deployments import ( |
9 | 16 | JobDeployment, |
@@ -207,3 +214,71 @@ def test_purge_job_deployment_queue(self, service, endpoint): |
207 | 214 | service.purge_queue(JOB_NAME) |
208 | 215 |
|
209 | 216 | assert responses.assert_call_count(url, 1) is True |
| 217 | + |
| 218 | + @responses.activate |
| 219 | + def test_update_preserves_volume_mounts_round_trip(self, service, endpoint): |
| 220 | + """Regression test: volume mount subclass fields (volume_id, secret_name, etc.) |
| 221 | + must survive a get → update round trip without being dropped during deserialization.""" |
| 222 | + volume_id = '550e8400-e29b-41d4-a716-446655440000' |
| 223 | + api_payload = { |
| 224 | + 'name': JOB_NAME, |
| 225 | + 'containers': [ |
| 226 | + { |
| 227 | + 'name': CONTAINER_NAME, |
| 228 | + 'image': 'busybox:latest', |
| 229 | + 'exposed_port': 8080, |
| 230 | + 'env': [], |
| 231 | + 'volume_mounts': [ |
| 232 | + {'type': 'scratch', 'mount_path': '/data'}, |
| 233 | + {'type': 'shared', 'mount_path': '/sfs', 'volume_id': volume_id}, |
| 234 | + { |
| 235 | + 'type': 'secret', |
| 236 | + 'mount_path': '/secrets', |
| 237 | + 'secret_name': 'my-secret', |
| 238 | + 'file_names': ['key.pem'], |
| 239 | + }, |
| 240 | + {'type': 'memory', 'mount_path': '/dev/shm', 'size_in_mb': 512}, |
| 241 | + ], |
| 242 | + } |
| 243 | + ], |
| 244 | + 'endpoint_base_url': 'https://test-job.datacrunch.io', |
| 245 | + 'created_at': '2024-01-01T00:00:00Z', |
| 246 | + 'compute': {'name': 'H100', 'size': 1}, |
| 247 | + 'container_registry_settings': {'is_private': False, 'credentials': None}, |
| 248 | + } |
| 249 | + |
| 250 | + get_url = f'{endpoint}/{JOB_NAME}' |
| 251 | + responses.add(responses.GET, get_url, json=api_payload, status=200) |
| 252 | + responses.add(responses.PATCH, get_url, json=api_payload, status=200) |
| 253 | + |
| 254 | + # Simulate the user's flow: get → modify image → update |
| 255 | + deployment = service.get_by_name(JOB_NAME) |
| 256 | + |
| 257 | + # Verify deserialization produced the correct subclasses |
| 258 | + vms = deployment.containers[0].volume_mounts |
| 259 | + assert isinstance(vms[0], GeneralStorageMount) |
| 260 | + assert isinstance(vms[1], SharedFileSystemMount) |
| 261 | + assert vms[1].volume_id == volume_id |
| 262 | + assert isinstance(vms[2], SecretMount) |
| 263 | + assert vms[2].secret_name == 'my-secret' |
| 264 | + assert vms[2].file_names == ['key.pem'] |
| 265 | + assert isinstance(vms[3], MemoryMount) |
| 266 | + assert vms[3].size_in_mb == 512 |
| 267 | + |
| 268 | + # Update only the image (exactly what the reported user script does) |
| 269 | + containers = list(deployment.containers) |
| 270 | + containers[0] = replace(containers[0], image='busybox:v2') |
| 271 | + updated_deployment = replace(deployment, containers=containers) |
| 272 | + |
| 273 | + service.update(JOB_NAME, updated_deployment) |
| 274 | + |
| 275 | + # Verify the PATCH request body still contains volume_id |
| 276 | + request_body = json.loads(responses.calls[1].request.body.decode('utf-8')) |
| 277 | + sent_vms = request_body['containers'][0]['volume_mounts'] |
| 278 | + assert sent_vms[0]['type'] == 'scratch' |
| 279 | + assert sent_vms[1]['type'] == 'shared' |
| 280 | + assert sent_vms[1]['volume_id'] == volume_id |
| 281 | + assert sent_vms[2]['type'] == 'secret' |
| 282 | + assert sent_vms[2]['secret_name'] == 'my-secret' |
| 283 | + assert sent_vms[3]['type'] == 'memory' |
| 284 | + assert sent_vms[3]['size_in_mb'] == 512 |
0 commit comments