dstack 0.18.39__py3-none-any.whl → 0.18.40__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. dstack/_internal/cli/services/configurators/fleet.py +1 -1
  2. dstack/_internal/cli/services/configurators/gateway.py +10 -1
  3. dstack/_internal/cli/utils/common.py +1 -2
  4. dstack/_internal/core/models/configurations.py +21 -0
  5. dstack/_internal/core/models/runs.py +1 -0
  6. dstack/_internal/core/models/volumes.py +9 -0
  7. dstack/_internal/core/services/logs.py +4 -1
  8. dstack/_internal/core/services/ssh/attach.py +6 -5
  9. dstack/_internal/proxy/lib/models.py +1 -0
  10. dstack/_internal/proxy/lib/testing/common.py +2 -0
  11. dstack/_internal/server/app.py +7 -3
  12. dstack/_internal/server/background/tasks/process_submitted_jobs.py +2 -2
  13. dstack/_internal/server/schemas/runner.py +1 -0
  14. dstack/_internal/server/services/fleets.py +1 -1
  15. dstack/_internal/server/services/jobs/configurators/base.py +10 -0
  16. dstack/_internal/server/services/jobs/configurators/dev.py +3 -0
  17. dstack/_internal/server/services/jobs/configurators/service.py +3 -0
  18. dstack/_internal/server/services/jobs/configurators/task.py +3 -0
  19. dstack/_internal/server/services/proxy/repo.py +1 -0
  20. dstack/_internal/server/services/proxy/services/service_proxy.py +4 -0
  21. dstack/_internal/server/services/runs.py +5 -4
  22. dstack/api/_public/runs.py +8 -0
  23. dstack/api/server/__init__.py +19 -3
  24. dstack/api/server/_runs.py +28 -8
  25. dstack/version.py +1 -1
  26. {dstack-0.18.39.dist-info → dstack-0.18.40.dist-info}/METADATA +6 -7
  27. {dstack-0.18.39.dist-info → dstack-0.18.40.dist-info}/RECORD +36 -36
  28. tests/_internal/core/services/test_logs.py +16 -6
  29. tests/_internal/server/background/tasks/test_process_submitted_jobs.py +74 -1
  30. tests/_internal/server/routers/test_runs.py +4 -0
  31. tests/_internal/server/services/proxy/routers/test_service_proxy.py +39 -0
  32. tests/_internal/server/services/runner/test_client.py +6 -2
  33. {dstack-0.18.39.dist-info → dstack-0.18.40.dist-info}/LICENSE.md +0 -0
  34. {dstack-0.18.39.dist-info → dstack-0.18.40.dist-info}/WHEEL +0 -0
  35. {dstack-0.18.39.dist-info → dstack-0.18.40.dist-info}/entry_points.txt +0 -0
  36. {dstack-0.18.39.dist-info → dstack-0.18.40.dist-info}/top_level.txt +0 -0
@@ -353,7 +353,7 @@ def _print_plan_header(plan: FleetPlan):
353
353
  f"${plan.max_offer_price:g} max[/]"
354
354
  )
355
355
  console.print()
356
- else:
356
+ elif fleet_type == "cloud":
357
357
  console.print(NO_OFFERS_WARNING)
358
358
 
359
359
 
@@ -22,6 +22,7 @@ from dstack._internal.core.models.gateways import (
22
22
  GatewayStatus,
23
23
  )
24
24
  from dstack._internal.core.models.repos.base import Repo
25
+ from dstack._internal.core.services.diff import diff_models
25
26
  from dstack._internal.utils.common import local_time
26
27
  from dstack.api._public import Client
27
28
 
@@ -56,7 +57,15 @@ class GatewayConfigurator(BaseApplyConfigurator):
56
57
  confirm_message += "Create the gateway?"
57
58
  else:
58
59
  action_message += f"Found gateway [code]{plan.spec.configuration.name}[/]."
59
- if plan.current_resource.configuration == plan.spec.configuration:
60
+ diff = diff_models(
61
+ plan.spec.configuration,
62
+ plan.current_resource.configuration,
63
+ )
64
+ changed_fields = list(diff.keys())
65
+ if (
66
+ plan.current_resource.configuration == plan.spec.configuration
67
+ or changed_fields == ["default"]
68
+ ):
60
69
  if command_args.yes and not command_args.force:
61
70
  # --force is required only with --yes,
62
71
  # otherwise we may ask for force apply interactively.
@@ -25,8 +25,7 @@ LIVE_TABLE_REFRESH_RATE_PER_SEC = 1
25
25
  LIVE_TABLE_PROVISION_INTERVAL_SECS = 2
26
26
  NO_OFFERS_WARNING = (
27
27
  "[warning]"
28
- "Could not find instance offers that match your requirements."
29
- " Please check the requirements table above or visit the troubleshooting guide:"
28
+ "No matching instance offers available. Possible reasons:"
30
29
  " https://dstack.ai/docs/guides/troubleshooting/#no-offers"
31
30
  "[/]\n"
32
31
  )
@@ -21,6 +21,7 @@ from dstack._internal.core.models.volumes import MountPoint, VolumeConfiguration
21
21
  CommandsList = List[str]
22
22
  ValidPort = conint(gt=0, le=65536)
23
23
  SERVICE_HTTPS_DEFAULT = True
24
+ STRIP_PREFIX_DEFAULT = True
24
25
 
25
26
 
26
27
  class RunConfigurationType(str, Enum):
@@ -130,6 +131,16 @@ class BaseRunConfiguration(CoreModel):
130
131
  description="Use image with NVIDIA CUDA Compiler (NVCC) included. Mutually exclusive with `image`"
131
132
  ),
132
133
  ]
134
+ single_branch: Annotated[
135
+ Optional[bool],
136
+ Field(
137
+ description=(
138
+ "Whether to clone and track only the current branch or all remote branches."
139
+ " Relevant only when using remote Git repos."
140
+ " Defaults to `false` for dev environments and to `true` for tasks and services"
141
+ )
142
+ ),
143
+ ] = None
133
144
  env: Annotated[
134
145
  Env,
135
146
  Field(description="The mapping or the list of environment variables"),
@@ -236,6 +247,16 @@ class ServiceConfigurationParams(CoreModel):
236
247
  ),
237
248
  ),
238
249
  ] = None
