diff --git a/src/dstack/_internal/cli/services/configurators/__init__.py b/src/dstack/_internal/cli/services/configurators/__init__.py index cf0d7f08f..51191758a 100644 --- a/src/dstack/_internal/cli/services/configurators/__init__.py +++ b/src/dstack/_internal/cli/services/configurators/__init__.py @@ -85,4 +85,12 @@ def load_apply_configuration( raise ConfigurationError(f"Failed to load configuration from {configuration_path}") if isinstance(configuration_path, int): return APPLY_STDIN_NAME, conf - return str(configuration_path.absolute().relative_to(Path.cwd())), conf + absolute_path = configuration_path.absolute() + try: + return str(absolute_path.relative_to(Path.cwd())), conf + except ValueError: + # The configuration may live outside the current directory, e.g. when + # passed by absolute path. Fall back to the absolute path instead of + # failing; relative `repos` and `files` entries are resolved against + # the configuration's directory, so they keep working either way. + return str(absolute_path), conf diff --git a/src/tests/_internal/cli/services/configurators/test_apply_configuration.py b/src/tests/_internal/cli/services/configurators/test_apply_configuration.py new file mode 100644 index 000000000..aba9a5d70 --- /dev/null +++ b/src/tests/_internal/cli/services/configurators/test_apply_configuration.py @@ -0,0 +1,65 @@ +from pathlib import Path + +import pytest + +from dstack._internal.cli.services.configurators import load_apply_configuration +from dstack._internal.core.errors import ConfigurationError +from dstack._internal.core.models.configurations import TaskConfiguration + +CONFIG_YAML = "type: task\ncommands:\n - echo hi\n" + + +def write_config(path: Path) -> Path: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(CONFIG_YAML) + return path + + +def chdir_work_dir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path: + work_dir = tmp_path / "work" + work_dir.mkdir(exist_ok=True) + monkeypatch.chdir(work_dir) + return work_dir + + +class TestLoadApplyConfiguration: + def test_configuration_outside_cwd_is_returned_as_absolute_path( + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ): + config_path = write_config(tmp_path / "conf" / ".dstack.yml") + chdir_work_dir(tmp_path, monkeypatch) + + path, conf = load_apply_configuration(str(config_path)) + + assert isinstance(conf, TaskConfiguration) + assert conf.commands == ["echo hi"] + assert Path(path).is_absolute() + assert Path(path) == config_path + + def test_configuration_inside_cwd_is_returned_as_relative_path( + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ): + write_config(tmp_path / "work" / "conf" / ".dstack.yml") + chdir_work_dir(tmp_path, monkeypatch) + + path, conf = load_apply_configuration(str(Path("conf") / ".dstack.yml")) + + assert isinstance(conf, TaskConfiguration) + assert path == str(Path("conf") / ".dstack.yml") + + def test_relative_path_with_parent_segments_stays_relative( + self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ): + write_config(tmp_path / "conf" / ".dstack.yml") + chdir_work_dir(tmp_path, monkeypatch) + + path, conf = load_apply_configuration(str(Path("..") / "conf" / ".dstack.yml")) + + assert isinstance(conf, TaskConfiguration) + assert path == str(Path("..") / "conf" / ".dstack.yml") + + def test_missing_configuration_raises(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch): + chdir_work_dir(tmp_path, monkeypatch) + + with pytest.raises(ConfigurationError): + load_apply_configuration(str(tmp_path / "missing.yml"))