Skip to content

Commit ef0d8a7

Browse files
authored
Resolve url for dstack login (#3427)
1 parent de7170b commit ef0d8a7

File tree

3 files changed

+34
-14
lines changed

3 files changed

+34
-14
lines changed

src/dstack/_internal/cli/commands/login.py

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from typing import Optional
88

99
from dstack._internal.cli.commands import BaseCommand
10-
from dstack._internal.cli.utils.common import console
10+
from dstack._internal.cli.utils.common import console, resolve_url
1111
from dstack._internal.core.errors import ClientError, CLIError
1212
from dstack._internal.core.models.users import UserWithCreds
1313
from dstack.api._public.runs import ConfigManager
@@ -202,19 +202,13 @@ def _create_server(self, handler: type[BaseHTTPRequestHandler]) -> HTTPServer:
202202

203203

204204
def _normalize_url_or_error(url: str) -> str:
205-
if not url.startswith("http://") and not url.startswith("https://"):
206-
url = "http://" + url
207-
parsed = urllib.parse.urlparse(url)
208-
if (
209-
not parsed.scheme
210-
or not parsed.hostname
211-
or parsed.path not in ("", "/")
212-
or parsed.params
213-
or parsed.query
214-
or parsed.fragment
215-
or (parsed.port is not None and not (1 <= parsed.port <= 65535))
216-
):
217-
raise CLIError("Invalid server URL format. Format: --url https://sky.dstack.ai")
205+
try:
206+
# Validate the URL and determine the URL scheme.
207+
# Need to resolve the scheme before making first POST request
208+
# since for some redirect codes (301), clients change POST to GET.
209+
url = resolve_url(url)
210+
except ValueError as e:
211+
raise CLIError(e.args[0])
218212
return url
219213

220214

src/dstack/_internal/cli/utils/common.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from pathlib import Path
44
from typing import Any, Dict, Optional, Union
55

6+
import requests
67
from rich.console import Console
78
from rich.prompt import Confirm
89
from rich.table import Table
@@ -128,3 +129,20 @@ def get_start_time(since: Optional[str]) -> Optional[datetime]:
128129
return parse_since(since)
129130
except ValueError as e:
130131
raise CLIError(e.args[0])
132+
133+
134+
def resolve_url(url: str, timeout: float = 5.0) -> str:
135+
"""
136+
Starts with http:// and follows redirects. Returns the final URL (including scheme).
137+
"""
138+
if not url.startswith("http://") and not url.startswith("https://"):
139+
url = "http://" + url
140+
try:
141+
response = requests.get(
142+
url,
143+
allow_redirects=True,
144+
timeout=timeout,
145+
)
146+
except requests.exceptions.ConnectionError as e:
147+
raise ValueError(f"Failed to resolve url {url}") from e
148+
return response.url

src/tests/_internal/cli/commands/test_login.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,12 @@ def test_login_no_projects(self, capsys: CaptureFixture, tmp_path: Path):
1313
patch("dstack._internal.cli.commands.login.webbrowser") as webbrowser_mock,
1414
patch("dstack._internal.cli.commands.login.APIClient") as APIClientMock,
1515
patch("dstack._internal.cli.commands.login._LoginServer") as LoginServerMock,
16+
patch(
17+
"dstack._internal.cli.commands.login._normalize_url_or_error"
18+
) as _normalize_url_or_error_mock,
1619
):
1720
webbrowser_mock.open.return_value = True
21+
_normalize_url_or_error_mock.return_value = "http://127.0.0.1:31313"
1822
APIClientMock.return_value.auth.list_providers.return_value = [
1923
SimpleNamespace(name="github", enabled=True)
2024
]
@@ -49,7 +53,11 @@ def test_login_configures_projects(self, capsys: CaptureFixture, tmp_path: Path)
4953
patch("dstack._internal.cli.commands.login.APIClient") as APIClientMock,
5054
patch("dstack._internal.cli.commands.login.ConfigManager") as ConfigManagerMock,
5155
patch("dstack._internal.cli.commands.login._LoginServer") as LoginServerMock,
56+
patch(
57+
"dstack._internal.cli.commands.login._normalize_url_or_error"
58+
) as _normalize_url_or_error_mock,
5259
):
60+
_normalize_url_or_error_mock.return_value = "http://127.0.0.1:31313"
5361
webbrowser_mock.open.return_value = True
5462
APIClientMock.return_value.auth.list_providers.return_value = [
5563
SimpleNamespace(name="github", enabled=True)

0 commit comments

Comments
 (0)