dstack 0.19.12rc1__py3-none-any.whl → 0.19.14__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.

Potentially problematic release.


This version of dstack might be problematic. Click here for more details.

Files changed (62) hide show
  1. dstack/_internal/cli/commands/attach.py +4 -4
  2. dstack/_internal/cli/services/configurators/run.py +44 -47
  3. dstack/_internal/cli/utils/run.py +31 -31
  4. dstack/_internal/core/backends/aws/compute.py +22 -9
  5. dstack/_internal/core/backends/aws/resources.py +26 -0
  6. dstack/_internal/core/backends/base/offers.py +0 -1
  7. dstack/_internal/core/backends/template/configurator.py.jinja +1 -6
  8. dstack/_internal/core/backends/template/models.py.jinja +4 -0
  9. dstack/_internal/core/compatibility/__init__.py +0 -0
  10. dstack/_internal/core/compatibility/fleets.py +72 -0
  11. dstack/_internal/core/compatibility/gateways.py +34 -0
  12. dstack/_internal/core/compatibility/runs.py +131 -0
  13. dstack/_internal/core/compatibility/volumes.py +32 -0
  14. dstack/_internal/core/models/configurations.py +1 -1
  15. dstack/_internal/core/models/fleets.py +6 -1
  16. dstack/_internal/core/models/instances.py +51 -12
  17. dstack/_internal/core/models/profiles.py +43 -3
  18. dstack/_internal/core/models/projects.py +1 -0
  19. dstack/_internal/core/models/repos/local.py +3 -3
  20. dstack/_internal/core/models/runs.py +139 -43
  21. dstack/_internal/server/app.py +46 -1
  22. dstack/_internal/server/background/tasks/process_running_jobs.py +92 -15
  23. dstack/_internal/server/background/tasks/process_runs.py +163 -80
  24. dstack/_internal/server/migrations/versions/35e90e1b0d3e_add_rolling_deployment_fields.py +42 -0
  25. dstack/_internal/server/migrations/versions/35f732ee4cf5_add_projectmodel_is_public.py +39 -0
  26. dstack/_internal/server/models.py +4 -0
  27. dstack/_internal/server/routers/projects.py +4 -3
  28. dstack/_internal/server/routers/prometheus.py +4 -1
  29. dstack/_internal/server/schemas/projects.py +1 -0
  30. dstack/_internal/server/security/permissions.py +36 -0
  31. dstack/_internal/server/services/jobs/__init__.py +1 -0
  32. dstack/_internal/server/services/jobs/configurators/base.py +11 -7
  33. dstack/_internal/server/services/projects.py +54 -1
  34. dstack/_internal/server/services/runner/client.py +4 -1
  35. dstack/_internal/server/services/runs.py +49 -29
  36. dstack/_internal/server/services/services/__init__.py +19 -0
  37. dstack/_internal/server/services/services/autoscalers.py +37 -26
  38. dstack/_internal/server/services/storage/__init__.py +38 -0
  39. dstack/_internal/server/services/storage/base.py +27 -0
  40. dstack/_internal/server/services/storage/gcs.py +44 -0
  41. dstack/_internal/server/services/{storage.py → storage/s3.py} +4 -27
  42. dstack/_internal/server/settings.py +7 -3
  43. dstack/_internal/server/statics/index.html +1 -1
  44. dstack/_internal/server/statics/{main-5b9786c955b42bf93581.js → main-0ac1e1583684417ae4d1.js} +1695 -62
  45. dstack/_internal/server/statics/{main-5b9786c955b42bf93581.js.map → main-0ac1e1583684417ae4d1.js.map} +1 -1
  46. dstack/_internal/server/statics/{main-8f9c66f404e9c7e7e020.css → main-f39c418b05fe14772dd8.css} +1 -1
  47. dstack/_internal/server/testing/common.py +11 -1
  48. dstack/_internal/settings.py +3 -0
  49. dstack/_internal/utils/common.py +4 -0
  50. dstack/api/_public/runs.py +14 -5
  51. dstack/api/server/_fleets.py +9 -69
  52. dstack/api/server/_gateways.py +3 -14
  53. dstack/api/server/_projects.py +2 -2
  54. dstack/api/server/_runs.py +4 -116
  55. dstack/api/server/_volumes.py +3 -14
  56. dstack/plugins/builtin/rest_plugin/_plugin.py +24 -5
  57. dstack/version.py +2 -2
  58. {dstack-0.19.12rc1.dist-info → dstack-0.19.14.dist-info}/METADATA +1 -1
  59. {dstack-0.19.12rc1.dist-info → dstack-0.19.14.dist-info}/RECORD +62 -52
  60. {dstack-0.19.12rc1.dist-info → dstack-0.19.14.dist-info}/WHEEL +0 -0
  61. {dstack-0.19.12rc1.dist-info → dstack-0.19.14.dist-info}/entry_points.txt +0 -0
  62. {dstack-0.19.12rc1.dist-info → dstack-0.19.14.dist-info}/licenses/LICENSE.md +0 -0
