diff --git a/src/dstack/api/server/_fleets.py b/src/dstack/api/server/_fleets.py index 1b6e1fa11..7a635db54 100644 --- a/src/dstack/api/server/_fleets.py +++ b/src/dstack/api/server/_fleets.py @@ -1,4 +1,4 @@ -from typing import List, Optional, Union +from typing import Any, Dict, List, Optional from pydantic import parse_obj_as @@ -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()) @@ -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: diff --git a/src/dstack/api/server/_runs.py b/src/dstack/api/server/_runs.py index 77143f9ad..b70ec1816 100644 --- a/src/dstack/api/server/_runs.py +++ b/src/dstack/api/server/_runs.py @@ -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, @@ -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()) @@ -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: diff --git a/src/dstack/api/server/_volumes.py b/src/dstack/api/server/_volumes.py index 4051473f5..8f6cdaa1a 100644 --- a/src/dstack/api/server/_volumes.py +++ b/src/dstack/api/server/_volumes.py @@ -1,4 +1,4 @@ -from typing import List +from typing import Dict, List from pydantic import parse_obj_as @@ -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}