diff --git a/src/dstack/_internal/core/compatibility/runs.py b/src/dstack/_internal/core/compatibility/runs.py index d33462b67..545c729cb 100644 --- a/src/dstack/_internal/core/compatibility/runs.py +++ b/src/dstack/_internal/core/compatibility/runs.py @@ -5,7 +5,10 @@ IncludeExcludeDictType, IncludeExcludeSetType, ) -from dstack._internal.core.models.configurations import TaskConfiguration +from dstack._internal.core.models.configurations import ( + ServiceConfiguration, + TaskConfiguration, +) from dstack._internal.core.models.runs import ( DEFAULT_REPLICA_GROUP_NAME, ApplyRunPlanInput, @@ -89,6 +92,10 @@ def get_run_spec_excludes(run_spec: RunSpec) -> IncludeExcludeDictType: if run_spec.configuration.nodes is None: # Omit nodes when unset so old servers never see null (pre-hetero nodes was int=1). configuration_excludes["nodes"] = True + elif isinstance(run_spec.configuration, ServiceConfiguration): + if run_spec.configuration.groups is None: + # Servers before 0.21.3 have no service `groups` and reject it as an extra field. + configuration_excludes["groups"] = True if configuration_excludes: spec_excludes["configuration"] = configuration_excludes diff --git a/src/tests/_internal/core/models/test_runs.py b/src/tests/_internal/core/models/test_runs.py index b5c613236..3ec6f8024 100644 --- a/src/tests/_internal/core/models/test_runs.py +++ b/src/tests/_internal/core/models/test_runs.py @@ -3,7 +3,10 @@ from dstack._internal.core.compatibility.runs import get_run_spec_excludes from dstack._internal.core.models.common import validate_extra_ignore -from dstack._internal.core.models.configurations import TaskConfiguration +from dstack._internal.core.models.configurations import ( + ServiceConfiguration, + TaskConfiguration, +) from dstack._internal.core.models.profiles import ( CreationPolicy, Profile, @@ -42,6 +45,27 @@ def test_unset_task_nodes_are_excluded_for_compatibility(): assert configuration_excludes["nodes"] is True +def test_unset_service_groups_are_excluded_for_compatibility(): + configuration = ServiceConfiguration(commands=["true"], port=8000) + + configuration_excludes = get_run_spec_excludes(RunSpec(configuration=configuration)).get( + "configuration" + ) + + assert isinstance(configuration_excludes, dict) + assert configuration_excludes["groups"] is True + + +def test_set_service_groups_are_not_excluded(): + configuration = ServiceConfiguration(port=8000, groups=[{"replicas": 1, "commands": ["true"]}]) + + configuration_excludes = get_run_spec_excludes(RunSpec(configuration=configuration)).get( + "configuration" + ) + + assert not isinstance(configuration_excludes, dict) or "groups" not in configuration_excludes + + def test_job_termination_reason_to_status_works_with_all_enum_variants(): for job_termination_reason in JobTerminationReason: job_status = job_termination_reason.to_status()