@@ -140,6 +140,7 @@ async def create_project(
140
140
  created_at: datetime = datetime(2023, 1, 2, 3, 4, tzinfo=timezone.utc),
141
141
  ssh_private_key: str = "",
142
142
  ssh_public_key: str = "",
143
+ is_public: bool = False,
143
144
  ) -> ProjectModel:
144
145
  if owner is None:
145
146
  owner = await create_user(session=session, name="test_owner")
@@ -149,6 +150,7 @@ async def create_project(
149
150
  created_at=created_at,
150
151
  ssh_private_key=ssh_private_key,
151
152
  ssh_public_key=ssh_public_key,
153
+ is_public=is_public,
152
154
  )
153
155
  session.add(project)
154
156
  await session.commit()
@@ -263,6 +265,7 @@ async def create_run(
263
265
  run_id: Optional[UUID] = None,
264
266
  deleted: bool = False,
265
267
  priority: int = 0,
268
+ deployment_num: int = 0,
266
269
  ) -> RunModel:
267
270
  if run_spec is None:
268
271
  run_spec = get_run_spec(
@@ -284,6 +287,8 @@ async def create_run(
284
287
  last_processed_at=submitted_at,
285
288
  jobs=[],
286
289
  priority=priority,
290
+ deployment_num=deployment_num,
291
+ desired_replica_count=1,
287
292
  )
288
293
  session.add(run)
289
294
  await session.commit()
@@ -303,18 +308,23 @@ async def create_job(
303
308
  instance: Optional[InstanceModel] = None,
304
309
  job_num: int = 0,
305
310
  replica_num: int = 0,
311
+ deployment_num: Optional[int] = None,
306
312
  instance_assigned: bool = False,
307
313
  disconnected_at: Optional[datetime] = None,
308
314
  ) -> JobModel:
315
+ if deployment_num is None:
316
+ deployment_num = run.deployment_num
309
317
  run_spec = RunSpec.parse_raw(run.run_spec)
310
318
  job_spec = (await get_job_specs_from_run_spec(run_spec, replica_num=replica_num))[0]
319
+ job_spec.job_num = job_num
311
320
  job = JobModel(
312
321
  project_id=run.project_id,
313
322
  run_id=run.id,
314
323
  run_name=run.run_name,
315
324
  job_num=job_num,
316
- job_name=run.run_name + f"-0-{replica_num}",
325
+ job_name=run.run_name + f"-{job_num}-{replica_num}",
317
326
  replica_num=replica_num,
327
+ deployment_num=deployment_num,
318
328
  submission_num=submission_num,
319
329
  submitted_at=submitted_at,
320
330
  last_processed_at=last_processed_at,
@@ -14,6 +14,9 @@ DSTACK_USE_LATEST_FROM_BRANCH = os.getenv("DSTACK_USE_LATEST_FROM_BRANCH") is no
14
14
 
15
15
  DSTACK_BASE_IMAGE = os.getenv("DSTACK_BASE_IMAGE", "dstackai/base")
16
16
  DSTACK_BASE_IMAGE_VERSION = os.getenv("DSTACK_BASE_IMAGE_VERSION", version.base_image)
17
+ DSTACK_BASE_IMAGE_UBUNTU_VERSION = os.getenv(
18
+ "DSTACK_BASE_IMAGE_UBUNTU_VERSION", version.base_image_ubuntu_version
19
+ )
17
20
 
18
21
 
19
22
  class FeatureFlags:
@@ -314,3 +314,7 @@ def make_proxy_url(server_url: str, proxy_url: str) -> str:
314
314
  path=concat_url_path(server.path, proxy.path),
315
315
  )
316
316
  return proxy.geturl()
317
+
318
+
319
+ def list_enum_values_for_annotation(enum_class: type[enum.Enum]) -> str:
320
+ return ", ".join(f"`{e.value}`" for e in enum_class)
@@ -31,6 +31,7 @@ from dstack._internal.core.models.resources import ResourcesSpec
31
31
  from dstack._internal.core.models.runs import (
32
32
  Job,
33
33
  JobSpec,
34
+ JobStatus,
34
35
  RunPlan,
35
36
  RunSpec,
36
37
  RunStatus,
@@ -184,7 +185,7 @@ class Run(ABC):
184
185
  self,
185
186
  start_time: Optional[datetime] = None,
186
187
  diagnose: bool = False,
187
- replica_num: int = 0,
188
+ replica_num: Optional[int] = None,
188
189
  job_num: int = 0,
189
190
  ) -> Iterable[bytes]:
190
191
  """
@@ -246,7 +247,7 @@ class Run(ABC):
246
247
  ssh_identity_file: Optional[PathLike] = None,
247
248
  bind_address: Optional[str] = None,
248
249
  ports_overrides: Optional[List[PortMapping]] = None,
249
- replica_num: int = 0,
250
+ replica_num: Optional[int] = None,
250
251
  job_num: int = 0,
251
252
  ) -> bool:
252
253
  """
@@ -254,6 +255,7 @@ class Run(ABC):
254
255
 
255
256
  Args:
256
257
  ssh_identity_file: SSH keypair to access instances.
258
+ replica_num: replica_num or None to attach to any running replica.
257
259
 
258
260
  Raises:
259
261
  dstack.api.PortUsedError: If ports are in use or the run is attached by another process.
@@ -265,7 +267,9 @@ class Run(ABC):
265
267
 
266
268
  job = self._find_job(replica_num=replica_num, job_num=job_num)
267
269
  if job is None:
268
- raise ClientError(f"Failed to find replica={replica_num} job={job_num}")
270
+ replica_repr = replica_num if replica_num is not None else "<any running>"
271
+ raise ClientError(f"Failed to find replica={replica_repr} job={job_num}")
272
+ replica_num = job.job_spec.replica_num
269
273
 
270
274
  name = self.name
271
275
  if replica_num != 0 or job_num != 0:
@@ -358,9 +362,14 @@ class Run(ABC):
358
362
  self._ssh_attach.detach()
359
363
  self._ssh_attach = None
360
364
 
361
- def _find_job(self, replica_num: int, job_num: int) -> Optional[Job]:
365
+ def _find_job(self, replica_num: Optional[int], job_num: int) -> Optional[Job]:
362
366
  for j in self._run.jobs:
363
- if j.job_spec.replica_num == replica_num and j.job_spec.job_num == job_num:
367
+ if (
368
+ replica_num is not None
369
+ and j.job_spec.replica_num == replica_num
370
+ or replica_num is None
371
+ and j.job_submissions[-1].status == JobStatus.RUNNING
372
+ ) and j.job_spec.job_num == job_num:
364
373
  return j
365
374
  return None
366
375
 
@@ -1,9 +1,13 @@
1
- from typing import Any, Dict, List, Optional, Union
1
+ from typing import List, Union
2
2
 
3
3
  from pydantic import parse_obj_as
4
4
 
5
+ from dstack._internal.core.compatibility.fleets import (
6
+ get_apply_plan_excludes,
7
+ get_create_fleet_excludes,
8
+ get_get_plan_excludes,
9
+ )
5
10
  from dstack._internal.core.models.fleets import ApplyFleetPlanInput, Fleet, FleetPlan, FleetSpec
6
- from dstack._internal.core.models.instances import Instance
7
11
  from dstack._internal.server.schemas.fleets import (
8
12
  ApplyFleetPlanRequest,
9
13
  CreateFleetRequest,
@@ -34,7 +38,7 @@ class FleetsAPIClient(APIClientGroup):
34
38
  spec: FleetSpec,
35
39
  ) -> FleetPlan:
36
40
  body = GetFleetPlanRequest(spec=spec)
37
- body_json = body.json(exclude=_get_get_plan_excludes(spec))
41
+ body_json = body.json(exclude=get_get_plan_excludes(spec))
38
42
  resp = self._request(f"/api/project/{project_name}/fleets/get_plan", body=body_json)
39
43
  return parse_obj_as(FleetPlan.__response__, resp.json())
40
44
 
@@ -46,7 +50,7 @@ class FleetsAPIClient(APIClientGroup):
46
50
  ) -> Fleet:
