Skip to content

Commit f7a5d71

Browse files
committed
feat(sdk/python): add not_before/not_after/with_app_info TLS key options
Extend get_tls_key with the three GetTlsKeyArgs proto fields (6/7/8) added on dstack OS >= 0.5.7. New params are kw-only so positional call sites keep working unchanged. When any new option is provided, the SDK probes the Version RPC first and raises a clear error on older OS images that lack it (same gating pattern as ed25519 key derivation). Mirrors the JS SDK change in dc5fc21.
1 parent 25283f1 commit f7a5d71

2 files changed

Lines changed: 108 additions & 1 deletion

File tree

sdk/python/src/dstack_sdk/dstack_client.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,19 @@ async def _ensure_algorithm_supported(self, algorithm: str) -> None:
401401
"OS version too old (Version RPC unavailable)"
402402
)
403403

404+
async def _ensure_tls_key_options_supported(
405+
self, feature_names: List[str]
406+
) -> None:
407+
"""Check OS version when 0.5.7+ TLS key options are requested."""
408+
try:
409+
await self.version()
410+
except Exception:
411+
features = ", ".join(feature_names)
412+
raise RuntimeError(
413+
f"TLS key options [{features}] are not supported: "
414+
"OS version too old (Version RPC unavailable)"
415+
)
416+
404417
async def get_key(
405418
self,
406419
path: str | None = None,
@@ -477,8 +490,28 @@ async def get_tls_key(
477490
usage_ra_tls: bool = False,
478491
usage_server_auth: bool = True,
479492
usage_client_auth: bool = False,
493+
*,
494+
not_before: Optional[int] = None,
495+
not_after: Optional[int] = None,
496+
with_app_info: Optional[bool] = None,
480497
) -> GetTlsKeyResponse:
481-
"""Request a TLS key from the service with optional parameters."""
498+
"""Request a TLS key from the service with optional parameters.
499+
500+
``not_before`` / ``not_after`` (seconds since UNIX epoch) and
501+
``with_app_info`` require dstack OS >= 0.5.7. When any of them is set,
502+
the SDK probes the guest agent ``Version`` RPC first and raises a
503+
clear error on older OS images.
504+
"""
505+
new_features: List[str] = []
506+
if not_before is not None:
507+
new_features.append("not_before")
508+
if not_after is not None:
509+
new_features.append("not_after")
510+
if with_app_info is not None:
511+
new_features.append("with_app_info")
512+
if new_features:
513+
await self._ensure_tls_key_options_supported(new_features)
514+
482515
data: Dict[str, Any] = {
483516
"subject": subject or "",
484517
"usage_ra_tls": usage_ra_tls,
@@ -487,6 +520,12 @@ async def get_tls_key(
487520
}
488521
if alt_names:
489522
data["alt_names"] = list(alt_names)
523+
if not_before is not None:
524+
data["not_before"] = not_before
525+
if not_after is not None:
526+
data["not_after"] = not_after
527+
if with_app_info is not None:
528+
data["with_app_info"] = with_app_info
490529

491530
result = await self._send_rpc_request("GetTlsKey", data)
492531
return GetTlsKeyResponse(**result)
@@ -604,6 +643,10 @@ def get_tls_key(
604643
usage_ra_tls: bool = False,
605644
usage_server_auth: bool = True,
606645
usage_client_auth: bool = False,
646+
*,
647+
not_before: Optional[int] = None,
648+
not_after: Optional[int] = None,
649+
with_app_info: Optional[bool] = None,
607650
) -> GetTlsKeyResponse:
608651
"""Request a TLS key from the service with optional parameters."""
609652
raise NotImplementedError

sdk/python/tests/test_client.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -649,3 +649,67 @@ async def test_mixed_sync_async_calls():
649649
# Both should work and return valid results
650650
assert len(sync_result.decode_key()) == 32
651651
assert len(async_result.decode_key()) == 32
652+
653+
654+
@pytest.mark.asyncio
655+
async def test_get_tls_key_new_options_payload(monkeypatch):
656+
"""0.5.7+ TLS options reach the payload and trigger the Version probe."""
657+
calls: list = []
658+
659+
async def fake_send(self, method, payload):
660+
calls.append((method, payload))
661+
if method == "Version":
662+
return {"version": "0.5.7", "rev": "test"}
663+
return {"key": "k", "certificate_chain": []}
664+
665+
monkeypatch.setenv("DSTACK_SIMULATOR_ENDPOINT", "http://localhost:0")
666+
monkeypatch.setattr(AsyncDstackClient, "_send_rpc_request", fake_send)
667+
client = AsyncDstackClient()
668+
result = await client.get_tls_key(
669+
subject="api.example.com",
670+
not_before=1_700_000_000,
671+
not_after=1_800_000_000,
672+
with_app_info=True,
673+
)
674+
assert isinstance(result, GetTlsKeyResponse)
675+
assert [c[0] for c in calls] == ["Version", "GetTlsKey"]
676+
payload = calls[1][1]
677+
assert payload["not_before"] == 1_700_000_000
678+
assert payload["not_after"] == 1_800_000_000
679+
assert payload["with_app_info"] is True
680+
681+
682+
@pytest.mark.asyncio
683+
async def test_get_tls_key_legacy_options_skip_version_probe(monkeypatch):
684+
"""Calls without the new options must NOT probe Version (backward compat)."""
685+
calls: list = []
686+
687+
async def fake_send(self, method, payload):
688+
calls.append((method, payload))
689+
return {"key": "k", "certificate_chain": []}
690+
691+
monkeypatch.setenv("DSTACK_SIMULATOR_ENDPOINT", "http://localhost:0")
692+
monkeypatch.setattr(AsyncDstackClient, "_send_rpc_request", fake_send)
693+
client = AsyncDstackClient()
694+
await client.get_tls_key(subject="api.example.com")
695+
assert [c[0] for c in calls] == ["GetTlsKey"]
696+
payload = calls[0][1]
697+
assert "not_before" not in payload
698+
assert "not_after" not in payload
699+
assert "with_app_info" not in payload
700+
701+
702+
@pytest.mark.asyncio
703+
async def test_get_tls_key_new_options_require_version(monkeypatch):
704+
"""Raise a clear error when Version RPC is unavailable on older OS."""
705+
706+
async def fake_send(self, method, payload):
707+
if method == "Version":
708+
raise RuntimeError("Version not implemented")
709+
return {"key": "k", "certificate_chain": []}
710+
711+
monkeypatch.setenv("DSTACK_SIMULATOR_ENDPOINT", "http://localhost:0")
712+
monkeypatch.setattr(AsyncDstackClient, "_send_rpc_request", fake_send)
713+
client = AsyncDstackClient()
714+
with pytest.raises(RuntimeError, match="TLS key options"):
715+
await client.get_tls_key(with_app_info=False)

0 commit comments

Comments
 (0)