250
+ strip_prefix: Annotated[
251
+ bool,
252
+ Field(
253
+ description=(
254
+ "Strip the `/proxy/services/<project name>/<run name>/` path prefix"
255
+ " when forwarding requests to the service. Only takes effect"
256
+ " when running the service without a gateway"
257
+ )
258
+ ),
259
+ ] = STRIP_PREFIX_DEFAULT
239
260
  model: Annotated[
240
261
  Optional[Union[AnyModel, str]],
241
262
  Field(
@@ -184,6 +184,7 @@ class JobSpec(CoreModel):
184
184
  home_dir: Optional[str]
185
185
  image_name: str
186
186
  privileged: bool = False
187
+ single_branch: Optional[bool] = None
187
188
  max_duration: Optional[int]
188
189
  stop_duration: Optional[int] = None
189
190
  registry_auth: Optional[RegistryAuth]
@@ -136,6 +136,15 @@ class VolumeMountPoint(CoreModel):
136
136
  class InstanceMountPoint(CoreModel):
137
137
  instance_path: Annotated[str, Field(description="The absolute path on the instance (host)")]
138
138
  path: Annotated[str, Field(description="The absolute path in the container")]
139
+ optional: Annotated[
140
+ bool,
141
+ Field(
142
+ description=(
143
+ "Allow running without this volume"
144
+ " in backends that do not support instance volumes"
145
+ ),
146
+ ),
147
+ ] = False
139
148
 
140
149
  _validate_instance_path = validator("instance_path", allow_reuse=True)(
141
150
  _validate_mount_point_path
@@ -42,11 +42,14 @@ class URLReplacer:
42
42
  qs = {k: v[0] for k, v in urllib.parse.parse_qs(url.query).items()}
43
43
  if app_spec and app_spec.url_query_params is not None:
44
44
  qs.update({k.encode(): v.encode() for k, v in app_spec.url_query_params.items()})
45
+ path = url.path
46
+ if not path.startswith(self.path_prefix.removesuffix(b"/")):
47
+ path = concat_url_path(self.path_prefix, path)
45
48
 
46
49
  url = url._replace(
47
50
  scheme=("https" if self.secure else "http").encode(),
48
51
  netloc=(self.hostname if omit_port else f"{self.hostname}:{local_port}").encode(),
49
- path=concat_url_path(self.path_prefix, url.path),
52
+ path=path,
50
53
  query=urllib.parse.urlencode(qs).encode(),
51
54
  )
52
55
  return url.geturl()
@@ -56,6 +56,7 @@ class SSHAttach:
56
56
  ssh_port: int,
57
57
  container_ssh_port: int,
58
58
  user: str,
59
+ container_user: str,
59
60
  id_rsa_path: PathLike,
60
61
  ports_lock: PortsLock,
61
62
  run_name: str,
@@ -74,7 +75,7 @@ class SSHAttach:
74
75
  self.control_sock_path = FilePath(control_sock_path)
75
76
  self.identity_file = FilePath(id_rsa_path)
76
77
  self.tunnel = SSHTunnel(
77
- destination=run_name,
78
+ destination=f"root@{run_name}",
78
79
  identity=self.identity_file,
79
80
  forwarded_sockets=ports_to_forwarded_sockets(
80
81
  ports=self.ports,
@@ -91,7 +92,7 @@ class SSHAttach:
91
92
  self.host_config = {
92
93
  "HostName": hostname,
93
94
  "Port": ssh_port,
94
- "User": user,
95
+ "User": user if dockerized else container_user,
95
96
  "IdentityFile": self.identity_file,
96
97
  "IdentitiesOnly": "yes",
97
98
  "StrictHostKeyChecking": "no",
@@ -111,7 +112,7 @@ class SSHAttach:
111
112
  self.container_config = {
112
113
  "HostName": "localhost",
113
114
  "Port": container_ssh_port,
114
- "User": "root", # TODO(#1535): support non-root images properly
115
+ "User": container_user,
115
116
  "IdentityFile": self.identity_file,
116
117
  "IdentitiesOnly": "yes",
117
118
  "StrictHostKeyChecking": "no",
@@ -122,7 +123,7 @@ class SSHAttach:
122
123
  self.container_config = {
123
124
  "HostName": hostname,
124
125
  "Port": ssh_port,
125
- "User": user,
126
+ "User": container_user,
126
127
  "IdentityFile": self.identity_file,
127
128
  "IdentitiesOnly": "yes",
128
129
  "StrictHostKeyChecking": "no",
@@ -136,7 +137,7 @@ class SSHAttach:
136
137
  self.host_config = {
137
138
  "HostName": hostname,
138
139
  "Port": container_ssh_port,
139
- "User": "root", # TODO(#1535): support non-root images properly
140
+ "User": container_user,
140
141
  "IdentityFile": self.identity_file,
141
142
  "IdentitiesOnly": "yes",
142
143
  "StrictHostKeyChecking": "no",
@@ -32,6 +32,7 @@ class Service(ImmutableModel):
32
32
  https: Optional[bool] # only used on gateways
33
33
  auth: bool
34
34
  client_max_body_size: int # only enforced on gateways
35
+ strip_prefix: bool = True # only used in-server
35
36
  replicas: tuple[Replica, ...]
36
37
 
37
38
  @property
@@ -29,6 +29,7 @@ def make_service(
29
29
  domain: Optional[str] = None,
30
30
  https: Optional[bool] = None,
31
31
  auth: bool = False,
32
+ strip_prefix: bool = True,
32
33
  ) -> Service:
33
34
  return Service(
34
35
  project_name=project_name,
@@ -37,6 +38,7 @@ def make_service(
37
38
  https=https,
38
39
  auth=auth,
39
40
  client_max_body_size=2**20,
41
+ strip_prefix=strip_prefix,
40
42
  replicas=(
41
43
  Replica(
42
44
  id="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
@@ -7,7 +7,7 @@ from pathlib import Path
7
7
  from typing import Awaitable, Callable, List
8
8
 
9
9
  import sentry_sdk
10
- from fastapi import FastAPI, Request, status
10
+ from fastapi import FastAPI, Request, Response, status
11
11
  from fastapi.datastructures import URL
12
12
  from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
13
13
  from fastapi.staticfiles import StaticFiles
@@ -215,10 +215,14 @@ def register_routes(app: FastAPI, ui: bool = True):
215
215
  @app.middleware("http")
216
216
  async def log_request(request: Request, call_next):
217
217
  start_time = time.time()
218
- response = await call_next(request)
218
+ response: Response = await call_next(request)
219
219
  process_time = time.time() - start_time
220
220
  logger.debug(
221
- "Processed request %s %s in %s", request.method, request.url, f"{process_time:0.6f}s"
221
+ "Processed request %s %s in %s. Status: %s",
222
+ request.method,
223
+ request.url,
224
+ f"{process_time:0.6f}s",
225
+ response.status_code,
222
226
  )
223
227
  return response
224
228
 
@@ -64,7 +64,7 @@ from dstack._internal.server.services.pools import (
64
64
  )
65
65
  from dstack._internal.server.services.runs import (
66
66
  check_can_attach_run_volumes,
67
- check_run_spec_has_instance_mounts,
67
+ check_run_spec_requires_instance_mounts,
68
68
  get_offer_volumes,
69
69
  get_run_volume_models,
70
70
  get_run_volumes,
@@ -418,7 +418,7 @@ async def _run_job_on_new_instance(
418
418
  master_job_provisioning_data=master_job_provisioning_data,
419
419
  volumes=volumes,
420
420
  privileged=job.job_spec.privileged,
421
- instance_mounts=check_run_spec_has_instance_mounts(run.run_spec),
421
+ instance_mounts=check_run_spec_requires_instance_mounts(run.run_spec),
422
422
  )
423
423
  # Limit number of offers tried to prevent long-running processing
424
424
  # in case all offers fail.
@@ -61,6 +61,7 @@ class SubmitBody(CoreModel):
61
61
  "entrypoint",
62
62
  "env",
63
63
  "gateway",
64
+ "single_branch",
64
65
  "max_duration",
65
66
  "working_dir",
66
67
  }
@@ -239,7 +239,6 @@ async def get_plan(
239
239
  user: UserModel,
240
240
  spec: FleetSpec,
241
241
  ) -> FleetPlan:
242
- # TODO: refactor offers logic into a separate module to avoid depending on runs
243
242
  current_fleet: Optional[Fleet] = None
244
243
  current_fleet_id: Optional[uuid.UUID] = None
245
244
  if spec.configuration.name is not None:
@@ -259,6 +258,7 @@ async def get_plan(
259
258
  requirements=_get_fleet_requirements(spec),
260
259
  )
261
260
  offers = [offer for _, offer in offers_with_backends]
261
+ _remove_fleet_spec_sensitive_info(spec)
262
262
  plan = FleetPlan(
263
263
  project_name=project.name,
264
264
  user=user.name,
@@ -63,6 +63,10 @@ class JobConfigurator(ABC):
63
63
  def _shell_commands(self) -> List[str]:
64
64
  pass
65
65
 
66
+ @abstractmethod
67
+ def _default_single_branch(self) -> bool:
68
+ pass
69
+
66
70
  @abstractmethod
67
71
  def _default_max_duration(self) -> Optional[int]:
68
72
  pass
@@ -104,6 +108,7 @@ class JobConfigurator(ABC):
104
108
  image_name=self._image_name(),
105
109
  user=await self._user(),
106
110
  privileged=self._privileged(),
111
+ single_branch=self._single_branch(),
107
112
  max_duration=self._max_duration(),
108
113
  stop_duration=self._stop_duration(),
109
114
  registry_auth=self._registry_auth(),
@@ -172,6 +177,11 @@ class JobConfigurator(ABC):
172
177
  def _privileged(self) -> bool:
173
178
  return self.run_spec.configuration.privileged
174
179
 
180
+ def _single_branch(self) -> bool:
181
+ if self.run_spec.configuration.single_branch is None:
182
+ return self._default_single_branch()
183
+ return self.run_spec.configuration.single_branch
184
+
175
185
  def _max_duration(self) -> Optional[int]:
176
186
  if self.run_spec.merged_profile.max_duration in [None, True]:
177
187
  return self._default_max_duration()
@@ -40,6 +40,9 @@ class DevEnvironmentJobConfigurator(JobConfigurator):
40
40
  commands += ["tail -f /dev/null"] # idle
41
41
  return commands
42
42
 
43
+ def _default_single_branch(self) -> bool:
44
+ return False
45
+
43
46
  def _default_max_duration(self) -> Optional[int]:
44
47
  return DEFAULT_MAX_DURATION_SECONDS
45
48
 
@@ -11,6 +11,9 @@ class ServiceJobConfigurator(JobConfigurator):
11
11
  def _shell_commands(self) -> List[str]:
12
12
  return self.run_spec.configuration.commands
13
13
 
14
+ def _default_single_branch(self) -> bool:
15
+ return True
16
+
14
17
  def _default_max_duration(self) -> Optional[int]:
15
18
  return None
16
19
 
@@ -25,6 +25,9 @@ class TaskJobConfigurator(JobConfigurator):
25
25
  def _shell_commands(self) -> List[str]:
26
26
  return self.run_spec.configuration.commands
27
27
 
28
+ def _default_single_branch(self) -> bool:
29
+ return True
30
+
28
31
  def _default_max_duration(self) -> Optional[int]:
29
32
  return DEFAULT_MAX_DURATION_SECONDS
30
33
 
@@ -98,6 +98,7 @@ class ServerProxyRepo(BaseProxyRepo):
98
98
  https=None,
99
99
  auth=run_spec.configuration.auth,
100
100
  client_max_body_size=DEFAULT_SERVICE_CLIENT_MAX_BODY_SIZE,
101
+ strip_prefix=run_spec.configuration.strip_prefix,
101
102
  replicas=tuple(replicas),
102
103
  )
103
104
 
@@ -12,6 +12,7 @@ from dstack._internal.proxy.lib.services.service_connection import (
12
12
  ServiceConnectionPool,
13
13
  get_service_replica_client,
14
14
  )
15
+ from dstack._internal.utils.common import concat_url_path
15
16
  from dstack._internal.utils.logging import get_logger
16
17
 
17
18
  logger = get_logger(__name__)
@@ -37,6 +38,9 @@ async def proxy(
37
38
 
38
39
  client = await get_service_replica_client(service, repo, service_conn_pool)
39
40
 
41
+ if not service.strip_prefix:
42
+ path = concat_url_path(request.scope.get("root_path", "/"), request.url.path)
43
+
40
44
  try:
41
45
  upstream_request = await build_upstream_request(request, path, client)
42
46
  except ClientDisconnect:
@@ -330,7 +330,7 @@ async def get_plan(
330
330
  multinode=jobs[0].job_spec.jobs_per_replica > 1,
331
331
  volumes=volumes,
332
332
  privileged=jobs[0].job_spec.privileged,
333
- instance_mounts=check_run_spec_has_instance_mounts(run_spec),
333
+ instance_mounts=check_run_spec_requires_instance_mounts(run_spec),
334
334
  )
335
335
 
336
336
  job_plans = []
@@ -897,9 +897,10 @@ def get_offer_mount_point_volume(
897
897
  raise ServerClientError("Failed to find an eligible volume for the mount point")
898
898
 
899
899
 
900
- def check_run_spec_has_instance_mounts(run_spec: RunSpec) -> bool:
900
+ def check_run_spec_requires_instance_mounts(run_spec: RunSpec) -> bool:
901
901
  return any(
902
- is_core_model_instance(mp, InstanceMountPoint) for mp in run_spec.configuration.volumes
902
+ is_core_model_instance(mp, InstanceMountPoint) and not mp.optional
903
+ for mp in run_spec.configuration.volumes
903
904
  )
904
905
 
905
906
 
@@ -967,7 +968,7 @@ def _validate_run_spec_and_set_defaults(run_spec: RunSpec):
967
968
  _UPDATABLE_SPEC_FIELDS = ["repo_code_hash", "configuration"]
968
969
  # Most service fields can be updated via replica redeployment.
969
970
  # TODO: Allow updating other fields when a rolling deployment is supported.
970
- _UPDATABLE_CONFIGURATION_FIELDS = ["replicas", "scaling"]
971
+ _UPDATABLE_CONFIGURATION_FIELDS = ["replicas", "scaling", "strip_prefix"]
971
972
 
972
973
 
973
974
  def _can_update_run_spec(current_run_spec: RunSpec, new_run_spec: RunSpec) -> bool:
@@ -324,11 +324,19 @@ class Run(ABC):
324
324
  if runtime_data is not None and runtime_data.ports is not None:
325
325
  container_ssh_port = runtime_data.ports.get(container_ssh_port, container_ssh_port)
326
326
 
327
+ # TODO: get login name from runner in case it's not specified in the run configuration
328
+ # (i.e. the default image user is used, and it is not root)
329
+ if job.job_spec.user is not None and job.job_spec.user.username is not None:
330
+ container_user = job.job_spec.user.username
331
+ else:
332
+ container_user = "root"
333
+
327
334
  self._ssh_attach = SSHAttach(
328
335
  hostname=provisioning_data.hostname,
329
336
  ssh_port=provisioning_data.ssh_port,
330
337
  container_ssh_port=container_ssh_port,
331
338
  user=provisioning_data.username,
339
+ container_user=container_user,
332
340
  id_rsa_path=ssh_identity_file,
333
341
  ports_lock=self._ports_lock,
334
342
  run_name=name,
@@ -133,9 +133,16 @@ class APIClient:
133
133
  else:
134
134
  raise ClientError(f"Failed to connect to dstack server {self._base_url}")
135
135
 
136
+ if 400 <= resp.status_code < 600:
137
+ logger.debug(
138
+ "Error requesting %s. Status: %s. Headers: %s. Body: %s",
139
+ resp.request.url,
140
+ resp.status_code,
141
+ resp.headers,
142
+ resp.content,
143
+ )
144
+
136
145
  if raise_for_status:
137
- if resp.status_code == 500:
138
- raise ClientError("Unexpected dstack server error")
139
146
  if resp.status_code == 400: # raise ServerClientError
140
147
  detail: List[Dict] = resp.json()["detail"]
141
148
  if len(detail) == 1 and detail[0]["code"] in _server_client_errors:
@@ -145,7 +152,16 @@ class APIClient:
145
152
  if resp.status_code == 422:
146
153
  formatted_error = pprint.pformat(resp.json())
147
154
  raise ClientError(f"Server validation error: \n{formatted_error}")
148
- resp.raise_for_status()
155
+ if resp.status_code == 403:
156
+ raise ClientError(
157
+ f"Access to {resp.request.url} is denied. Please check your access token"
158
+ )
159
+ if 400 <= resp.status_code < 600:
160
+ raise ClientError(
161
+ f"Unexpected error: status code {resp.status_code}"
162
+ f" when requesting {resp.request.url}."
163
+ " Check server logs or run with DSTACK_CLI_LOG_LEVEL=DEBUG to see more details"
164
+ )
149
165
  return resp
150
166
 
151
167
 
@@ -1,9 +1,14 @@
1
1
  from datetime import datetime
2
- from typing import List, Optional, Union
2
+ from typing import Any, List, Optional, Union
3
3
  from uuid import UUID
4
4
 
5
5
  from pydantic import parse_obj_as
6
6
 
7
+ from dstack._internal.core.models.common import is_core_model_instance
8
+ from dstack._internal.core.models.configurations import (
9
+ STRIP_PREFIX_DEFAULT,
10
+ ServiceConfiguration,
11
+ )
7
12
  from dstack._internal.core.models.pools import Instance
8
13
  from dstack._internal.core.models.profiles import Profile
9
14
  from dstack._internal.core.models.runs import (
@@ -14,6 +19,7 @@ from dstack._internal.core.models.runs import (
14
19
  RunPlan,
15
20
  RunSpec,
16
21
  )
22
+ from dstack._internal.core.models.volumes import InstanceMountPoint
17
23
  from dstack._internal.server.schemas.runs import (
18
24
  ApplyRunPlanRequest,
19
25
  CreateInstanceRequest,
@@ -117,34 +123,48 @@ class RunsAPIClient(APIClientGroup):
117
123
 
118
124
  def _get_run_spec_excludes(run_spec: RunSpec) -> Optional[dict]:
119
125
  spec_excludes: dict[str, set[str]] = {}
120
- configuration_excludes: set[str] = set()
126
+ configuration_excludes: dict[str, Any] = {}
121
127
  profile_excludes: set[str] = set()
122
128
  configuration = run_spec.configuration
123
129
  profile = run_spec.profile
124
130
 
125
131
  # client >= 0.18.18 / server <= 0.18.17 compatibility tweak
126
132
  if not configuration.privileged:
127
- configuration_excludes.add("privileged")
133
+ configuration_excludes["privileged"] = True
128
134
  # client >= 0.18.23 / server <= 0.18.22 compatibility tweak
129
135
  if configuration.type == "service" and configuration.gateway is None:
130
- configuration_excludes.add("gateway")
136
+ configuration_excludes["gateway"] = True
131
137
  # client >= 0.18.30 / server <= 0.18.29 compatibility tweak
132
138
  if run_spec.configuration.user is None:
133
- configuration_excludes.add("user")
139
+ configuration_excludes["user"] = True
134
140
  # client >= 0.18.30 / server <= 0.18.29 compatibility tweak
135
141
  if configuration.reservation is None:
136
- configuration_excludes.add("reservation")
142
+ configuration_excludes["reservation"] = True
137
143
  if profile is not None and profile.reservation is None:
138
144
  profile_excludes.add("reservation")
139
145
  if configuration.idle_duration is None:
140
- configuration_excludes.add("idle_duration")
146
+ configuration_excludes["idle_duration"] = True
141
147
  if profile is not None and profile.idle_duration is None:
142
148
  profile_excludes.add("idle_duration")
143
149
  # client >= 0.18.38 / server <= 0.18.37 compatibility tweak
144
150
  if configuration.stop_duration is None:
145
- configuration_excludes.add("stop_duration")
151
+ configuration_excludes["stop_duration"] = True
146
152
  if profile is not None and profile.stop_duration is None:
147
153
  profile_excludes.add("stop_duration")
154
+ # client >= 0.18.40 / server <= 0.18.39 compatibility tweak
155
+ if (
156
+ is_core_model_instance(configuration, ServiceConfiguration)
157
+ and configuration.strip_prefix == STRIP_PREFIX_DEFAULT
158
+ ):
159
+ configuration_excludes["strip_prefix"] = True
160
+ if configuration.single_branch is None:
161
+ configuration_excludes["single_branch"] = True
162
+ if all(
163
+ not is_core_model_instance(v, InstanceMountPoint) or not v.optional
164
+ for v in configuration.volumes
165
+ ):
166
+ configuration_excludes["volumes"] = {"__all__": {"optional"}}
167
+
148
168
  if configuration_excludes:
149
169
  spec_excludes["configuration"] = configuration_excludes
150
170
  if profile_excludes:
dstack/version.py CHANGED
@@ -1,3 +1,3 @@
1
- __version__ = "0.18.39"
1
+ __version__ = "0.18.40"
2
2
  __is_release__ = True
3
3
  base_image = "0.6"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dstack
3
- Version: 0.18.39
3
+ Version: 0.18.40
4
4
  Summary: dstack is an open-source orchestration engine for running AI workloads on any cloud or on-premises.
5
5
  Home-page: https://dstack.ai
6
6
  Author: Andrey Cheptsov
@@ -303,17 +303,16 @@ for AI workloads both in the cloud and on-prem, speeding up the development, tra
303
303
 
304
304
  #### Accelerators
305
305
 
306
- `dstack` supports `NVIDIA GPU`, `AMD GPU`, and `Google Cloud TPU` out of the box.
306
+ `dstack` supports `NVIDIA`, `AMD`, `Google TPU`, and `Intel Gaudi` accelerators out of the box.
307
307
 
308
308
  ## Major news ✨
309
309
 
310
- - [2025/01] [dstack 0.18.35: Vultr backend](https://github.com/dstackai/dstack/releases/tag/0.18.35)
311
- - [2024/12] [dstack 0.18.33: TPU v6e support](https://github.com/dstackai/dstack/releases/tag/0.18.33)
310
+ - [2025/01] [dstack 0.18.38: Intel Gaudi](https://github.com/dstackai/dstack/releases/tag/0.18.38)
311
+ - [2025/01] [dstack 0.18.35: Vultr](https://github.com/dstackai/dstack/releases/tag/0.18.35)
312
+ - [2024/12] [dstack 0.18.32: TPU v6e](https://github.com/dstackai/dstack/releases/tag/0.18.32)
312
313
  - [2024/12] [dstack 0.18.30: AWS Capacity Reservations and Capacity Blocks](https://github.com/dstackai/dstack/releases/tag/0.18.30)
313
- - [2024/11] [dstack 0.18.23: Gateway is optional](https://github.com/dstackai/dstack/releases/tag/0.18.23)
314
314
  - [2024/10] [dstack 0.18.21: Instance volumes](https://github.com/dstackai/dstack/releases/tag/0.18.21)
315
- - [2024/10] [dstack 0.18.18: Hardware metrics](https://github.com/dstackai/dstack/releases/tag/0.18.18)
316
- - [2024/10] [dstack 0.18.17: AMD support with SSH fleets, AWS EFA](https://github.com/dstackai/dstack/releases/tag/0.18.17)
315
+ - [2024/10] [dstack 0.18.18: Hardware metrics monitoring](https://github.com/dstackai/dstack/releases/tag/0.18.18)
317
316
 
318
317
  ## Installation
319
318
 
@@ -1,5 +1,5 @@
1
1
  dstack/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- dstack/version.py,sha256=7qGndtEXTRftCO2WIy-ZZmNA1UtbKIp-_gFrfNOHU3I,65
2
+ dstack/version.py,sha256=v4U-Zn6q3of9xvVoHDDcM25yWZKnFyXN_K1y22za1rw,65
3
3
  dstack/_internal/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  dstack/_internal/compat.py,sha256=bF9U9fTMfL8UVhCouedoUSTYFl7UAOiU0WXrnRoByxw,40
5
5
  dstack/_internal/settings.py,sha256=8XODoSW2joaEndvZxuHUPSFK85sGgJ7fVL976isYeJM,557
@@ -27,12 +27,12 @@ dstack/_internal/cli/services/profile.py,sha256=Fl052TeMCbWO1Q6HiaPVD9CdfClzZAkk
27
27
  dstack/_internal/cli/services/repos.py,sha256=n2INRJtTYV5y60-Yp3F-V5kJkqwZI_fdGXPnxvZhkNw,3321
28
28
  dstack/_internal/cli/services/configurators/__init__.py,sha256=z94VPBFqybP8Zpwy3CzYxmpPAqYBOvRRLpXoz2H4GKI,2697
29
29
  dstack/_internal/cli/services/configurators/base.py,sha256=YsyNmHTv1wVbvaAjQi_qNQAb4oK9T_Wgu6RKg-uyzvY,3379
30
- dstack/_internal/cli/services/configurators/fleet.py,sha256=YbZh6x1UuvECh7ZMmxvQQfGzBUi0Kxn3stsVutKWLKo,14259
31
- dstack/_internal/cli/services/configurators/gateway.py,sha256=0pwAPFYoANioeh6RJiq5IggMc4ywAVfEz3hX91FoPaY,8061
30
+ dstack/_internal/cli/services/configurators/fleet.py,sha256=iqNJSoji7IiVb3Kb4KA6XTzI5k2xUYF7DK9aE7iverk,14281
31
+ dstack/_internal/cli/services/configurators/gateway.py,sha256=BlLRBC-rl9a0xpO3c1N4k5yinvVQB5YU238g-8NMINY,8389
32
32
  dstack/_internal/cli/services/configurators/run.py,sha256=GLGhCcrOzp96Yc6RiISg-DZEM43WT7bhsZmD8z0oAVA,23271
33
33
  dstack/_internal/cli/services/configurators/volume.py,sha256=UAKfx-J5fUdnnR2RBvbRJy0YKzXOoub4f4ywMsZwk38,7983
34
34
  dstack/_internal/cli/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
35
- dstack/_internal/cli/utils/common.py,sha256=xuHxRG6pl9Amt8PRbpu0wav6P5ac2wtLUMBj0URxRSc,1852
35
+ dstack/_internal/cli/utils/common.py,sha256=rfmzqrsgR3rXW3wj0vxDdvrhUUg2aIy4A6E9MZbd55g,1763
36
36
  dstack/_internal/cli/utils/fleet.py,sha256=tumFG4qgrKjmb0Iik64tWcPrenaOjpeXC6AZSezQ3_A,2991
37
37
  dstack/_internal/cli/utils/gateway.py,sha256=jMytH6u3x8hctMhm9bcmXLJxSgTXmpW8M9abATQrw3c,1474
38
38
  dstack/_internal/cli/utils/rich.py,sha256=Gx1MJU929kMKsbdo9qF7XHARNta2426Ssb-xMLVhwbQ,5710
@@ -114,7 +114,7 @@ dstack/_internal/core/backends/vultr/config.py,sha256=TqjT2yhW9rAiDpJ96gBFTM638F
114
114
  dstack/_internal/core/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
115
115
  dstack/_internal/core/models/common.py,sha256=XNfEP_lHAs398oDfSVn8kCX79GIWvl0hDs05Rz1CBic,2579
116
116
  dstack/_internal/core/models/config.py,sha256=JJ7rT7dztzTWCY5TkoyxXxTvG5D4IFYhGe7EzwkLOWQ,581
117
- dstack/_internal/core/models/configurations.py,sha256=0ZCQ7atvMnIKvpK1FFmw7IXox3WvAgyfqAtaJmWCLns,13030
117
+ dstack/_internal/core/models/configurations.py,sha256=BrxbkQ5xhCw-pT7DsEdJeLa92uEWQM3LHPmSiYOohHE,13806
118
118
  dstack/_internal/core/models/envs.py,sha256=ls2ahIhEb3LYLX_auH3F46uTRp2zdlMWKEpjmU_V3S4,5017
119
119
  dstack/_internal/core/models/fleets.py,sha256=tSAFkBugchMhvLRk8c0loRpteTYnbA0sDhUI8q_EcXI,8969
120
120
  dstack/_internal/core/models/gateways.py,sha256=n9lPq6X0n2a7fF-CseiBVDDwasJsU3VQumTetMsfZcc,3465
@@ -126,13 +126,13 @@ dstack/_internal/core/models/pools.py,sha256=mZSZ_9CKTIhnoXfkQLEOjJg_EDZW7kXXigC
126
126
  dstack/_internal/core/models/profiles.py,sha256=CRvVf2bph56HOz7OmpCwreOR2yCHdfVgovyjKxYBsiM,8011
127
127
  dstack/_internal/core/models/projects.py,sha256=hY3rhLWkuDRsavAdNvQaK6IhnTr7yBHTmNwy9k7zjVE,643
128
128
  dstack/_internal/core/models/resources.py,sha256=AzBqGEZLHmg-HpjQtcb_W4pJQRxRtDyeuiHO4berdBA,12408
129
- dstack/_internal/core/models/runs.py,sha256=fHpvKW_4LfhhtbzvTHwVXRye6gzQikclmEudKPGQEHk,16930
129
+ dstack/_internal/core/models/runs.py,sha256=CqyQ_wMne7G9XHLm2BRH84J0tgG3Ny77No1A8hTCmgQ,16971
130
130
  dstack/_internal/core/models/secrets.py,sha256=OlnBI8ESBnpjSqB0-Vr3z8JcqB2Ydfiwo8IJUuM5jAc,219
131
131
  dstack/_internal/core/models/server.py,sha256=Hkc1v2s3KOiwslsWVmhUOAzcSeREoG-HD1SzSX9WUGg,152
132
132
  dstack/_internal/core/models/services.py,sha256=2Hpi7j0Q1shaf_0wd0C0044AJAmuYi-D3qx3PH849oI,3076
133
133
  dstack/_internal/core/models/unix.py,sha256=KxnSQELnkAjjuUgYcQKVkf-UAbYREBD8WCWDvHfOkuA,1915
134
134
  dstack/_internal/core/models/users.py,sha256=o_rd0GAmd6jufypVUs9P12NRri3rgAPDt-KxnqNNsGw,703
135
- dstack/_internal/core/models/volumes.py,sha256=piArLbgpUyejRJW2vtluqmqXhLmyj9rhzsCfPPGAZ50,4975
135
+ dstack/_internal/core/models/volumes.py,sha256=MagqSb9jB2WnQlP-xVYkkPYHtKh9r54ajVEUYNF9iKc,5215
136
136
  dstack/_internal/core/models/backends/__init__.py,sha256=iPMAv4j7gpHc0cjt3SxzQGb-sywms8LUXt7IjtKNPnM,5589
137
137
  dstack/_internal/core/models/backends/aws.py,sha256=H0RB7pTVb7vjW_-MDpBBwPLbAoKDRtYFHe_n4ppXz6w,2463
138
138
  dstack/_internal/core/models/backends/azure.py,sha256=u5R8vLVS85X27P17IZ1L-EcXmxg08sC0E1EFxkVG2YA,1996
@@ -157,12 +157,12 @@ dstack/_internal/core/models/repos/virtual.py,sha256=1p6k2vBfWI6UT91os3lZfV5wl8b
157
157
  dstack/_internal/core/services/__init__.py,sha256=Utrh2zbYsP6IBGWMGROaHUefgo_TvLloN3logcdhuC4,282
158
158
  dstack/_internal/core/services/api_client.py,sha256=HTQ0fcZciUh-nmfV09hUt__Z4N2zq_R12xAiYXD0Jy0,650
159
159
  dstack/_internal/core/services/diff.py,sha256=lw7vTOYnGtRWlEulgVlHt1KjLdC8VJpfBcqMRuFZzSQ,532
160
- dstack/_internal/core/services/logs.py,sha256=63s5OKyrZEs-0iLBKRFSn5VldelyWY75gD4RHj_Ejlo,2053
160
+ dstack/_internal/core/services/logs.py,sha256=7_eJdH4MD-3rVb4A6rIJfjj_p4jzUOCmjRVlPD-UDsg,2166
161
161
  dstack/_internal/core/services/profiles.py,sha256=SJGT6LG8JmrfsqlyXOkPZd61BDwZ4Q1ngcmDVgq826M,2160
162
162
  dstack/_internal/core/services/repos.py,sha256=YgTmLX8U1Y5cz2dG9yThK1-Geq02iik_C6DGZ0y6uEY,5311
163
163
  dstack/_internal/core/services/configs/__init__.py,sha256=2jwu1w0vEDNcKGdeI15SS4EtpLSZzRpG8xZslHgTJmY,5484
164
164
  dstack/_internal/core/services/ssh/__init__.py,sha256=UhgC3Lv3CPSGqSPEQZIKOfLKUlCFnaB0uqPQhfKCFt0,878
165
- dstack/_internal/core/services/ssh/attach.py,sha256=4KNiHnM5vsJ8SNWcKGRqhZSKygxDoGLfMBkIAICLHEE,7488
165
+ dstack/_internal/core/services/ssh/attach.py,sha256=A-A8H9lwdIFrxk6FOtHaCZ02fNXo73yz4c-QN1_BQ98,7489
166
166
  dstack/_internal/core/services/ssh/client.py,sha256=FUg74jNOagYR3OKuWMBwNgJWIDwHfYrIMSER8xn92D8,4102
167
167
  dstack/_internal/core/services/ssh/ports.py,sha256=yWgcACqTNwfLMG24U28LYXlZuEb1V7cSiYBQBqVN5yU,2884
168
168
  dstack/_internal/core/services/ssh/tunnel.py,sha256=lHNK1hEcqMP_T_Y6cM_OLHVIRCEA4hud-sM5Jmp4svM,10296
@@ -201,7 +201,7 @@ dstack/_internal/proxy/lib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJ
201
201
  dstack/_internal/proxy/lib/auth.py,sha256=bI85UFTupPxlcAJZqpOuqS64I4MeIyzp_gHxM3916V4,183
202
202
  dstack/_internal/proxy/lib/deps.py,sha256=fFq1mJQK_XcqCNgORTowhc0jCblRiGIK1TeRDC2wkL8,3538
203
203
  dstack/_internal/proxy/lib/errors.py,sha256=sUO1dDMkr0oKghB0yUdoHLhB66tX--FkHWrJ5jzPB4w,437
204
- dstack/_internal/proxy/lib/models.py,sha256=MC8ATwPdgXi6l2SCQjuR8WFp-yuuhCn0OD8DU9QXZsg,2625
204
+ dstack/_internal/proxy/lib/models.py,sha256=duixvPYU6lnusp3tYjxU675Zo3idXfOiAPQ1ZRnv1h8,2678
205
205
  dstack/_internal/proxy/lib/repo.py,sha256=zkWZ9XZzQHfCa-eifec7H7UYnJZLgeRuiQls7Rc2pkA,781
206
206
  dstack/_internal/proxy/lib/routers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
207
207
  dstack/_internal/proxy/lib/routers/model_proxy.py,sha256=57GFRpVRXcVY-347HnUSUr4w4RsxsjLuuZiJs8DwDpM,3895
@@ -217,9 +217,9 @@ dstack/_internal/proxy/lib/services/model_proxy/clients/openai.py,sha256=K8cMgGQ
217
217
  dstack/_internal/proxy/lib/services/model_proxy/clients/tgi.py,sha256=7TsqrlUVo2PYdEIbmmGNTz0ktf_T8l4Zpw4w4HYAYTs,7845
218
218
  dstack/_internal/proxy/lib/testing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
219
219
  dstack/_internal/proxy/lib/testing/auth.py,sha256=-firWTnis9Eogoi58BURv1S-te4Hf9x1Q_aYLZMDll4,465
220
- dstack/_internal/proxy/lib/testing/common.py,sha256=peH9vQF0g4wsJCcF_FasKOXmSlCFVSEuwOBpA0CHANc,1452
220
+ dstack/_internal/proxy/lib/testing/common.py,sha256=5e2VYboMqjBnUxkvidaWLoQ-uaBGh_hnURb_VJc38q4,1518
221
221
  dstack/_internal/server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
222
- dstack/_internal/server/app.py,sha256=Bz875CPuYuewbqoo7slo9b1UckLWz1TTvBJtu50o9qI,11214
222
+ dstack/_internal/server/app.py,sha256=pPto7JaBEkwvPF2FR8sqDzwIyMovI_85xk6WXfnnCYU,11317
223
223
  dstack/_internal/server/db.py,sha256=WjuqmjG3QAZmSMCeUaJ_ynbowlHuNAvYCZO649cTPHc,3210
224
224
  dstack/_internal/server/deps.py,sha256=31e8SU_ogPJWHIDLkgl7cuC_5V91xbJoLyAj17VanfM,670
225
225
  dstack/_internal/server/main.py,sha256=kztKhCYNoHSDyJJQScWfZXE0naNleJOCQULW6dd8SGw,109
@@ -234,7 +234,7 @@ dstack/_internal/server/background/tasks/process_metrics.py,sha256=99Uu41B-XS7QZ
234
234
  dstack/_internal/server/background/tasks/process_placement_groups.py,sha256=DZctiMF2TXKbYeygoz--ZiC9MXVL1sQJgq0oqRIc7hg,3858
235
235
  dstack/_internal/server/background/tasks/process_running_jobs.py,sha256=9vRGefwyjBagmOyot5tjBi6tXP9gwycdrLGqs_-Iu14,27634
236
236
  dstack/_internal/server/background/tasks/process_runs.py,sha256=LPe0RvDmNGSnIT2RNEYh5TceFx69z_JPYxwpU8mP9kQ,17254
237
- dstack/_internal/server/background/tasks/process_submitted_jobs.py,sha256=F-w8njVMXF_fmKZayzptChCn-Unn4dTq7WDMAZ0sFv4,24354
237
+ dstack/_internal/server/background/tasks/process_submitted_jobs.py,sha256=CkLdkt3D59RK3UkzLqvHkXZty2Zpf-Ksk3mmVQskHgg,24364
238
238
  dstack/_internal/server/background/tasks/process_terminating_jobs.py,sha256=Y_ISp_lY0S0H-Qn7s5t-jVBYiBPoS7080LM5HbP1pRY,3745
239
239
  dstack/_internal/server/background/tasks/process_volumes.py,sha256=UoMtYjX6MbSoMXOOuBhA0FVIlQMT_wZWsRNeYVV3cSI,4703
240
240
  dstack/_internal/server/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -319,7 +319,7 @@ dstack/_internal/server/schemas/logs.py,sha256=JGt39fBEFRjHhlGT1jIC6kwQhujxPO8ue
319
319
  dstack/_internal/server/schemas/pools.py,sha256=gPUUXLVwwud_nyOc5cXfSqz-6ladlnBb4Uzu4SJgCg4,734
320
320
  dstack/_internal/server/schemas/projects.py,sha256=-YIaAYxMgOMKtw-5xPTmmBBwa_2tjJ3jqVPYEnOeTF8,437
321
321
  dstack/_internal/server/schemas/repos.py,sha256=dRmT4CEs1lXN_kB8ZKtL1aZGLuBy-zGfPxLXTn4FwBY,2027
322
- dstack/_internal/server/schemas/runner.py,sha256=Nq4vQHsOiry2iIET2KvBYVCJkQlalW6QqjqxWNXW2Vw,4159
322
+ dstack/_internal/server/schemas/runner.py,sha256=Avqfu4iorAta_0BTWXfk_ydz4V0Eu0_6ayo3DHzH8HQ,4192
323
323
  dstack/_internal/server/schemas/runs.py,sha256=Tp-_q_XijeuH-CR14y5xM1-B4TfTDSzWouVCbsg3NsU,1774
324
324
  dstack/_internal/server/schemas/secrets.py,sha256=mfqLSM7PqxVQ-GIWB6RfPRUOvSvvaRv-JxXAYxZ6dyY,373
325
325
  dstack/_internal/server/schemas/users.py,sha256=FuDqwRVe3mOmv497vOZKjI0a_d4Wt2g4ZiCJcyfHEKA,495
@@ -329,7 +329,7 @@ dstack/_internal/server/security/permissions.py,sha256=K756I8fnBLBrq4PZwz1ttt74H
329
329
  dstack/_internal/server/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
330
330
  dstack/_internal/server/services/config.py,sha256=rXGihEDrfdDJtDsfeYlbHQW8eGej56VDYTV6BJ_aWjg,29241
331
331
  dstack/_internal/server/services/docker.py,sha256=3EcYPiVsrNBGDQYOb60QJ241mzTT6lJROYQXIwt-8dk,5351
332
- dstack/_internal/server/services/fleets.py,sha256=XmZWulyVLxjWRDhVVTqh9Z12TPdGZR6B8RBZdg0P__s,28038
332
+ dstack/_internal/server/services/fleets.py,sha256=SOU6m-EVbRdpfTkT7I0UUyVXQJTF4_zpcSNWEIb1lQo,27998
333
333
  dstack/_internal/server/services/locking.py,sha256=UV5kc-BgROBuNDpBrp8jkcf-fHPOpHLc8JufL-8mbN8,2453
334
334
  dstack/_internal/server/services/logging.py,sha256=Nu1628kW2hqB__N0Eyr07wGWjVWxfyJnczonTJ72kSM,417
335
335
  dstack/_internal/server/services/logs.py,sha256=53pymPDaM9-xXHFzCyEHdvM49JPTpsLI2aPTSP5zaPo,20090
@@ -340,7 +340,7 @@ dstack/_internal/server/services/placement.py,sha256=DWZ8-iAE3o0J0xaHikuJYZzpuBi
340
340
  dstack/_internal/server/services/pools.py,sha256=nkYsYb47lBnPswl3GleNvm30OIhRw4fPH7Z9PJpmhyo,25154
341
341
  dstack/_internal/server/services/projects.py,sha256=HWOnFOC6LmvbjDzRKADv7tXKIDWthbO11zrsHU7vkew,14709
342
342
  dstack/_internal/server/services/repos.py,sha256=f9ztN7jz_2gvD9hXF5sJwWDVyG2-NHRfjIdSukowPh8,9342
343
- dstack/_internal/server/services/runs.py,sha256=PzDBJGI_RJ_KKNgZ0SKE0R3XfY9Oj1nZrJ8_qfj7b4I,40530
343
+ dstack/_internal/server/services/runs.py,sha256=W8y7oOzLG_b2qsQar7UG0482NUhcWIGCnqFgrUmiGr4,40584
344
344
  dstack/_internal/server/services/storage.py,sha256=6I0xI_3_RpJNbKZwHjDnjrEwXGdHfiaeb5li15T-M1I,1884
345
345
  dstack/_internal/server/services/users.py,sha256=L-exfxHdhj3TKX-gSjezHrYK6tnrt5qsQs-zZng1tUI,7123
346
346
  dstack/_internal/server/services/volumes.py,sha256=GMHUlSjeiIc9LSWKes-ZPRKgyvWD6y1VT8w6mH-URtg,12635
@@ -372,21 +372,21 @@ dstack/_internal/server/services/gateways/connection.py,sha256=ot3lV85XdmCT45vBW
372
372
  dstack/_internal/server/services/gateways/pool.py,sha256=0LclTl1tyx-doS78LeaAKjr-SMp98zuwh5f9s06JSd0,1914
373
373
  dstack/_internal/server/services/jobs/__init__.py,sha256=cgY4Tp66zIxIxsW_oj8_JUpEnUXuTutwhewzx2off2Y,19593
374
374
  dstack/_internal/server/services/jobs/configurators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
375
- dstack/_internal/server/services/jobs/configurators/base.py,sha256=gfcUY5dpJu3LfRsD683TNvfBgb8VrDSEfJStCSZsPdM,8347
376
- dstack/_internal/server/services/jobs/configurators/dev.py,sha256=V9e1M2Fyb4j4PzjASrkORtyPJN6Gh-YSClQBW2ZJnjA,1950
377
- dstack/_internal/server/services/jobs/configurators/service.py,sha256=2LnaFWq0vLieX6amjTPh2Upu4GKvKlz_THUrb8Nlo9k,862
378
- dstack/_internal/server/services/jobs/configurators/task.py,sha256=zq_J3Pksn2fBWgvexOu7EJln480zVTwINVpRo9LgMwM,1435
375
+ dstack/_internal/server/services/jobs/configurators/base.py,sha256=uqT5bQWjf8IcYrD85_-WYEYoFyMYYn-FAc-sqKE-zQA,8683
376
+ dstack/_internal/server/services/jobs/configurators/dev.py,sha256=2EEtfOpNXP5d9b3ZErE3dA3Co8aAAv1fbx9wAjRZpGY,2018
377
+ dstack/_internal/server/services/jobs/configurators/service.py,sha256=FOWrLE-6YFSMuGqjOwYTLMV4FuIM5lCMDFjS0l0CoLI,929
378
+ dstack/_internal/server/services/jobs/configurators/task.py,sha256=ojCgdMyGak8u00tdu-th6U6w0pGRENV1mPtajwzeA_s,1502
379
379
  dstack/_internal/server/services/jobs/configurators/extensions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
380
380
  dstack/_internal/server/services/jobs/configurators/extensions/base.py,sha256=xJbHxaaSJ1zjn8zuuApP1Xt2uBaedPhhc-IY0NtDDJQ,418
381
381
  dstack/_internal/server/services/jobs/configurators/extensions/vscode.py,sha256=DAj8OEVLyL1x8Jko2EXKhnAkcSnlO1sJk6o6eiiVkDI,1611
382
382
  dstack/_internal/server/services/proxy/__init__.py,sha256=aklmvGaGXISztQft-nH8R98WRU6x_L0Fx3RwVDhwt3g,85
383
383
  dstack/_internal/server/services/proxy/auth.py,sha256=AVhDCnk9KvxJ7Jsd8Xmotl_29V4lNuBw8mQNBhx90Ws,485
384
384
  dstack/_internal/server/services/proxy/deps.py,sha256=u4dHHKBlTgC8Fnnl3onErgE1HGKFwyZTwvq0uT6IdQ4,846
385
- dstack/_internal/server/services/proxy/repo.py,sha256=MRJ7n3PM1lgGGZeQc1-Va5eWiGCafSMgPm3FLpBlE5I,6326
385
+ dstack/_internal/server/services/proxy/repo.py,sha256=JaEgvTsWREoI4aK16qdiRWWlJEV8JdyRnjRK90uylqM,6388
386
386
  dstack/_internal/server/services/proxy/routers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
387
387
  dstack/_internal/server/services/proxy/routers/service_proxy.py,sha256=8rsuenv9LUlFJghV8Ly4vewGmc4pDdVTyuU0xiNLuVQ,1581
388
388
  dstack/_internal/server/services/proxy/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
389
- dstack/_internal/server/services/proxy/services/service_proxy.py,sha256=uhu6GoUR6bqSBC-uBjn1gc8j-cDznTOAylxJU8MKucA,4701
389
+ dstack/_internal/server/services/proxy/services/service_proxy.py,sha256=4JrSxHqhBYqU1oENii89Db-bzkFWExYrOy-0mNEhWBs,4879
390
390
  dstack/_internal/server/services/runner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
391
391
  dstack/_internal/server/services/runner/client.py,sha256=G8LTHOfZTqGpkSiv1ceJyckN-o4Xk3Zf4pDt_Da3acI,14805
392
392
  dstack/_internal/server/services/runner/ssh.py,sha256=RlfnCS3d6rx06qYUXXbCY7aAjRdxe6Q6A5VbaRwzEbk,3933
@@ -524,9 +524,9 @@ dstack/api/_public/backends.py,sha256=IBeotauJzlte379LzRM6ByZCl_98QemGZsm3p_WX98
524
524
  dstack/api/_public/pools.py,sha256=wlTai0px6aQNeKyL-6nOil6DCj-GdozqqDGmnTg4ubw,1033
525
525
  dstack/api/_public/repos.py,sha256=LYWy1W3Z-2y_D82Jln6YzU2F_asPULGJq1GM9HAAMIc,6173
526
526
  dstack/api/_public/resources.py,sha256=lTQM2nb7_QB2RCwmpQlBkYkpiN_aWjgF78wI7Op_RlQ,2947
527
- dstack/api/_public/runs.py,sha256=qPGu95Yd4cy6MqkThXURpxHMT_mhK1d6H0WfGg_2-44,24292
527
+ dstack/api/_public/runs.py,sha256=3527ziltD9UNTDPMkaHHUXXQog2VLFXKQ0zgmRsnCb4,24718
528
528
  dstack/api/huggingface/__init__.py,sha256=oIrEij3wttLZ1yrywEGvCMd6zswMQrX5pPjrqdSi0UA,2201
529
- dstack/api/server/__init__.py,sha256=l1SWc9szAYtcBhXugkReq3NZQUl04XIrIHtASBEDJE0,5142
529
+ dstack/api/server/__init__.py,sha256=1Sw1bB8NItIFWlL1bdeUN1Tuwu3uaYOJWFrg1c4VULY,5801
530
530
  dstack/api/server/_backends.py,sha256=mMePYgFMHAUma3Gycx54VsaxAL_IFhBRcI4H4eWWMg0,1908
531
531
  dstack/api/server/_fleets.py,sha256=aB8eJtiQDKjhBxVkkYhQCaiF0Q5hWeJOXuTFs7SwnBg,3870
532
532
  dstack/api/server/_gateways.py,sha256=_OcFOOxUdh4YIA15ZNQQT6mfj0cSKP2jJ_bQXxugS_w,2568
@@ -536,7 +536,7 @@ dstack/api/server/_metrics.py,sha256=OPb8sLhI_U605sHOPrELgy0_6cNFLJVfpvr-qkEukRM
536
536
  dstack/api/server/_pools.py,sha256=PSs8R4FAKZk-jtD4GmATceMyipnasOy2XEg-8lmiN28,2715
537
537
  dstack/api/server/_projects.py,sha256=g6kNSU6jer8u7Kaut1I0Ft4wRMLBBCQShJf3fOB63hQ,1440
538
538
  dstack/api/server/_repos.py,sha256=gf9fAQw3rzqfPGq-uT8I2Ju_Zn6G4aWTKis8gb8Polc,1885
539
- dstack/api/server/_runs.py,sha256=YF9ezqBKC4mBR8Nf79ypHx8yT-ur4GehoQ68EZuEpwQ,6100
539
+ dstack/api/server/_runs.py,sha256=C8vE8uLRjiftRo_wKrr2wpcd_416qy2CsgUNXS3u_yY,6946
540
540
  dstack/api/server/_secrets.py,sha256=VqLfrIcmBJtPxNDRkXTG44H5SWoY788YJapScUukvdY,1576
541
541
  dstack/api/server/_users.py,sha256=XzhgGKc5Tsr0-xkz3T6rGyWZ1tO7aYNhLux2eE7dAoY,1738
542
542
  dstack/api/server/_volumes.py,sha256=E5VlZY44DRkaRUT0swrftYDoEgicDoVIBzzpYkiJhkw,1356
@@ -578,7 +578,7 @@ tests/_internal/core/models/test_volumes.py,sha256=V47bMK5UI1-1wrTl_rq-WClAF_OjC
578
578
  tests/_internal/core/models/repos/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
579
579
  tests/_internal/core/models/repos/test_remote.py,sha256=6PQcuk9pVfQFG97Vpj_blkK2Veq8tvP6oRg4MUC9mX4,3041
580
580
  tests/_internal/core/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
581
- tests/_internal/core/services/test_logs.py,sha256=uM4en0RtLbVm_OOVgYU1nsE2_tY03zGzv4civY9TNZE,5290
581
+ tests/_internal/core/services/test_logs.py,sha256=EI4qmFTPWEP-SXYMcXddSlrAqz03PTdx6VRvYl3Jinw,5732
582
582
  tests/_internal/core/services/ssh/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
583
583
  tests/_internal/core/services/ssh/test_client.py,sha256=xR8O8DiCtPVRxBILiQLS921XjdxzE3CMXHAQt635_HU,2537
584
584
  tests/_internal/core/services/ssh/test_tunnel.py,sha256=icgac2ni8SMgh49uwtPll_vxJu0lLN_py1oHXAo4yqU,7317
@@ -610,7 +610,7 @@ tests/_internal/server/background/tasks/test_process_metrics.py,sha256=k6L0biz7E
610
610
  tests/_internal/server/background/tasks/test_process_placement_groups.py,sha256=shHqZvS8QoNwQe8J29-aHk2X2HqX-gsxcQiZevU8yuY,1528
611
611
  tests/_internal/server/background/tasks/test_process_running_jobs.py,sha256=EzdAYVUowb9cGb_bdvKG11jTHMuL9D48brXVD4V6GFY,18812
612
612
  tests/_internal/server/background/tasks/test_process_runs.py,sha256=Ks3K1O-BMyYSbMr-1L76IEzZ9aLQaVjxF6go-Nj-HxY,13668
613
- tests/_internal/server/background/tasks/test_process_submitted_jobs.py,sha256=sWXMhti6ws4CBXHOhHypt6IbslNYkO9E0Ai689u3jNE,19767
613
+ tests/_internal/server/background/tasks/test_process_submitted_jobs.py,sha256=K4vdsvTWQfcRWVVAN_yRA8FBqkntIvwnZAjZCObWPsk,22633
614
614
  tests/_internal/server/background/tasks/test_process_submitted_volumes.py,sha256=njfRDdDzVtDL8rech_004Wf5z5PpoQVWFufKh1yUyYY,2171
615
615
  tests/_internal/server/background/tasks/test_process_terminating_jobs.py,sha256=hatdc4ANgQvJiVLfR9jE3URQjzqFD9FI6ZG9ES_vJOo,9541
616
616
  tests/_internal/server/routers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -623,7 +623,7 @@ tests/_internal/server/routers/test_metrics.py,sha256=NSdb_x_1Nwsqgrf6KXBf257qd2
623
623
  tests/_internal/server/routers/test_pools.py,sha256=pqPfIengCzI74D8ojYZbswEtGYhBXFtAlCvzg2RQr6c,25027
624
624
  tests/_internal/server/routers/test_projects.py,sha256=Pa_YL9lYhYt06TFmuZFEf9pxc-K3fM9L29ULf4gCELw,23950
625
625
  tests/_internal/server/routers/test_repos.py,sha256=4O4mCDlAPh8xJU_XWtwLhFWRkoTxXvZC1N2DLPyz2AI,17225
626
- tests/_internal/server/routers/test_runs.py,sha256=6BDz8MJLkeOxIiGmhZ0bGsNAqzPfeaFsTIp3RISrOTY,70375
626
+ tests/_internal/server/routers/test_runs.py,sha256=tjY_Q3HVpKky45lCq2TeT_hEhsCqXyCHZnofXy-NYPM,70541
627
627
  tests/_internal/server/routers/test_server.py,sha256=ROkuRNNJEkMQuK8guZ3Qy3iRRfiWvPIJJJDc09BI0D4,489
628
628
  tests/_internal/server/routers/test_users.py,sha256=5QSLvfn9SroGsZoBGmSTEaaqJcdEO8EUUy_YuvLL8ss,12787
629
629
  tests/_internal/server/routers/test_volumes.py,sha256=4nv7ba9VkjbYFyYyQmO0coyav_Z9x5t_ayoiyKmvXVw,15947
@@ -641,9 +641,9 @@ tests/_internal/server/services/encryption/keys/__init__.py,sha256=47DEQpj8HBSa-
641
641
  tests/_internal/server/services/encryption/keys/test_aes.py,sha256=BtDb5ZeXKKNkAg7KOLXKjpQivMIWpmZe5zaJg-JHFo0,601
642
642
  tests/_internal/server/services/proxy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
643
643
  tests/_internal/server/services/proxy/routers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
644
- tests/_internal/server/services/proxy/routers/test_service_proxy.py,sha256=L-5eubZeixAWsN0qu0v82Y10jUIEvXOmot4GibEcSj8,8263
644
+ tests/_internal/server/services/proxy/routers/test_service_proxy.py,sha256=cGyBwfSgT268CRhkgpTEIM3O1U9z8tSNTYjHQrn3e0E,9843
645
645
  tests/_internal/server/services/runner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
646
- tests/_internal/server/services/runner/test_client.py,sha256=9Z18Cd5haCBRfNafA1-HMl9Mus0yeNmougHJVM_XeUY,16114
646
+ tests/_internal/server/services/runner/test_client.py,sha256=LRJzmyMOGtURc-S4uICfhHjO2K0fxvCa--yXflnoTDM,16212
647
647
  tests/_internal/server/services/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
648
648
  tests/_internal/server/services/services/test_autoscalers.py,sha256=YMi4W5gKFOpEdutc2Fli1L1DIUxCJLHE7ji-CJoiVac,3986
649
649
  tests/_internal/server/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -658,9 +658,9 @@ tests/_internal/utils/test_path.py,sha256=rzS-1YCxsFUocBe42dghLOMFNymPruGrA7bqFZ
658
658
  tests/_internal/utils/test_ssh.py,sha256=V-cBFPhD--9eM9d1uQQgpj2gnYLA3c43f4cX9uJ6E-U,1743
659
659
  tests/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
660
660
  tests/api/test_utils.py,sha256=SSSqHcNE5cZVqDq4n2sKZthRoXaZ_Bx7z1AAN5xTM9s,391
661
- dstack-0.18.39.dist-info/LICENSE.md,sha256=qDABaRGjSKVOib1U8viw2P_96sIK7Puo426784oD9f8,15976
662
- dstack-0.18.39.dist-info/METADATA,sha256=CYXwEOvMQeQdMnOMUUWcVf-8g2zMRwMnqzY1SFee_ic,17813
663
- dstack-0.18.39.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
664
- dstack-0.18.39.dist-info/entry_points.txt,sha256=GnLrMS8hx3rWAySQjA7tPNhtixV6a-brRkmal1PKoHc,58
665
- dstack-0.18.39.dist-info/top_level.txt,sha256=3BrIO1zrqxT9P20ymhRM6k15meZXzbPL6ykBlDZG2_k,13
666
- dstack-0.18.39.dist-info/RECORD,,
661
+ dstack-0.18.40.dist-info/LICENSE.md,sha256=qDABaRGjSKVOib1U8viw2P_96sIK7Puo426784oD9f8,15976
662
+ dstack-0.18.40.dist-info/METADATA,sha256=yWuhZjhn3TFZLBfOUVx5ZANLFkIDGbPWufBypQko9Kk,17690
663
+ dstack-0.18.40.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
664
+ dstack-0.18.40.dist-info/entry_points.txt,sha256=GnLrMS8hx3rWAySQjA7tPNhtixV6a-brRkmal1PKoHc,58
665
+ dstack-0.18.40.dist-info/top_level.txt,sha256=3BrIO1zrqxT9P20ymhRM6k15meZXzbPL6ykBlDZG2_k,13
666
+ dstack-0.18.40.dist-info/RECORD,,
@@ -1,3 +1,5 @@
1
+ import pytest
2
+
1
3
  from dstack._internal.core.models.runs import AppSpec
2
4
  from dstack._internal.core.services.logs import URLReplacer
3
5
 
@@ -126,7 +128,18 @@ class TestServiceURLReplacer:
126
128
  )
127
129
  assert replacer(b"http://0.0.0.0:8000/qwerty") == b"https://secure.host.com/qwerty"
128
130
 
129
- def test_in_server_proxy(self):
131
+ @pytest.mark.parametrize(
132
+ ("in_path", "out_path"),
133
+ [
134
+ ("", "/proxy/services/main/service/"),
135
+ ("/", "/proxy/services/main/service/"),
136
+ ("/a/b/c", "/proxy/services/main/service/a/b/c"),
137
+ ("/proxy/services/main/service", "/proxy/services/main/service"),
138
+ ("/proxy/services/main/service/", "/proxy/services/main/service/"),
139
+ ("/proxy/services/main/service/a/b/c", "/proxy/services/main/service/a/b/c"),
140
+ ],
141
+ )
142
+ def test_adds_prefix_unless_already_present(self, in_path: str, out_path: str) -> None:
130
143
  replacer = URLReplacer(
131
144
  ports={8888: 3000},
132
145
  app_specs=[],
@@ -135,9 +148,6 @@ class TestServiceURLReplacer:
135
148
  path_prefix="/proxy/services/main/service/",
136
149
  )
137
150
  assert (
138
- replacer(b"http://0.0.0.0:8888") == b"http://0.0.0.0:3000/proxy/services/main/service/"
139
- )
140
- assert (
141
- replacer(b"http://0.0.0.0:8888/qwerty")
142
- == b"http://0.0.0.0:3000/proxy/services/main/service/qwerty"
151
+ replacer(f"http://0.0.0.0:8888{in_path}".encode())
152
+ == f"http://0.0.0.0:3000{out_path}".encode()
143
153
  )
@@ -302,6 +302,78 @@ class TestProcessSubmittedJobs:
302
302
  await session.refresh(pool)
303
303
  assert not pool.instances
304
304
 
305
+ @pytest.mark.asyncio
306
+ @pytest.mark.parametrize("test_db", ["sqlite", "postgres"], indirect=True)
307
+ async def test_provisions_job_with_optional_instance_volume_not_attached(
308
+ self,
309
+ test_db,
310
+ session: AsyncSession,
311
+ ):
312
+ project = await create_project(session=session)
313
+ user = await create_user(session=session)
314
+ pool = await create_pool(session=session, project=project)
315
+ repo = await create_repo(
316
+ session=session,
317
+ project_id=project.id,
318
+ )
319
+ run_spec = get_run_spec(run_name="test-run", repo_id=repo.name)
320
+ run_spec.configuration.volumes = [
321
+ InstanceMountPoint(instance_path="/root/.cache", path="/cache", optional=True)
322
+ ]
323
+ run = await create_run(
324
+ session=session,
325
+ project=project,
326
+ repo=repo,
327
+ user=user,
328
+ run_name="test-run",
329
+ run_spec=run_spec,
330
+ )
331
+ job = await create_job(
332
+ session=session,
333
+ run=run,
334
+ instance_assigned=True,
335
+ )
336
+ offer = InstanceOfferWithAvailability(
337
+ backend=BackendType.RUNPOD,
338
+ instance=InstanceType(
339
+ name="instance",
340
+ resources=Resources(cpus=1, memory_mib=512, spot=False, gpus=[]),
341
+ ),
342
+ region="us",
343
+ price=1.0,
344
+ availability=InstanceAvailability.AVAILABLE,
345
+ )
346
+ with patch("dstack._internal.server.services.backends.get_project_backends") as m:
347
+ backend_mock = Mock()
348
+ m.return_value = [backend_mock]
349
+ backend_mock.TYPE = BackendType.RUNPOD
350
+ backend_mock.compute.return_value.get_offers_cached.return_value = [offer]
351
+ backend_mock.compute.return_value.run_job.return_value = JobProvisioningData(
352
+ backend=offer.backend,
353
+ instance_type=offer.instance,
354
+ instance_id="instance_id",
355
+ hostname="1.1.1.1",
356
+ internal_ip=None,
357
+ region=offer.region,
358
+ price=offer.price,
359
+ username="ubuntu",
360
+ ssh_port=22,
361
+ ssh_proxy=None,
362
+ dockerized=False,
363
+ backend_data=None,
364
+ )
365
+ await process_submitted_jobs()
366
+
367
+ await session.refresh(job)
368
+ assert job is not None
369
+ assert job.status == JobStatus.PROVISIONING
370
+
371
+ await session.refresh(pool)
372
+ instance_offer = InstanceOfferWithAvailability.parse_raw(pool.instances[0].offer)
373
+ assert offer == instance_offer
374
+ pool_job_provisioning_data = pool.instances[0].job_provisioning_data
375
+ assert pool_job_provisioning_data == job.job_provisioning_data
376
+
305
377
  @pytest.mark.asyncio
306
378
  @pytest.mark.parametrize("test_db", ["sqlite", "postgres"], indirect=True)
307
379
  async def test_fails_job_when_no_capacity(self, test_db, session: AsyncSession):
@@ -412,7 +484,8 @@ class TestProcessSubmittedJobs:
412
484
  run_spec = get_run_spec(run_name="test-run", repo_id=repo.name)
413
485
  run_spec.configuration.volumes = [
414
486
  VolumeMountPoint(name=volume.name, path="/volume"),
415
- InstanceMountPoint(instance_path="/root/.cache", path="/cache"),
487
+ InstanceMountPoint(instance_path="/root/.data", path="/data"),
488
+ InstanceMountPoint(instance_path="/root/.cache", path="/cache", optional=True),
416
489
  ]
417
490
  run = await create_run(
418
491
  session=session,
@@ -110,6 +110,7 @@ def get_dev_env_run_plan_dict(
110
110
  "instance_types": None,
111
111
  "creation_policy": None,
112
112
  "instance_name": None,
113
+ "single_branch": None,
113
114
  "max_duration": "off",
114
115
  "stop_duration": None,
115
116
  "max_price": None,
@@ -180,6 +181,7 @@ def get_dev_env_run_plan_dict(
180
181
  "replica_num": 0,
181
182
  "job_num": 0,
182
183
  "jobs_per_replica": 1,
184
+ "single_branch": False,
183
185
  "max_duration": None,
184
186
  "stop_duration": 300,
185
187
  "registry_auth": None,
@@ -261,6 +263,7 @@ def get_dev_env_run_dict(
261
263
  "instance_types": None,
262
264
  "creation_policy": None,
263
265
  "instance_name": None,
266
+ "single_branch": None,
264
267
  "max_duration": "off",
265
268
  "stop_duration": None,
266
269
  "max_price": None,
@@ -331,6 +334,7 @@ def get_dev_env_run_dict(
331
334
  "replica_num": 0,
332
335
  "job_num": 0,
333
336
  "jobs_per_replica": 1,
337
+ "single_branch": False,
334
338
  "max_duration": None,
335
339
  "stop_duration": 300,
336
340
  "registry_auth": None,
@@ -4,6 +4,7 @@ from unittest.mock import patch
4
4
  import httpx
5
5
  import pytest
6
6
  from fastapi import FastAPI
7
+ from fastapi.responses import PlainTextResponse
7
8
 
8
9
  from dstack._internal.proxy.gateway.repo.repo import GatewayProxyRepo
9
10
  from dstack._internal.proxy.lib.auth import BaseProxyAuthProvider
@@ -25,6 +26,8 @@ ProxyTestRepo = GatewayProxyRepo
25
26
 
26
27
  @pytest.fixture
27
28
  def mock_replica_client_httpbin(httpbin) -> Generator[None, None, None]:
29
+ """Mocks deployed services. Replaces them with httpbin"""
30
+
28
31
  with patch(
29
32
  "dstack._internal.proxy.lib.services.service_connection.ServiceConnectionPool.get_or_add"
30
33
  ) as add_connection_mock:
@@ -34,6 +37,20 @@ def mock_replica_client_httpbin(httpbin) -> Generator[None, None, None]:
34
37
  yield
35
38
 
36
39
 
40
+ @pytest.fixture
41
+ def mock_replica_client_path_reporter() -> Generator[None, None, None]:
42
+ """Mocks deployed services. Replaces them with an app that returns the requested path"""
43
+
44
+ app = FastAPI()
45
+ app.get("{path:path}")(lambda path: PlainTextResponse(path))
46
+ client = ServiceClient(base_url="http://test/", transport=httpx.ASGITransport(app))
47
+ with patch(
48
+ "dstack._internal.proxy.lib.services.service_connection.ServiceConnectionPool.get_or_add"
49
+ ) as add_connection_mock:
50
+ add_connection_mock.return_value.client.return_value = client
51
+ yield
52
+
53
+
37
54
  def make_app(
38
55
  repo: BaseProxyRepo, auth: BaseProxyAuthProvider = ProxyTestAuthProvider()
39
56
  ) -> FastAPI:
@@ -200,3 +217,25 @@ async def test_auth(mock_replica_client_httpbin, token: Optional[str], status: i
200
217
  url = "http://test-host/proxy/services/test-proj/httpbin/"
201
218
  resp = await client.get(url, headers=headers)
202
219
  assert resp.status_code == status
220
+
221
+
222
+ @pytest.mark.asyncio
223
+ @pytest.mark.parametrize(
224
+ ("strip", "downstream_path", "upstream_path"),
225
+ [
226
+ (True, "/proxy/services/my-proj/my-run/", "/"),
227
+ (True, "/proxy/services/my-proj/my-run/a/b", "/a/b"),
228
+ (False, "/proxy/services/my-proj/my-run/", "/proxy/services/my-proj/my-run/"),
229
+ (False, "/proxy/services/my-proj/my-run/a/b", "/proxy/services/my-proj/my-run/a/b"),
230
+ ],
231
+ )
232
+ async def test_strip_prefix(
233
+ mock_replica_client_path_reporter, strip: bool, downstream_path: str, upstream_path: str
234
+ ) -> None:
235
+ repo = ProxyTestRepo()
236
+ await repo.set_project(make_project("my-proj"))
237
+ await repo.set_service(make_service("my-proj", "my-run", strip_prefix=strip))
238
+ _, client = make_app_client(repo)
239
+ resp = await client.get(f"http://test-host{downstream_path}")
240
+ assert resp.status_code == 200
241
+ assert resp.text == upstream_path
@@ -175,7 +175,9 @@ class TestShimClientV1(BaseShimClientTest):
175
175
  "device_name": "/dev/sdv",
176
176
  }
177
177
  ],
178
- "instance_mounts": [{"instance_path": "/mnt/nfs/home", "path": "/home"}],
178
+ "instance_mounts": [
179
+ {"instance_path": "/mnt/nfs/home", "path": "/home", "optional": False}
180
+ ],
179
181
  }
180
182
  self.assert_request(adapter, 0, "POST", "/api/submit", expected_request)
181
183
 
@@ -341,7 +343,9 @@ class TestShimClientV2(BaseShimClientTest):
341
343
  }
342
344
  ],
343
345
  "volume_mounts": [{"name": "vol", "path": "/vol"}],
344
- "instance_mounts": [{"instance_path": "/mnt/nfs/home", "path": "/home"}],
346
+ "instance_mounts": [
347
+ {"instance_path": "/mnt/nfs/home", "path": "/home", "optional": False}
348
+ ],
345
349
  "host_ssh_user": "dstack",
346
350
  "host_ssh_keys": ["host_key"],
347
351
  "container_ssh_keys": ["project_key", "user_key"],