47
51
  plan_input = ApplyFleetPlanInput.__response__.parse_obj(plan)
48
52
  body = ApplyFleetPlanRequest(plan=plan_input, force=force)
49
- body_json = body.json(exclude=_get_apply_plan_excludes(plan_input))
53
+ body_json = body.json(exclude=get_apply_plan_excludes(plan_input))
50
54
  resp = self._request(f"/api/project/{project_name}/fleets/apply", body=body_json)
51
55
  return parse_obj_as(Fleet.__response__, resp.json())
52
56
 
@@ -66,70 +70,6 @@ class FleetsAPIClient(APIClientGroup):
66
70
  spec: FleetSpec,
67
71
  ) -> Fleet:
68
72
  body = CreateFleetRequest(spec=spec)
69
- body_json = body.json(exclude=_get_create_fleet_excludes(spec))
73
+ body_json = body.json(exclude=get_create_fleet_excludes(spec))
70
74
  resp = self._request(f"/api/project/{project_name}/fleets/create", body=body_json)
71
75
  return parse_obj_as(Fleet.__response__, resp.json())
72
-
73
-
74
- def _get_get_plan_excludes(fleet_spec: FleetSpec) -> Dict:
75
- get_plan_excludes = {}
76
- spec_excludes = _get_fleet_spec_excludes(fleet_spec)
77
- if spec_excludes:
78
- get_plan_excludes["spec"] = spec_excludes
79
- return get_plan_excludes
80
-
81
-
82
- def _get_apply_plan_excludes(plan_input: ApplyFleetPlanInput) -> Dict:
83
- apply_plan_excludes = {}
84
- spec_excludes = _get_fleet_spec_excludes(plan_input.spec)
85
- if spec_excludes:
86
- apply_plan_excludes["spec"] = apply_plan_excludes
87
- current_resource = plan_input.current_resource
88
- if current_resource is not None:
89
- current_resource_excludes = {}
90
- apply_plan_excludes["current_resource"] = current_resource_excludes
91
- if all(map(_should_exclude_instance_cpu_arch, current_resource.instances)):
92
- current_resource_excludes["instances"] = {
93
- "__all__": {"instance_type": {"resources": {"cpu_arch"}}}
94
- }
95
- return {"plan": apply_plan_excludes}
96
-
97
-
98
- def _should_exclude_instance_cpu_arch(instance: Instance) -> bool:
99
- try:
100
- return instance.instance_type.resources.cpu_arch is None
101
- except AttributeError:
102
- return True
103
-
104
-
105
- def _get_create_fleet_excludes(fleet_spec: FleetSpec) -> Dict:
106
- create_fleet_excludes = {}
107
- spec_excludes = _get_fleet_spec_excludes(fleet_spec)
108
- if spec_excludes:
109
- create_fleet_excludes["spec"] = spec_excludes
110
- return create_fleet_excludes
111
-
112
-
113
- def _get_fleet_spec_excludes(fleet_spec: FleetSpec) -> Optional[Dict]:
114
- """
115
- Returns `fleet_spec` exclude mapping to exclude certain fields from the request.
116
- Use this method to exclude new fields when they are not set to keep
117
- clients backward-compatibility with older servers.
118
- """
119
- spec_excludes: Dict[str, Any] = {}
120
- configuration_excludes: Dict[str, Any] = {}
121
- profile_excludes: set[str] = set()
122
- profile = fleet_spec.profile
123
- if profile.fleets is None:
124
- profile_excludes.add("fleets")
125
- if fleet_spec.configuration.tags is None:
126
- configuration_excludes["tags"] = True
127
- if profile.tags is None:
128
- profile_excludes.add("tags")
129
- if configuration_excludes:
130
- spec_excludes["configuration"] = configuration_excludes
131
- if profile_excludes:
132
- spec_excludes["profile"] = profile_excludes
133
- if spec_excludes:
134
- return spec_excludes
135
- return None
@@ -1,7 +1,8 @@
1
- from typing import Dict, List
1
+ from typing import List
2
2
 
