Skip to content

Commit 10ecf33

Browse files
Add https: auto mode for services
Allow setting `https` to `auto` in service configuration. When set to `auto`, the effective HTTPS setting is resolved at registration time based on the gateway's certificate configuration: enabled for Let's Encrypt, disabled for no certificate or ACM. The default remains `true` for backward compatibility. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 7c4314b commit 10ecf33

3 files changed

Lines changed: 116 additions & 6 deletions

File tree

src/dstack/_internal/core/models/configurations.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -856,9 +856,13 @@ class ServiceConfigurationParams(CoreModel):
856856
)
857857
),
858858
] = None
859-
https: Annotated[bool, Field(description="Enable HTTPS if running with a gateway")] = (
860-
SERVICE_HTTPS_DEFAULT
861-
)
859+
https: Annotated[
860+
Union[bool, Literal["auto"]],
861+
Field(
862+
description="Enable HTTPS if running with a gateway."
863+
" Set to `auto` to determine automatically based on gateway configuration"
864+
),
865+
] = SERVICE_HTTPS_DEFAULT
862866
auth: Annotated[bool, Field(description="Enable the authorization")] = True
863867

864868
scaling: Annotated[

src/dstack/_internal/server/services/services/__init__.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
)
2323
from dstack._internal.core.models.configurations import (
2424
DEFAULT_REPLICA_GROUP_NAME,
25-
SERVICE_HTTPS_DEFAULT,
2625
ServiceConfiguration,
2726
)
2827
from dstack._internal.core.models.gateways import GatewayConfiguration, GatewayStatus
@@ -241,7 +240,7 @@ def _register_service_in_server(run_model: RunModel, run_spec: RunSpec) -> Servi
241240
"Service with SGLang router configuration requires a gateway. "
242241
"Please configure a gateway with the SGLang router enabled."
243242
)
244-
if run_spec.configuration.https != SERVICE_HTTPS_DEFAULT:
243+
if run_spec.configuration.https is False:
245244
# Note: if the user sets `https: <default-value>`, it will be ignored silently
246245
# TODO: in 0.19, make `https` Optional to be able to tell if it was set or omitted
247246
raise ServerClientError(
@@ -416,7 +415,14 @@ async def unregister_replica(session: AsyncSession, job_model: JobModel):
416415

417416
def _get_service_https(run_spec: RunSpec, configuration: GatewayConfiguration) -> bool:
418417
assert run_spec.configuration.type == "service"
419-
if not run_spec.configuration.https:
418+
https = run_spec.configuration.https
419+
if https == "auto":
420+
if configuration.certificate is None:
421+
return False
422+
if configuration.certificate.type == "acm":
423+
return False
424+
return True
425+
if not https:
420426
return False
421427
if configuration.certificate is not None and configuration.certificate.type == "acm":
422428
return False
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
from typing import Literal, Optional, Union
2+
from unittest.mock import MagicMock
3+
4+
import pytest
5+
6+
from dstack._internal.core.errors import ServerClientError
7+
from dstack._internal.core.models.backends.base import BackendType
8+
from dstack._internal.core.models.configurations import ServiceConfiguration
9+
from dstack._internal.core.models.gateways import (
10+
ACMGatewayCertificate,
11+
AnyGatewayCertificate,
12+
GatewayConfiguration,
13+
LetsEncryptGatewayCertificate,
14+
)
15+
from dstack._internal.core.models.runs import RunSpec
16+
from dstack._internal.server.services.services import (
17+
_get_service_https,
18+
_register_service_in_server,
19+
)
20+
from dstack._internal.server.testing.common import get_run_spec
21+
22+
23+
def _service_run_spec(https: Union[bool, Literal["auto"]] = "auto") -> RunSpec:
24+
return get_run_spec(
25+
repo_id="test-repo",
26+
configuration=ServiceConfiguration(commands=["python serve.py"], port=8000, https=https),
27+
)
28+
29+
30+
def _gateway_config(
31+
certificate: Optional[AnyGatewayCertificate] = LetsEncryptGatewayCertificate(),
32+
) -> GatewayConfiguration:
33+
return GatewayConfiguration(
34+
backend=BackendType.AWS,
35+
region="us-east-1",
36+
certificate=certificate,
37+
)
38+
39+
40+
def _mock_run_model() -> MagicMock:
41+
run_model = MagicMock()
42+
run_model.project.name = "test-project"
43+
run_model.run_name = "test-run"
44+
return run_model
45+
46+
47+
class TestServiceConfigurationHttps:
48+
def test_default_is_true(self) -> None:
49+
conf = ServiceConfiguration(commands=["python serve.py"], port=8000)
50+
assert conf.https is True
51+
52+
def test_accepts_auto(self) -> None:
53+
conf = ServiceConfiguration(commands=["python serve.py"], port=8000, https="auto")
54+
assert conf.https == "auto"
55+
56+
57+
class TestGetServiceHttps:
58+
def test_auto_resolves_to_true_with_lets_encrypt_gateway(self) -> None:
59+
run_spec = _service_run_spec(https="auto")
60+
gw = _gateway_config(certificate=LetsEncryptGatewayCertificate())
61+
assert _get_service_https(run_spec, gw) is True
62+
63+
def test_auto_resolves_to_false_when_gateway_has_no_certificate(self) -> None:
64+
run_spec = _service_run_spec(https="auto")
65+
gw = _gateway_config(certificate=None)
66+
assert _get_service_https(run_spec, gw) is False
67+
68+
def test_auto_resolves_to_false_with_acm_gateway(self) -> None:
69+
run_spec = _service_run_spec(https="auto")
70+
gw = _gateway_config(
71+
certificate=ACMGatewayCertificate(arn="arn:aws:acm:us-east-1:123:cert/abc")
72+
)
73+
assert _get_service_https(run_spec, gw) is False
74+
75+
def test_true_enables_https_regardless_of_gateway_certificate(self) -> None:
76+
run_spec = _service_run_spec(https=True)
77+
gw = _gateway_config(certificate=None)
78+
assert _get_service_https(run_spec, gw) is True
79+
80+
def test_false_disables_https_regardless_of_gateway_certificate(self) -> None:
81+
run_spec = _service_run_spec(https=False)
82+
gw = _gateway_config(certificate=LetsEncryptGatewayCertificate())
83+
assert _get_service_https(run_spec, gw) is False
84+
85+
86+
class TestRegisterServiceInServerHttps:
87+
def test_allows_default_true_without_gateway(self) -> None:
88+
run_spec = _service_run_spec(https=True)
89+
result = _register_service_in_server(_mock_run_model(), run_spec)
90+
assert result is not None
91+
92+
def test_allows_auto_without_gateway(self) -> None:
93+
run_spec = _service_run_spec(https="auto")
94+
result = _register_service_in_server(_mock_run_model(), run_spec)
95+
assert result is not None
96+
97+
def test_rejects_explicit_false_without_gateway(self) -> None:
98+
run_spec = _service_run_spec(https=False)
99+
with pytest.raises(ServerClientError, match="not applicable"):
100+
_register_service_in_server(_mock_run_model(), run_spec)

0 commit comments

Comments
 (0)