Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 15 additions & 57 deletions src/dstack/api/server/_fleets.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import List, Optional, Union
from typing import Any, Dict, List, Optional

from pydantic import parse_obj_as

Expand All @@ -22,7 +22,7 @@ def get(self, project_name: str, name: str) -> Fleet:
body = GetFleetRequest(name=name)
resp = self._request(
f"/api/project/{project_name}/fleets/get",
body=body.json(exclude={"id"}), # `id` is not supported in pre-0.18.36 servers
body=body.json(),
)
return parse_obj_as(Fleet.__response__, resp.json())

Expand Down Expand Up @@ -55,62 +55,20 @@ def delete_instances(self, project_name: str, name: str, instance_nums: List[int
self._request(f"/api/project/{project_name}/fleets/delete_instances", body=body.json())


_ExcludeDict = dict[str, Union[bool, set[str], "_ExcludeDict"]]


def _get_fleet_spec_excludes(fleet_spec: FleetSpec) -> Optional[_ExcludeDict]:
spec_excludes: _ExcludeDict = {}
configuration_excludes: _ExcludeDict = {}
def _get_fleet_spec_excludes(fleet_spec: FleetSpec) -> Optional[Dict]:
"""
Returns `fleet_spec` exclude mapping to exclude certain fields from the request.
Use this method to exclude new fields when they are not set to keep
clients backward-compatibility with older servers.
"""
spec_excludes: Dict[str, Any] = {}
configuration_excludes: Dict[str, Any] = {}
profile_excludes: set[str] = set()
ssh_config_excludes: _ExcludeDict = {}
ssh_hosts_excludes: set[str] = set()

# TODO: Can be removed in 0.19
if fleet_spec.configuration_path is None:
spec_excludes["configuration_path"] = True
if fleet_spec.configuration.ssh_config is not None:
if fleet_spec.configuration.ssh_config.proxy_jump is None:
ssh_config_excludes["proxy_jump"] = True
if all(
isinstance(h, str) or h.proxy_jump is None
for h in fleet_spec.configuration.ssh_config.hosts
):
ssh_hosts_excludes.add("proxy_jump")
if all(
isinstance(h, str) or h.internal_ip is None
for h in fleet_spec.configuration.ssh_config.hosts
):
ssh_hosts_excludes.add("internal_ip")
if all(
isinstance(h, str) or h.blocks == 1 for h in fleet_spec.configuration.ssh_config.hosts
):
ssh_hosts_excludes.add("blocks")
# client >= 0.18.30 / server <= 0.18.29 compatibility tweak
if fleet_spec.configuration.reservation is None:
configuration_excludes["reservation"] = True
if fleet_spec.profile is not None and fleet_spec.profile.reservation is None:
profile_excludes.add("reservation")
if fleet_spec.configuration.idle_duration is None:
configuration_excludes["idle_duration"] = True
if fleet_spec.profile is not None and fleet_spec.profile.idle_duration is None:
profile_excludes.add("idle_duration")
# client >= 0.18.38 / server <= 0.18.37 compatibility tweak
if fleet_spec.profile is not None and fleet_spec.profile.stop_duration is None:
profile_excludes.add("stop_duration")
# client >= 0.18.41 / server <= 0.18.40 compatibility tweak
if fleet_spec.configuration.availability_zones is None:
configuration_excludes["availability_zones"] = True
if fleet_spec.profile is not None and fleet_spec.profile.availability_zones is None:
profile_excludes.add("availability_zones")
if fleet_spec.configuration.blocks == 1:
configuration_excludes["blocks"] = True
if fleet_spec.profile is not None and fleet_spec.profile.utilization_policy is None:
profile_excludes.add("utilization_policy")

if ssh_hosts_excludes:
ssh_config_excludes["hosts"] = {"__all__": ssh_hosts_excludes}
if ssh_config_excludes:
configuration_excludes["ssh_config"] = ssh_config_excludes
# Fields can be excluded like this:
# if fleet_spec.configuration.availability_zones is None:
# configuration_excludes["availability_zones"] = True
# if fleet_spec.profile is not None and fleet_spec.profile.availability_zones is None:
# profile_excludes.add("availability_zones")
if configuration_excludes:
spec_excludes["configuration"] = configuration_excludes
if profile_excludes:
Expand Down
87 changes: 21 additions & 66 deletions src/dstack/api/server/_runs.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,15 @@
from datetime import datetime
from typing import Any, List, Optional, Union
from typing import Any, Dict, List, Optional, Union
from uuid import UUID

from pydantic import parse_obj_as

from dstack._internal.core.models.common import is_core_model_instance
from dstack._internal.core.models.configurations import (
STRIP_PREFIX_DEFAULT,
DevEnvironmentConfiguration,
ServiceConfiguration,
)
from dstack._internal.core.models.runs import (
ApplyRunPlanInput,
Run,
RunPlan,
RunSpec,
)
from dstack._internal.core.models.volumes import InstanceMountPoint
from dstack._internal.server.schemas.runs import (
ApplyRunPlanRequest,
DeleteRunsRequest,
Expand Down Expand Up @@ -55,8 +48,7 @@ def list(

def get(self, project_name: str, run_name: str) -> Run:
body = GetRunRequest(run_name=run_name)
# dstack versions prior to 0.18.34 don't support id field, and we don't use it here either
json_body = body.json(exclude={"id"})
json_body = body.json()
resp = self._request(f"/api/project/{project_name}/runs/get", body=json_body)
return parse_obj_as(Run.__response__, resp.json())

Expand Down Expand Up @@ -91,71 +83,34 @@ def delete(self, project_name: str, runs_names: List[str]):
self._request(f"/api/project/{project_name}/runs/delete", body=body.json())


def _get_apply_plan_excludes(plan: ApplyRunPlanInput) -> Optional[dict]:
def _get_apply_plan_excludes(plan: ApplyRunPlanInput) -> Optional[Dict]:
"""
Returns `plan` exclude mapping to exclude certain fields from the request.
Use this method to exclude new fields when they are not set to keep
clients backward-compatibility with older servers.
"""
run_spec_excludes = _get_run_spec_excludes(plan.run_spec)
if run_spec_excludes is not None:
return {"plan": run_spec_excludes}
return None


def _get_run_spec_excludes(run_spec: RunSpec) -> Optional[dict]:
def _get_run_spec_excludes(run_spec: RunSpec) -> Optional[Dict]:
"""
Returns `run_spec` exclude mapping to exclude certain fields from the request.
Use this method to exclude new fields when they are not set to keep
clients backward-compatibility with older servers.
"""
spec_excludes: dict[str, Any] = {}
configuration_excludes: dict[str, Any] = {}
profile_excludes: set[str] = set()
configuration = run_spec.configuration
profile = run_spec.profile

# client >= 0.18.18 / server <= 0.18.17 compatibility tweak
if not configuration.privileged:
configuration_excludes["privileged"] = True
# client >= 0.18.23 / server <= 0.18.22 compatibility tweak
if configuration.type == "service" and configuration.gateway is None:
configuration_excludes["gateway"] = True
# client >= 0.18.30 / server <= 0.18.29 compatibility tweak
if run_spec.configuration.user is None:
configuration_excludes["user"] = True
# client >= 0.18.30 / server <= 0.18.29 compatibility tweak
if configuration.reservation is None:
configuration_excludes["reservation"] = True
if profile is not None and profile.reservation is None:
profile_excludes.add("reservation")
if configuration.idle_duration is None:
configuration_excludes["idle_duration"] = True
if profile is not None and profile.idle_duration is None:
profile_excludes.add("idle_duration")
# client >= 0.18.38 / server <= 0.18.37 compatibility tweak
if configuration.stop_duration is None:
configuration_excludes["stop_duration"] = True
if profile is not None and profile.stop_duration is None:
profile_excludes.add("stop_duration")
# client >= 0.18.40 / server <= 0.18.39 compatibility tweak
if (
is_core_model_instance(configuration, ServiceConfiguration)
and configuration.strip_prefix == STRIP_PREFIX_DEFAULT
):
configuration_excludes["strip_prefix"] = True
if configuration.single_branch is None:
configuration_excludes["single_branch"] = True
if all(
not is_core_model_instance(v, InstanceMountPoint) or not v.optional
for v in configuration.volumes
):
configuration_excludes["volumes"] = {"__all__": {"optional"}}
# client >= 0.18.41 / server <= 0.18.40 compatibility tweak
if configuration.availability_zones is None:
configuration_excludes["availability_zones"] = True
if profile is not None and profile.availability_zones is None:
profile_excludes.add("availability_zones")
if (
is_core_model_instance(configuration, DevEnvironmentConfiguration)
and configuration.inactivity_duration is None
):
configuration_excludes["inactivity_duration"] = True
if configuration.utilization_policy is None:
configuration_excludes["utilization_policy"] = True
if profile is not None and profile.utilization_policy is None:
profile_excludes.add("utilization_policy")

# configuration = run_spec.configuration
# profile = run_spec.profile
# Fields can be excluded like this:
# if configuration.availability_zones is None:
# configuration_excludes["availability_zones"] = True
# if profile is not None and profile.availability_zones is None:
# profile_excludes.add("availability_zones")
if configuration_excludes:
spec_excludes["configuration"] = configuration_excludes
if profile_excludes:
Expand Down
15 changes: 10 additions & 5 deletions src/dstack/api/server/_volumes.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import List
from typing import Dict, List

from pydantic import parse_obj_as

Expand Down Expand Up @@ -38,9 +38,14 @@ def delete(self, project_name: str, names: List[str]) -> None:
self._request(f"/api/project/{project_name}/volumes/delete", body=body.json())


def _get_volume_configuration_excludes(configuration: VolumeConfiguration) -> dict:
def _get_volume_configuration_excludes(configuration: VolumeConfiguration) -> Dict:
"""
Returns `configuration` exclude mapping to exclude certain fields from the request.
Use this method to exclude new fields when they are not set to keep
clients backward-compatibility with older servers.
"""
configuration_excludes = {}
# client >= 0.18.41 / server <= 0.18.40 compatibility tweak
if configuration.availability_zone is None:
configuration_excludes["availability_zone"] = True
# Fields can be excluded like this:
# if configuration.availability_zone is None:
# configuration_excludes["availability_zone"] = True
return {"configuration": configuration_excludes}