3
3
  from pydantic import parse_obj_as
4
4
 
5
+ from dstack._internal.core.compatibility.gateways import get_create_gateway_excludes
5
6
  from dstack._internal.core.models.gateways import Gateway, GatewayConfiguration
6
7
  from dstack._internal.server.schemas.gateways import (
7
8
  CreateGatewayRequest,
@@ -31,7 +32,7 @@ class GatewaysAPIClient(APIClientGroup):
31
32
  body = CreateGatewayRequest(configuration=configuration)
32
33
  resp = self._request(
33
34
  f"/api/project/{project_name}/gateways/create",
34
- body=body.json(exclude=_get_gateway_configuration_excludes(configuration)),
35
+ body=body.json(exclude=get_create_gateway_excludes(configuration)),
35
36
  )
36
37
  return parse_obj_as(Gateway.__response__, resp.json())
37
38
 
@@ -51,15 +52,3 @@ class GatewaysAPIClient(APIClientGroup):
51
52
  f"/api/project/{project_name}/gateways/set_wildcard_domain", body=body.json()
52
53
  )
53
54
  return parse_obj_as(Gateway.__response__, resp.json())
54
-
55
-
56
- def _get_gateway_configuration_excludes(configuration: GatewayConfiguration) -> Dict:
57
- """
58
- Returns `configuration` exclude mapping to exclude certain fields from the request.
59
- Use this method to exclude new fields when they are not set to keep
60
- clients backward-compatibility with older servers.
61
- """
62
- configuration_excludes = {}
63
- if configuration.tags is None:
64
- configuration_excludes["tags"] = True
65
- return {"configuration": configuration_excludes}
@@ -17,8 +17,8 @@ class ProjectsAPIClient(APIClientGroup):
17
17
  resp = self._request("/api/projects/list")
