Skip to content

Commit f98a4cf

Browse files
authored
Disable LEGACY_REPO_DIR with feature flag (#3305)
* Disable LEGACY_REPO_DIR with feature flag Enabling DSTACK_FF_LEGACY_REPO_DIR_DISABLED does the following: - Changes `working_dir` default value from `/workflow` to the image's working dir, unless the client is older than 0.19.27, in which case `/workflow` is still used. - Forbids relative `working_dir` (client side only). - Makes `repos[].path` required, unless the client is older than 0.19.27, in which case `/workflow` is still used Part-of: #3124 * Use $DSTACK_WORKING_DIR in IDE link
1 parent 3b58cae commit f98a4cf

File tree

11 files changed

+65
-22
lines changed

11 files changed

+65
-22
lines changed

src/dstack/_internal/cli/services/configurators/run.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
get_repo_creds_and_default_branch,
5858
load_repo,
5959
)
60+
from dstack._internal.settings import FeatureFlags
6061
from dstack._internal.utils.common import local_time
6162
from dstack._internal.utils.interpolator import InterpolatorError, VariablesInterpolator
6263
from dstack._internal.utils.logging import get_logger
@@ -100,14 +101,18 @@ def apply_configuration(
100101
# Use the default working dir for the image for tasks and services if `commands`
101102
# is not set (emulate pre-0.19.27 JobConfigutor logic), otherwise fall back to
102103
# `/workflow`.
103-
if isinstance(conf, DevEnvironmentConfiguration) or conf.commands:
104+
if not FeatureFlags.LEGACY_REPO_DIR_DISABLED and (
105+
isinstance(conf, DevEnvironmentConfiguration) or conf.commands
106+
):
104107
# relative path for compatibility with pre-0.19.27 servers
105108
conf.working_dir = "."
106109
warn(
107110
f'The [code]working_dir[/code] is not set — using legacy default [code]"{LEGACY_REPO_DIR}"[/code].'
108111
" Future versions will default to the [code]image[/code]'s working directory."
109112
)
110113
elif not is_absolute_posix_path(working_dir):
114+
if FeatureFlags.LEGACY_REPO_DIR_DISABLED:
115+
raise ConfigurationError("`working_dir` must be absolute")
111116
legacy_working_dir = PurePosixPath(LEGACY_REPO_DIR) / working_dir
112117
warn(
113118
"[code]working_dir[/code] is relative."
@@ -124,6 +129,8 @@ def apply_configuration(
124129
pass
125130

126131
if conf.repos and conf.repos[0].path is None:
132+
if FeatureFlags.LEGACY_REPO_DIR_DISABLED:
133+
raise ConfigurationError("`repos[0].path` is not set")
127134
warn(
128135
"[code]repos[0].path[/code] is not set,"
129136
f" using legacy repo path [code]{LEGACY_REPO_DIR}[/code]\n\n"

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

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from dstack._internal.core.models.configurations import LEGACY_REPO_DIR, ServiceConfiguration
55
from dstack._internal.core.models.runs import ApplyRunPlanInput, JobSpec, JobSubmission, RunSpec
66
from dstack._internal.server.schemas.runs import GetRunPlanRequest, ListRunsRequest
7+
from dstack._internal.settings import FeatureFlags
78

89

910
def get_list_runs_excludes(list_runs_request: ListRunsRequest) -> IncludeExcludeSetType:
@@ -133,10 +134,15 @@ def get_run_spec_excludes(run_spec: RunSpec) -> IncludeExcludeDictType:
133134
configuration = run_spec.configuration
134135
profile = run_spec.profile
135136

136-
if run_spec.repo_dir in [None, LEGACY_REPO_DIR]:
137-
spec_excludes["repo_dir"] = True
138-
elif run_spec.repo_dir == "." and configuration.working_dir in [None, LEGACY_REPO_DIR, "."]:
139-
spec_excludes["repo_dir"] = True
137+
if not FeatureFlags.LEGACY_REPO_DIR_DISABLED:
138+
if run_spec.repo_dir in [None, LEGACY_REPO_DIR]:
139+
spec_excludes["repo_dir"] = True
140+
elif run_spec.repo_dir == "." and configuration.working_dir in [
141+
None,
142+
LEGACY_REPO_DIR,
143+
".",
144+
]:
145+
spec_excludes["repo_dir"] = True
140146

141147
if configuration.fleets is None:
142148
configuration_excludes["fleets"] = True

src/dstack/_internal/server/routers/runs.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
)
3636

3737

38-
def use_legacy_default_working_dir(request: Request) -> bool:
38+
def use_legacy_repo_dir(request: Request) -> bool:
3939
client_release = cast(Optional[tuple[int, ...]], request.state.client_release)
4040
return client_release is not None and client_release < (0, 19, 27)
4141

@@ -110,7 +110,7 @@ async def get_plan(
110110
body: GetRunPlanRequest,
111111
session: Annotated[AsyncSession, Depends(get_session)],
112112
user_project: Annotated[tuple[UserModel, ProjectModel], Depends(ProjectMember())],
113-
legacy_default_working_dir: Annotated[bool, Depends(use_legacy_default_working_dir)],
113+
legacy_repo_dir: Annotated[bool, Depends(use_legacy_repo_dir)],
114114
):
115115
"""
116116
Returns a run plan for the given run spec.
@@ -125,7 +125,7 @@ async def get_plan(
125125
user=user,
126126
run_spec=body.run_spec,
127127
max_offers=body.max_offers,
128-
legacy_default_working_dir=legacy_default_working_dir,
128+
legacy_repo_dir=legacy_repo_dir,
129129
)
130130
return CustomORJSONResponse(run_plan)
131131

@@ -138,7 +138,7 @@ async def apply_plan(
138138
body: ApplyRunPlanRequest,
139139
session: Annotated[AsyncSession, Depends(get_session)],
140140
user_project: Annotated[tuple[UserModel, ProjectModel], Depends(ProjectMember())],
141-
legacy_default_working_dir: Annotated[bool, Depends(use_legacy_default_working_dir)],
141+
legacy_repo_dir: Annotated[bool, Depends(use_legacy_repo_dir)],
142142
):
143143
"""
144144
Creates a new run or updates an existing run.
@@ -156,7 +156,7 @@ async def apply_plan(
156156
project=project,
157157
plan=body.plan,
158158
force=body.force,
159-
legacy_default_working_dir=legacy_default_working_dir,
159+
legacy_repo_dir=legacy_repo_dir,
160160
)
161161
)
162162

src/dstack/_internal/server/services/jobs/configurators/base.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,9 @@ def _repo_dir(self) -> str:
313313
Returns absolute or relative path
314314
"""
315315
repo_dir = self.run_spec.repo_dir
316+
# We need this fallback indefinitely, as there may be RunSpecs submitted before
317+
# repos[].path became required, and JobSpec is regenerated from RunSpec on each retry
318+
# and in-place update.
316319
if repo_dir is None:
317320
return LEGACY_REPO_DIR
318321
return repo_dir

src/dstack/_internal/server/services/jobs/configurators/extensions/cursor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,6 @@ def get_print_readme_commands(self) -> List[str]:
3737
return [
3838
"echo To open in Cursor, use link below:",
3939
"echo",
40-
f'echo " cursor://vscode-remote/ssh-remote+{self.run_name}$DSTACK_REPO_DIR"',
40+
f'echo " cursor://vscode-remote/ssh-remote+{self.run_name}$DSTACK_WORKING_DIR"',
4141
"echo",
4242
]

src/dstack/_internal/server/services/jobs/configurators/extensions/vscode.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,6 @@ def get_print_readme_commands(self) -> List[str]:
3737
return [
3838
"echo 'To open in VS Code Desktop, use link below:'",
3939
"echo",
40-
f'echo " vscode://vscode-remote/ssh-remote+{self.run_name}$DSTACK_REPO_DIR"',
40+
f'echo " vscode://vscode-remote/ssh-remote+{self.run_name}$DSTACK_WORKING_DIR"',
4141
"echo",
4242
]

src/dstack/_internal/server/services/runs/__init__.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ async def get_plan(
283283
user: UserModel,
284284
run_spec: RunSpec,
285285
max_offers: Optional[int],
286-
legacy_default_working_dir: bool = False,
286+
legacy_repo_dir: bool = False,
287287
) -> RunPlan:
288288
# Spec must be copied by parsing to calculate merged_profile
289289
effective_run_spec = RunSpec.parse_obj(run_spec.dict())
@@ -296,7 +296,7 @@ async def get_plan(
296296
validate_run_spec_and_set_defaults(
297297
user=user,
298298
run_spec=effective_run_spec,
299-
legacy_default_working_dir=legacy_default_working_dir,
299+
legacy_repo_dir=legacy_repo_dir,
300300
)
301301
profile = effective_run_spec.merged_profile
302302

@@ -342,7 +342,7 @@ async def apply_plan(
342342
project: ProjectModel,
343343
plan: ApplyRunPlanInput,
344344
force: bool,
345-
legacy_default_working_dir: bool = False,
345+
legacy_repo_dir: bool = False,
346346
) -> Run:
347347
run_spec = plan.run_spec
348348
run_spec = await apply_plugin_policies(
@@ -353,7 +353,7 @@ async def apply_plan(
353353
# Spec must be copied by parsing to calculate merged_profile
354354
run_spec = RunSpec.parse_obj(run_spec.dict())
355355
validate_run_spec_and_set_defaults(
356-
user=user, run_spec=run_spec, legacy_default_working_dir=legacy_default_working_dir
356+
user=user, run_spec=run_spec, legacy_repo_dir=legacy_repo_dir
357357
)
358358
if run_spec.run_name is None:
359359
return await submit_run(

src/dstack/_internal/server/services/runs/spec.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from dstack._internal.server.models import UserModel
1010
from dstack._internal.server.services.docker import is_valid_docker_volume_target
1111
from dstack._internal.server.services.resources import set_resources_defaults
12+
from dstack._internal.settings import FeatureFlags
1213
from dstack._internal.utils.logging import get_logger
1314

1415
logger = get_logger(__name__)
@@ -55,7 +56,7 @@
5556

5657

5758
def validate_run_spec_and_set_defaults(
58-
user: UserModel, run_spec: RunSpec, legacy_default_working_dir: bool = False
59+
user: UserModel, run_spec: RunSpec, legacy_repo_dir: bool = False
5960
):
6061
# This function may set defaults for null run_spec values,
6162
# although most defaults are resolved when building job_spec
@@ -111,8 +112,10 @@ def validate_run_spec_and_set_defaults(
111112
run_spec.ssh_key_pub = user.ssh_public_key
112113
else:
113114
raise ServerClientError("ssh_key_pub must be set if the user has no ssh_public_key")
114-
if run_spec.configuration.working_dir is None and legacy_default_working_dir:
115+
if run_spec.configuration.working_dir is None and legacy_repo_dir:
115116
run_spec.configuration.working_dir = LEGACY_REPO_DIR
117+
if run_spec.repo_dir is None and FeatureFlags.LEGACY_REPO_DIR_DISABLED and not legacy_repo_dir:
118+
raise ServerClientError("Repo path is not set")
116119

117120

118121
def check_can_update_run_spec(current_run_spec: RunSpec, new_run_spec: RunSpec):

src/dstack/_internal/settings.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,11 @@ class FeatureFlags:
3636
"""
3737

3838
AUTOCREATED_FLEETS_DISABLED = os.getenv("DSTACK_FF_AUTOCREATED_FLEETS_DISABLED") is not None
39+
40+
# Enabling LEGACY_REPO_DIR_DISABLED does the following:
41+
# - Changes `working_dir` default value from `/workflow` to the image's working dir, unless
42+
# the client is older than 0.19.27, in which case `/workflow` is still used.
43+
# - Forbids relative `working_dir` (client side only).
44+
# - Makes `repos[].path` required, unless the client is older than 0.19.27,
45+
# in which case `/workflow` is still used.
46+
LEGACY_REPO_DIR_DISABLED = os.getenv("DSTACK_FF_LEGACY_REPO_DIR_DISABLED") is not None

src/tests/_internal/server/routers/test_runs.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ def get_dev_env_run_plan_dict(
9797
" && echo"
9898
" && echo 'To open in VS Code Desktop, use link below:'"
9999
" && echo"
100-
' && echo " vscode://vscode-remote/ssh-remote+dry-run$DSTACK_REPO_DIR"'
100+
' && echo " vscode://vscode-remote/ssh-remote+dry-run$DSTACK_WORKING_DIR"'
101101
" && echo"
102102
" && echo 'To connect via SSH, use: `ssh dry-run`'"
103103
" && echo"
@@ -121,7 +121,7 @@ def get_dev_env_run_plan_dict(
121121
" && echo"
122122
" && echo 'To open in VS Code Desktop, use link below:'"
123123
" && echo"
124-
' && echo " vscode://vscode-remote/ssh-remote+dry-run$DSTACK_REPO_DIR"'
124+
' && echo " vscode://vscode-remote/ssh-remote+dry-run$DSTACK_WORKING_DIR"'
125125
" && echo"
126126
" && echo 'To connect via SSH, use: `ssh dry-run`'"
127127
" && echo"
@@ -302,7 +302,7 @@ def get_dev_env_run_dict(
302302
" && echo"
303303
" && echo 'To open in VS Code Desktop, use link below:'"
304304
" && echo"
305-
' && echo " vscode://vscode-remote/ssh-remote+test-run$DSTACK_REPO_DIR"'
305+
' && echo " vscode://vscode-remote/ssh-remote+test-run$DSTACK_WORKING_DIR"'
306306
" && echo"
307307
" && echo 'To connect via SSH, use: `ssh test-run`'"
308308
" && echo"
@@ -326,7 +326,7 @@ def get_dev_env_run_dict(
326326
" && echo"
327327
" && echo 'To open in VS Code Desktop, use link below:'"
328328
" && echo"
329-
' && echo " vscode://vscode-remote/ssh-remote+test-run$DSTACK_REPO_DIR"'
329+
' && echo " vscode://vscode-remote/ssh-remote+test-run$DSTACK_WORKING_DIR"'
330330
" && echo"
331331
" && echo 'To connect via SSH, use: `ssh test-run`'"
332332
" && echo"

0 commit comments

Comments
 (0)