Skip to content

Commit b6c0de7

Browse files
Andrey Cheptsovfededagos
andcommitted
Support targeting specific run instances
Co-authored-by: Federico D'Agostino <fede.dagos@gmail.com>
1 parent cd0e93c commit b6c0de7

13 files changed

Lines changed: 1924 additions & 17 deletions

File tree

mkdocs/docs/reference/dstack.yml/dev-environment.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,40 @@ The `dev-environment` configuration type allows running [dev environments](../..
3434
type:
3535
required: true
3636

37+
### `instances[n]` { #_instances data-toc-label="instances" }
38+
39+
When `instances` is set, the run is placed only on matching existing fleet instances.
40+
41+
=== "By name"
42+
43+
#SCHEMA# dstack._internal.core.models.profiles.InstanceNameSelector
44+
overrides:
45+
show_root_heading: false
46+
type:
47+
required: true
48+
49+
=== "By hostname"
50+
51+
#SCHEMA# dstack._internal.core.models.profiles.InstanceHostnameSelector
52+
overrides:
53+
show_root_heading: false
54+
type:
55+
required: true
56+
57+
=== "By fleet and instance number"
58+
59+
#SCHEMA# dstack._internal.core.models.profiles.FleetInstanceSelector
60+
overrides:
61+
show_root_heading: false
62+
type:
63+
required: true
64+
65+
??? info "Short syntax"
66+
67+
The short syntax for instances is an instance name string.
68+
69+
* `my-fleet-1`, same as `{name: my-fleet-1}`
70+
3771
### `resources`
3872

3973
#SCHEMA# dstack._internal.core.models.resources.ResourcesSpec

mkdocs/docs/reference/dstack.yml/service.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,40 @@ The `service` configuration type allows running [services](../../concepts/servic
114114
type:
115115
required: true
116116

117+
### `instances[n]` { #_instances data-toc-label="instances" }
118+
119+
When `instances` is set, the run is placed only on matching existing fleet instances.
120+
121+
=== "By name"
122+
123+
#SCHEMA# dstack._internal.core.models.profiles.InstanceNameSelector
124+
overrides:
125+
show_root_heading: false
126+
type:
127+
required: true
128+
129+
=== "By hostname"
130+
131+
#SCHEMA# dstack._internal.core.models.profiles.InstanceHostnameSelector
132+
overrides:
133+
show_root_heading: false
134+
type:
135+
required: true
136+
137+
=== "By fleet and instance number"
138+
139+
#SCHEMA# dstack._internal.core.models.profiles.FleetInstanceSelector
140+
overrides:
141+
show_root_heading: false
142+
type:
143+
required: true
144+
145+
??? info "Short syntax"
146+
147+
The short syntax for instances is an instance name string.
148+
149+
* `my-fleet-1`, same as `{name: my-fleet-1}`
150+
117151
### `resources`
118152

119153
#SCHEMA# dstack._internal.core.models.resources.ResourcesSpec

mkdocs/docs/reference/dstack.yml/task.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,40 @@ The `task` configuration type allows running [tasks](../../concepts/tasks.md).
3434
type:
3535
required: true
3636

37+
### `instances[n]` { #_instances data-toc-label="instances" }
38+
39+
When `instances` is set, the run is placed only on matching existing fleet instances.
40+
41+
=== "By name"
42+
43+
#SCHEMA# dstack._internal.core.models.profiles.InstanceNameSelector
44+
overrides:
45+
show_root_heading: false
46+
type:
47+
required: true
48+
49+
=== "By hostname"
50+
51+
#SCHEMA# dstack._internal.core.models.profiles.InstanceHostnameSelector
52+
overrides:
53+
show_root_heading: false
54+
type:
55+
required: true
56+
57+
=== "By fleet and instance number"
58+
59+
#SCHEMA# dstack._internal.core.models.profiles.FleetInstanceSelector
60+
overrides:
61+
show_root_heading: false
62+
type:
63+
required: true
64+
65+
??? info "Short syntax"
66+
67+
The short syntax for instances is an instance name string.
68+
69+
* `my-fleet-1`, same as `{name: my-fleet-1}`
70+
3771
### `resources`
3872

3973
#SCHEMA# dstack._internal.core.models.resources.ResourcesSpec

src/dstack/_internal/core/compatibility/common.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ def get_profile_excludes(profile: Optional[ProfileParams]) -> IncludeExcludeSetT
1010
return excludes
1111
if profile.backend_options is None:
1212
excludes.add("backend_options")
13+
if profile.instances is None:
14+
excludes.add("instances")
1315
return excludes
1416

1517

src/dstack/_internal/core/compatibility/runs.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ def get_run_spec_excludes(run_spec: RunSpec) -> IncludeExcludeDictType:
8383
spec_excludes: IncludeExcludeDictType = {}
8484
configuration_excludes: IncludeExcludeDictType = {}
8585
profile_excludes = get_profile_excludes(run_spec.profile)
86+
for field in get_profile_excludes(run_spec.configuration):
87+
configuration_excludes[field] = True
8688

8789
if run_spec.configuration.backend_options is None:
8890
configuration_excludes["backend_options"] = True

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

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,58 @@ def crons(self) -> List[str]:
234234
return self.cron
235235

236236

237+
class InstanceNameSelector(CoreModel):
238+
name: Annotated[str, Field(description="The fleet instance name", min_length=1)]
239+
240+
241+
class InstanceHostnameSelector(CoreModel):
242+
hostname: Annotated[
243+
str, Field(description="The fleet instance hostname or IP address", min_length=1)
244+
]
245+
246+
247+
def _parse_fleet_instance_selector_fleet(v: Any) -> Any:
248+
if isinstance(v, str):
249+
return EntityReference.parse(v)
250+
return v
251+
252+
253+
class FleetInstanceSelectorConfig(CoreConfig):
254+
@staticmethod
255+
def schema_extra(schema: Dict[str, Any]):
256+
add_extra_schema_types(
257+
schema["properties"]["fleet"],
258+
extra_types=[{"type": "string", "minLength": 1}],
259+
)
260+
261+
262+
class FleetInstanceSelector(generate_dual_core_model(FleetInstanceSelectorConfig)):
263+
fleet: Annotated[
264+
EntityReference,
265+
Field(
266+
description=(
267+
"The fleet reference. For fleets owned by the current project, specify"
268+
" the fleet name. For a fleet from another project, specify"
269+
" `<project name>/<fleet name>` or an object with `project` and `name`."
270+
),
271+
),
272+
]
273+
instance: Annotated[int, Field(description="The fleet instance number", ge=0)]
274+
275+
_validate_fleet = validator("fleet", pre=True, allow_reuse=True)(
276+
_parse_fleet_instance_selector_fleet
277+
)
278+
279+
280+
InstanceSelector = Union[InstanceNameSelector, InstanceHostnameSelector, FleetInstanceSelector]
281+
282+
283+
def parse_instance_selector(v: Union[InstanceSelector, str]) -> InstanceSelector:
284+
if isinstance(v, str):
285+
return InstanceNameSelector(name=v)
286+
return v
287+
288+
237289
class ProfileParamsConfig(CoreConfig):
238290
@staticmethod
239291
def schema_extra(schema: Dict[str, Any]):
@@ -249,6 +301,10 @@ def schema_extra(schema: Dict[str, Any]):
249301
schema["properties"]["idle_duration"],
250302
extra_types=[{"type": "string"}],
251303
)
304+
add_extra_schema_types(
305+
schema["properties"]["instances"]["items"],
306+
extra_types=[{"type": "string", "minLength": 1}],
307+
)
252308

253309

254310
class ProfileParams(CoreModel):
@@ -391,6 +447,18 @@ class ProfileParams(CoreModel):
391447
),
392448
),
393449
] = None
450+
instances: Annotated[
451+
Optional[List[InstanceSelector]],
452+
Field(
453+
description=(
454+
"The specific fleet instances to consider for reuse."
455+
" Each value can be an instance name string, or an object with"
456+
" `name`, `hostname`, or `fleet` and `instance`."
457+
" When set, the run is only placed on matching existing instances."
458+
),
459+
min_items=1,
460+
),
461+
] = None
394462
tags: Annotated[
395463
Optional[Dict[str, str]],
396464
Field(
@@ -416,11 +484,23 @@ class ProfileParams(CoreModel):
416484
parse_idle_duration
417485
)
418486
_validate_fleets = validator("fleets", allow_reuse=True, each_item=True)(EntityReference.parse)
487+
_validate_instances = validator("instances", pre=True, allow_reuse=True, each_item=True)(
488+
parse_instance_selector
489+
)
419490
_validate_tags = validator("tags", pre=True, allow_reuse=True)(tags_validator)
420491
_validate_backend_options = validator("backend_options", allow_reuse=True)(
421492
validate_backend_options
422493
)
423494

495+
def dict(self, *args, **kwargs) -> Dict:
496+
# super() does not work with pydantic-duality
497+
res = CoreModel.dict(self, *args, **kwargs)
498+
# Older servers reject unknown fields in profile/request models. Since `instances`
499+
# did not exist before, do not serialize it when unset.
500+
if self.instances is None:
501+
res.pop("instances", None)
502+
return res
503+
424504

425505
class ProfileProps(CoreModel):
426506
name: Annotated[

0 commit comments

Comments
 (0)