18
18
  return parse_obj_as(List[Project.__response__], resp.json())
19
19
 
20
- def create(self, project_name: str) -> Project:
21
- body = CreateProjectRequest(project_name=project_name)
20
+ def create(self, project_name: str, is_public: bool = False) -> Project:
21
+ body = CreateProjectRequest(project_name=project_name, is_public=is_public)
22
22
  resp = self._request("/api/projects/create", body=body.json())
23
23
  return parse_obj_as(Project.__response__, resp.json())
24
24
 
@@ -1,13 +1,12 @@
1
1
  from datetime import datetime
2
- from typing import Any, Dict, List, Optional, Union
2
+ from typing import 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.configurations import ServiceConfiguration
7
+ from dstack._internal.core.compatibility.runs import get_apply_plan_excludes, get_get_plan_excludes
8
8
  from dstack._internal.core.models.runs import (
9
9
  ApplyRunPlanInput,
10
- JobSubmission,
11
10
  Run,
12
11
  RunPlan,
13
12
  RunSpec,
@@ -60,7 +59,7 @@ class RunsAPIClient(APIClientGroup):
60
59
  body = GetRunPlanRequest(run_spec=run_spec, max_offers=max_offers)
61
60
  resp = self._request(
62
61
  f"/api/project/{project_name}/runs/get_plan",
63
- body=body.json(exclude=_get_get_plan_excludes(body)),
62
+ body=body.json(exclude=get_get_plan_excludes(body)),
64
63
  )
65
64
  return parse_obj_as(RunPlan.__response__, resp.json())
66
65
 
@@ -74,7 +73,7 @@ class RunsAPIClient(APIClientGroup):
74
73
  body = ApplyRunPlanRequest(plan=plan_input, force=force)
75
74
  resp = self._request(
76
75
  f"/api/project/{project_name}/runs/apply",
77
- body=body.json(exclude=_get_apply_plan_excludes(plan_input)),
76
+ body=body.json(exclude=get_apply_plan_excludes(plan_input)),
78
77
  )
79
78
  return parse_obj_as(Run.__response__, resp.json())
80
79
 
@@ -85,114 +84,3 @@ class RunsAPIClient(APIClientGroup):
85
84
  def delete(self, project_name: str, runs_names: List[str]):
86
85
  body = DeleteRunsRequest(runs_names=runs_names)
87
86
  self._request(f"/api/project/{project_name}/runs/delete", body=body.json())
88
-
89
-
90
- def _get_apply_plan_excludes(plan: ApplyRunPlanInput) -> Optional[Dict]:
91
- """
92
- Returns `plan` exclude mapping to exclude certain fields from the request.
93
- Use this method to exclude new fields when they are not set to keep
94
- clients backward-compatibility with older servers.
95
- """
96
- apply_plan_excludes = {}
97
- run_spec_excludes = _get_run_spec_excludes(plan.run_spec)
98
- if run_spec_excludes is not None:
99
- apply_plan_excludes["run_spec"] = run_spec_excludes
100
- current_resource = plan.current_resource
101
- if current_resource is not None:
102
- current_resource_excludes = {}
103
- apply_plan_excludes["current_resource"] = current_resource_excludes
104
- current_resource_excludes["run_spec"] = _get_run_spec_excludes(current_resource.run_spec)
105
- job_submissions_excludes = {}
106
- current_resource_excludes["jobs"] = {
107
- "__all__": {"job_submissions": {"__all__": job_submissions_excludes}}
108
- }
109
- job_submissions = [js for j in current_resource.jobs for js in j.job_submissions]
110
- if all(map(_should_exclude_job_submission_jpd_cpu_arch, job_submissions)):
111
- job_submissions_excludes["job_provisioning_data"] = {
112
- "instance_type": {"resources": {"cpu_arch"}}
113
- }
114
- if all(map(_should_exclude_job_submission_jrd_cpu_arch, job_submissions)):
115
- job_submissions_excludes["job_runtime_data"] = {
116
- "offer": {"instance": {"resources": {"cpu_arch"}}}
117
- }
118
- if all(js.exit_status is None for js in job_submissions):
119
- job_submissions_excludes["exit_status"] = True
120
- latest_job_submission = current_resource.latest_job_submission
121
- if latest_job_submission is not None:
122
- latest_job_submission_excludes = {}
123
- current_resource_excludes["latest_job_submission"] = latest_job_submission_excludes
124
- if _should_exclude_job_submission_jpd_cpu_arch(latest_job_submission):
125
- latest_job_submission_excludes["job_provisioning_data"] = {
126
- "instance_type": {"resources": {"cpu_arch"}}
127
- }
128
- if _should_exclude_job_submission_jrd_cpu_arch(latest_job_submission):
129
- latest_job_submission_excludes["job_runtime_data"] = {
130
- "offer": {"instance": {"resources": {"cpu_arch"}}}
131
- }
132
- if latest_job_submission.exit_status is None:
133
- latest_job_submission_excludes["exit_status"] = True
134
- return {"plan": apply_plan_excludes}
135
-
136
-
137
- def _should_exclude_job_submission_jpd_cpu_arch(job_submission: JobSubmission) -> bool:
138
- try:
139
- return job_submission.job_provisioning_data.instance_type.resources.cpu_arch is None
140
- except AttributeError:
141
- return True
142
-
143
-
144
- def _should_exclude_job_submission_jrd_cpu_arch(job_submission: JobSubmission) -> bool:
145
- try:
146
- return job_submission.job_runtime_data.offer.instance.resources.cpu_arch is None
147
- except AttributeError:
148
- return True
149
-
150
-
151
- def _get_get_plan_excludes(request: GetRunPlanRequest) -> Optional[Dict]:
152
- """
153
- Excludes new fields when they are not set to keep
154
- clients backward-compatibility with older servers.
155
- """
156
- get_plan_excludes = {}
157
- run_spec_excludes = _get_run_spec_excludes(request.run_spec)
158
- if run_spec_excludes is not None:
159
- get_plan_excludes["run_spec"] = run_spec_excludes
160
- if request.max_offers is None:
161
- get_plan_excludes["max_offers"] = True
162
- return get_plan_excludes
163
-
164
-
165
- def _get_run_spec_excludes(run_spec: RunSpec) -> Optional[Dict]:
166
- """
167
- Returns `run_spec` exclude mapping to exclude certain fields from the request.
168
- Use this method to exclude new fields when they are not set to keep
169
- clients backward-compatibility with older servers.
170
- """
171
- spec_excludes: dict[str, Any] = {}
172
- configuration_excludes: dict[str, Any] = {}
173
- profile_excludes: set[str] = set()
174
- configuration = run_spec.configuration
175
- profile = run_spec.profile
176
-
177
- if configuration.fleets is None:
178
- configuration_excludes["fleets"] = True
179
- if profile is not None and profile.fleets is None:
180
- profile_excludes.add("fleets")
181
- if configuration.tags is None:
182
- configuration_excludes["tags"] = True
183
- if profile is not None and profile.tags is None:
184
- profile_excludes.add("tags")
185
- if isinstance(configuration, ServiceConfiguration) and not configuration.rate_limits:
186
- configuration_excludes["rate_limits"] = True
187
- if configuration.shell is None:
188
- configuration_excludes["shell"] = True
189
- if configuration.priority is None:
190
- configuration_excludes["priority"] = True
191
-
192
- if configuration_excludes:
193
- spec_excludes["configuration"] = configuration_excludes
194
- if profile_excludes:
195
- spec_excludes["profile"] = profile_excludes
196
- if spec_excludes:
197
- return spec_excludes
198
- return None
@@ -1,7 +1,8 @@
1
- from typing import Dict, List
1
+ from typing import List
2
2
 
3
3
  from pydantic import parse_obj_as
4
4
 
5
+ from dstack._internal.core.compatibility.volumes import get_create_volume_excludes
5
6
  from dstack._internal.core.models.volumes import Volume, VolumeConfiguration
6
7
  from dstack._internal.server.schemas.volumes import (
7
8
  CreateVolumeRequest,
@@ -29,22 +30,10 @@ class VolumesAPIClient(APIClientGroup):
29
30
  body = CreateVolumeRequest(configuration=configuration)
30
31
  resp = self._request(
31
32
  f"/api/project/{project_name}/volumes/create",
32
- body=body.json(exclude=_get_volume_configuration_excludes(configuration)),
33
+ body=body.json(exclude=get_create_volume_excludes(configuration)),
33
34
  )
34
35
  return parse_obj_as(Volume.__response__, resp.json())
35
36
 
36
37
  def delete(self, project_name: str, names: List[str]) -> None:
37
38
  body = DeleteVolumesRequest(names=names)
38
39
  self._request(f"/api/project/{project_name}/volumes/delete", body=body.json())
39
-
40
-
41
- def _get_volume_configuration_excludes(configuration: VolumeConfiguration) -> Dict:
42
- """
43
- Returns `configuration` exclude mapping to exclude certain fields from the request.
44
- Use this method to exclude new fields when they are not set to keep
45
- clients backward-compatibility with older servers.
46
- """
47
- configuration_excludes = {}
48
- if configuration.tags is None:
49
- configuration_excludes["tags"] = True
50
- return {"configuration": configuration_excludes}
@@ -1,10 +1,14 @@
1
1
  import json
2
2
  import os
3
- from typing import Type
3
+ from typing import Dict, Optional, Type
4
4
 
5
5
  import requests
6
6
  from pydantic import ValidationError
7
7
 
8
+ from dstack._internal.core.compatibility.fleets import get_fleet_spec_excludes
9
+ from dstack._internal.core.compatibility.gateways import get_gateway_spec_excludes
10
+ from dstack._internal.core.compatibility.runs import get_run_spec_excludes
11
+ from dstack._internal.core.compatibility.volumes import get_volume_spec_excludes
8
12
  from dstack._internal.core.errors import ServerClientError
9
13
  from dstack._internal.core.models.fleets import FleetSpec
10
14
  from dstack._internal.core.models.gateways import GatewaySpec
@@ -44,12 +48,17 @@ class CustomApplyPolicy(ApplyPolicy):
44
48
  logger.error(f"Plugin service rejected apply request: {response.error}")
45
49
  raise ServerClientError(f"Apply request rejected: {response.error}")
46
50
 
47
- def _call_plugin_service(self, spec_request: SpecApplyRequest, endpoint: str) -> ApplySpec:
51
+ def _call_plugin_service(
52
+ self,
53
+ spec_request: SpecApplyRequest,
54
+ endpoint: str,
55
+ excludes: Optional[Dict],
56
+ ) -> ApplySpec:
48
57
  response = None
49
58
  try:
50
59
  response = requests.post(
51
60
  f"{self._plugin_service_uri}{endpoint}",
52
- json=spec_request.dict(),
61
+ json=spec_request.dict(exclude={"spec": excludes}),
53
62
  headers={"accept": "application/json", "Content-Type": "application/json"},
54
63
  timeout=PLUGIN_REQUEST_TIMEOUT_SEC,
55
64
  )
@@ -75,10 +84,11 @@ class CustomApplyPolicy(ApplyPolicy):
75
84
  user: str,
76
85
  project: str,
77
86
  spec: ApplySpec,
87
+ excludes: Optional[Dict] = None,
78
88
  ) -> ApplySpec:
79
89
  try:
80
90
  spec_request = request_cls(user=user, project=project, spec=spec)
81
- spec_json = self._call_plugin_service(spec_request, endpoint)
91
+ spec_json = self._call_plugin_service(spec_request, endpoint, excludes)
82
92
  response = response_cls(**spec_json)
83
93
  self._check_request_rejected(response)
84
94
  return response.spec
@@ -88,7 +98,13 @@ class CustomApplyPolicy(ApplyPolicy):
88
98
 
89
99
  def on_run_apply(self, user: str, project: str, spec: RunSpec) -> RunSpec:
90
100
  return self._on_apply(
91
- RunSpecRequest, RunSpecResponse, "/apply_policies/on_run_apply", user, project, spec
101
+ RunSpecRequest,
102
+ RunSpecResponse,
103
+ "/apply_policies/on_run_apply",
104
+ user,
105
+ project,
106
+ spec,
107
+ excludes=get_run_spec_excludes(spec),
92
108
  )
93
109
 
94
110
  def on_fleet_apply(self, user: str, project: str, spec: FleetSpec) -> FleetSpec:
@@ -99,6 +115,7 @@ class CustomApplyPolicy(ApplyPolicy):
99
115
  user,
100
116
  project,
101
117
  spec,
118
+ excludes=get_fleet_spec_excludes(spec),
102
119
  )
103
120
 
104
121
  def on_volume_apply(self, user: str, project: str, spec: VolumeSpec) -> VolumeSpec:
@@ -109,6 +126,7 @@ class CustomApplyPolicy(ApplyPolicy):
109
126
  user,
110
127
  project,
111
128
  spec,
129
+ excludes=get_volume_spec_excludes(spec),
112
130
  )
113
131
 
114
132
  def on_gateway_apply(self, user: str, project: str, spec: GatewaySpec) -> GatewaySpec:
@@ -119,6 +137,7 @@ class CustomApplyPolicy(ApplyPolicy):
119
137
  user,
120
138
  project,
121
139
  spec,
140
+ excludes=get_gateway_spec_excludes(spec),
122
141
  )
123
142
 
124
143
 
dstack/version.py CHANGED
@@ -1,3 +1,3 @@
1
- __version__ = "0.19.12rc1"
1
+ __version__ = "0.19.14"
2
2
  __is_release__ = True
3
- base_image = "0.8"
3
+ base_image = "0.10" base_image_ubuntu_version = "22.04"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dstack
3
- Version: 0.19.12rc1
3
+ Version: 0.19.14
4
4
  Summary: dstack is an open-source orchestration engine for running AI workloads on any cloud or on-premises.
5
5
  Project-URL: Homepage, https://dstack.ai
6
6
  Project-URL: Source, https://github.com/dstackai/dstack