Skip to content

Commit a5d3202

Browse files
authored
Drop config.yml->repos and LocalRepo support (#3352)
1 parent 7beadf5 commit a5d3202

File tree

13 files changed

+142
-291
lines changed

13 files changed

+142
-291
lines changed

docs/docs/reference/api/python/index.md

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ from dstack.api import Task, GPU, Client, Resources
1414
client = Client.from_config()
1515

1616
task = Task(
17-
name="my-awesome-run", # If not specified, a random name is assigned
17+
name="my-awesome-run", # If not specified, a random name is assigned
1818
image="ghcr.io/huggingface/text-generation-inference:latest",
1919
env={"MODEL_ID": "TheBloke/Llama-2-13B-chat-GPTQ"},
2020
commands=[
@@ -42,7 +42,7 @@ finally:
4242
```
4343

4444
!!! info "NOTE:"
45-
1. The `configuration` argument in the `apply_configuration` method can be either `dstack.api.Task`, `dstack.api.Service`, or `dstack.api.DevEnvironment`.
45+
1. The `configuration` argument in the `apply_configuration` method can be either `dstack.api.Task`, `dstack.api.Service`, or `dstack.api.DevEnvironment`.
4646
2. When you create `dstack.api.Task`, `dstack.api.Service`, or `dstack.api.DevEnvironment`, you can specify the `image` argument. If `image` isn't specified, the default image will be used. For a private Docker registry, ensure you also pass the `registry_auth` argument.
4747
3. The `repo` argument in the `apply_configuration` method allows the mounting of a local folder, a remote repo, or a
4848
programmatically created repo. In this case, the `commands` argument can refer to the files within this repo.
@@ -173,15 +173,6 @@ finally:
173173
memory: dstack.api.Memory
174174
Range: dstack.api.Range
175175

176-
### `dstack.api.LocalRepo` { #dstack.api.LocalRepo data-toc-label="LocalRepo" }
177-
178-
::: dstack.api.LocalRepo
179-
options:
180-
show_bases: false
181-
show_root_heading: false
182-
show_root_toc_entry: false
183-
heading_level: 4
184-
185176
### `dstack.api.RemoteRepo` { #dstack.api.RemoteRepo data-toc-label="RemoteRepo" }
186177

187178
::: dstack.api.RemoteRepo

src/dstack/_internal/cli/commands/init.py

Lines changed: 4 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,11 @@
66
from dstack._internal.cli.commands import BaseCommand
77
from dstack._internal.cli.services.repos import (
88
get_repo_from_dir,
9+
get_repo_from_url,
910
is_git_repo_url,
1011
register_init_repo_args,
1112
)
12-
from dstack._internal.cli.utils.common import confirm_ask, console, warn
13-
from dstack._internal.core.errors import ConfigurationError
14-
from dstack._internal.core.models.repos.remote import RemoteRepo
15-
from dstack._internal.core.services.configs import ConfigManager
13+
from dstack._internal.cli.utils.common import console
1614
from dstack.api import Client
1715

1816

@@ -36,20 +34,6 @@ def _register(self):
3634
dest="repo",
3735
)
3836
register_init_repo_args(self._parser)
39-
# Deprecated since 0.19.25, ignored
40-
self._parser.add_argument(
41-
"--ssh-identity",
42-
metavar="SSH_PRIVATE_KEY",
43-
help=argparse.SUPPRESS,
44-
type=Path,
45-
dest="ssh_identity_file",
46-
)
47-
# A hidden mode for transitional period only, remove it with repos in `config.yml`
48-
self._parser.add_argument(
49-
"--remove",
50-
help=argparse.SUPPRESS,
51-
action="store_true",
52-
)
5337

5438
def _command(self, args: argparse.Namespace):
5539
super()._command(args)
@@ -65,45 +49,10 @@ def _command(self, args: argparse.Namespace):
6549
else:
6650
repo_path = Path.cwd()
6751

68-
if args.remove:
69-
if repo_url is not None:
70-
raise ConfigurationError(f"Local path expected, got URL: {repo_url}")
71-
assert repo_path is not None
72-
config_manager = ConfigManager()
73-
repo_config = config_manager.get_repo_config(repo_path)
74-
if repo_config is None:
75-
raise ConfigurationError("Repo record not found, nothing to remove")
76-
console.print(
77-
f"You are about to remove the repo {repo_path}\n"
78-
"Only the record about the repo will be removed,"
79-
" the repo files will remain intact\n"
80-
)
81-
if not confirm_ask("Remove the repo?"):
82-
return
83-
config_manager.delete_repo_config(repo_config.repo_id)
84-
config_manager.save()
85-
console.print("Repo has been removed")
86-
return
87-
88-
local: bool = args.local
89-
if local:
90-
warn(
91-
"Local repos are deprecated since 0.19.25 and will be removed soon. Consider"
92-
" using [code]files[/code] instead: https://dstack.ai/docs/concepts/tasks/#files"
93-
)
94-
if args.ssh_identity_file:
95-
warn(
96-
"[code]--ssh-identity[/code] in [code]dstack init[/code] is deprecated and ignored"
97-
" since 0.19.25. Use this option with [code]dstack apply[/code]"
98-
" and [code]dstack attach[/code] instead"
99-
)
100-
10152
if repo_url is not None:
102-
# Dummy repo branch to avoid autodetection that fails on private repos.
103-
# We don't need branch/hash for repo_id anyway.
104-
repo = RemoteRepo.from_url(repo_url, repo_branch="master")
53+
repo = get_repo_from_url(repo_url)
10554
elif repo_path is not None:
106-
repo = get_repo_from_dir(repo_path, local=local)
55+
repo = get_repo_from_dir(repo_path)
10756
else:
10857
assert False, "should not reach here"
10958
api = Client.from_config(project_name=args.project)

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

Lines changed: 42 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@
1818
from dstack._internal.cli.services.profile import apply_profile_args, register_profile_args
1919
from dstack._internal.cli.services.repos import (
2020
get_repo_from_dir,
21+
get_repo_from_url,
2122
init_default_virtual_repo,
2223
is_git_repo_url,
2324
register_init_repo_args,
2425
)
25-
from dstack._internal.cli.utils.common import confirm_ask, console, warn
26+
from dstack._internal.cli.utils.common import confirm_ask, console
2627
from dstack._internal.cli.utils.rich import MultiItemStatus
2728
from dstack._internal.cli.utils.run import get_runs_table, print_run_plan
2829
from dstack._internal.core.errors import (
@@ -44,17 +45,13 @@
4445
TaskConfiguration,
4546
)
4647
from dstack._internal.core.models.repos import RepoHeadWithCreds
47-
from dstack._internal.core.models.repos.base import Repo
48-
from dstack._internal.core.models.repos.local import LocalRepo
4948
from dstack._internal.core.models.repos.remote import RemoteRepo, RemoteRepoCreds
5049
from dstack._internal.core.models.resources import CPUSpec
5150
from dstack._internal.core.models.runs import JobStatus, JobSubmission, RunSpec, RunStatus
52-
from dstack._internal.core.services.configs import ConfigManager
5351
from dstack._internal.core.services.diff import diff_models
5452
from dstack._internal.core.services.repos import (
5553
InvalidRepoCredentialsError,
5654
get_repo_creds_and_default_branch,
57-
load_repo,
5855
)
5956
from dstack._internal.utils.common import local_time
6057
from dstack._internal.utils.interpolator import InterpolatorError, VariablesInterpolator
@@ -96,8 +93,9 @@ def apply_configuration(
9693
if conf.working_dir is not None and not is_absolute_posix_path(conf.working_dir):
9794
raise ConfigurationError("working_dir must be absolute")
9895

99-
config_manager = ConfigManager()
100-
repo = self.get_repo(conf, configuration_path, configurator_args, config_manager)
96+
repo = self.get_repo(conf, configuration_path, configurator_args)
97+
if repo is None:
98+
repo = init_default_virtual_repo(api=self.api)
10199
profile = load_profile(Path.cwd(), configurator_args.profile)
102100
with console.status("Getting apply plan..."):
103101
run_plan = self.api.runs.get_run_plan(
@@ -475,12 +473,11 @@ def get_repo(
475473
conf: RunConfigurationT,
476474
configuration_path: str,
477475
configurator_args: argparse.Namespace,
478-
config_manager: ConfigManager,
479-
) -> Repo:
476+
) -> Optional[RemoteRepo]:
480477
if configurator_args.no_repo:
481-
return init_default_virtual_repo(api=self.api)
478+
return None
482479

483-
repo: Optional[Repo] = None
480+
repo: Optional[RemoteRepo] = None
484481
repo_head: Optional[RepoHeadWithCreds] = None
485482
repo_branch: Optional[str] = configurator_args.repo_branch
486483
repo_hash: Optional[str] = configurator_args.repo_hash
@@ -497,8 +494,6 @@ def get_repo(
497494
local_path: Optional[Path] = None
498495
# dummy value, safe to join with any path
499496
root_dir = Path(".")
500-
# True if no repo specified, but we found one in `config.yml`
501-
legacy_local_path = False
502497
if repo_arg := configurator_args.repo:
503498
if is_git_repo_url(repo_arg):
504499
url = repo_arg
@@ -521,84 +516,49 @@ def get_repo(
521516
if repo_hash is None:
522517
repo_hash = repo_spec.hash
523518
else:
524-
local_path = Path.cwd()
525-
legacy_local_path = True
519+
return None
520+
526521
if url:
527-
repo = RemoteRepo.from_url(repo_url=url)
522+
repo = get_repo_from_url(url)
528523
repo_head = self.api.repos.get(repo_id=repo.repo_id, with_creds=True)
529524
elif local_path:
530-
if legacy_local_path:
531-
if repo_config := config_manager.get_repo_config(local_path):
532-
repo = load_repo(repo_config)
533-
repo_head = self.api.repos.get(repo_id=repo.repo_id, with_creds=True)
534-
if repo_head is not None:
535-
warn(
536-
"The repo is not specified but found and will be used in the run\n"
537-
"Future versions will not load repos automatically\n"
538-
"To prepare for future versions and get rid of this warning:\n"
539-
"- If you need the repo in the run, either specify [code]repos[/code]"
540-
" in the configuration or use [code]--repo .[/code]\n"
541-
"- If you don't need the repo in the run, either run"
542-
" [code]dstack init --remove[/code] once (it removes only the record"
543-
" about the repo, the repo files will remain intact)"
544-
" or use [code]--no-repo[/code]"
545-
)
546-
else:
547-
# ignore stale entries in `config.yml`
548-
repo = None
549-
init = False
550-
else:
551-
original_local_path = local_path
552-
local_path = local_path.expanduser()
553-
if not local_path.is_absolute():
554-
local_path = (root_dir / local_path).resolve()
555-
if not local_path.exists():
556-
raise ConfigurationError(
557-
f"Invalid repo path: {original_local_path} -> {local_path}"
558-
)
559-
local: bool = configurator_args.local
560-
repo = get_repo_from_dir(local_path, local=local)
561-
repo_head = self.api.repos.get(repo_id=repo.repo_id, with_creds=True)
562-
if isinstance(repo, RemoteRepo):
563-
repo_branch = repo.run_repo_data.repo_branch
564-
repo_hash = repo.run_repo_data.repo_hash
525+
original_local_path = local_path
526+
local_path = local_path.expanduser()
527+
if not local_path.is_absolute():
528+
local_path = (root_dir / local_path).resolve()
529+
if not local_path.exists():
530+
raise ConfigurationError(
531+
f"Invalid repo path: {original_local_path} -> {local_path}"
532+
)
533+
repo = get_repo_from_dir(local_path)
534+
repo_head = self.api.repos.get(repo_id=repo.repo_id, with_creds=True)
535+
repo_branch = repo.run_repo_data.repo_branch
536+
repo_hash = repo.run_repo_data.repo_hash
565537
else:
566538
assert False, "should not reach here"
567539

568-
if repo is None:
569-
return init_default_virtual_repo(api=self.api)
540+
assert repo.repo_url is not None
570541

571-
if isinstance(repo, RemoteRepo):
572-
assert repo.repo_url is not None
542+
if repo_head is not None and repo_head.repo_creds is not None:
543+
if git_identity_file is None and oauth_token is None:
544+
git_private_key = repo_head.repo_creds.private_key
545+
oauth_token = repo_head.repo_creds.oauth_token
546+
else:
547+
init = True
573548

574-
if repo_head is not None and repo_head.repo_creds is not None:
575-
if git_identity_file is None and oauth_token is None:
576-
git_private_key = repo_head.repo_creds.private_key
577-
oauth_token = repo_head.repo_creds.oauth_token
578-
else:
579-
init = True
549+
try:
550+
repo_creds, _ = get_repo_creds_and_default_branch(
551+
repo_url=repo.repo_url,
552+
identity_file=git_identity_file,
553+
private_key=git_private_key,
554+
oauth_token=oauth_token,
555+
)
556+
except InvalidRepoCredentialsError as e:
557+
raise CLIError(*e.args) from e
580558

581-
try:
582-
repo_creds, default_repo_branch = get_repo_creds_and_default_branch(
583-
repo_url=repo.repo_url,
584-
identity_file=git_identity_file,
585-
private_key=git_private_key,
586-
oauth_token=oauth_token,
587-
)
588-
except InvalidRepoCredentialsError as e:
589-
raise CLIError(*e.args) from e
590-
591-
if repo_branch is None and repo_hash is None:
592-
if default_repo_branch is None:
593-
raise CLIError(
594-
"Failed to automatically detect remote repo branch."
595-
" Specify branch or hash."
596-
)
597-
# TODO: remove in 0.20. Currently `default_repo_branch` is sent only for backward compatibility of `dstack-runner`.
598-
repo_branch = default_repo_branch
599-
repo.run_repo_data.repo_branch = repo_branch
600-
if repo_hash is not None:
601-
repo.run_repo_data.repo_hash = repo_hash
559+
repo.run_repo_data.repo_branch = repo_branch
560+
if repo_hash is not None:
561+
repo.run_repo_data.repo_hash = repo_hash
602562

603563
if init:
604564
self.api.repos.init(
@@ -608,15 +568,6 @@ def get_repo(
608568
creds=repo_creds,
609569
)
610570

611-
if isinstance(repo, LocalRepo):
612-
warn(
613-
f"{repo.repo_dir} is a local repo\n"
614-
"Local repos are deprecated since 0.19.25 and will be removed soon\n"
615-
"There are two options:\n"
616-
"- Migrate to [code]files[/code]: https://dstack.ai/docs/concepts/tasks/#files\n"
617-
"- Specify [code]--no-repo[/code] if you don't need the repo at all"
618-
)
619-
620571
return repo
621572

622573

src/dstack/_internal/cli/services/repos.py

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
import argparse
2-
from typing import Literal, Union, overload
1+
from pathlib import Path
32

43
import git
54

65
from dstack._internal.cli.services.configurators.base import ArgsParser
76
from dstack._internal.core.errors import CLIError
8-
from dstack._internal.core.models.repos.local import LocalRepo
97
from dstack._internal.core.models.repos.remote import GitRepoURL, RemoteRepo, RepoError
108
from dstack._internal.core.models.repos.virtual import VirtualRepo
119
from dstack._internal.utils.path import PathLike
@@ -28,12 +26,6 @@ def register_init_repo_args(parser: ArgsParser):
2826
type=str,
2927
dest="git_identity_file",
3028
)
31-
# Deprecated since 0.19.25
32-
parser.add_argument(
33-
"--local",
34-
action="store_true",
35-
help=argparse.SUPPRESS,
36-
)
3729

3830

3931
def init_default_virtual_repo(api: Client) -> VirtualRepo:
@@ -42,17 +34,12 @@ def init_default_virtual_repo(api: Client) -> VirtualRepo:
4234
return repo
4335

4436

45-
@overload
46-
def get_repo_from_dir(repo_dir: PathLike, local: Literal[False] = False) -> RemoteRepo: ...
47-
48-
49-
@overload
50-
def get_repo_from_dir(repo_dir: PathLike, local: Literal[True]) -> LocalRepo: ...
51-
52-
53-
def get_repo_from_dir(repo_dir: PathLike, local: bool = False) -> Union[RemoteRepo, LocalRepo]:
54-
if local:
55-
return LocalRepo.from_dir(repo_dir)
37+
def get_repo_from_dir(repo_dir: PathLike) -> RemoteRepo:
38+
repo_dir = Path(repo_dir)
39+
if not repo_dir.exists():
40+
raise CLIError(f"Path does not exist: {repo_dir}")
41+
if not repo_dir.is_dir():
42+
raise CLIError(f"Path is not a directory: {repo_dir}")
5643
try:
5744
return RemoteRepo.from_dir(repo_dir)
5845
except git.InvalidGitRepositoryError:
@@ -61,6 +48,17 @@ def get_repo_from_dir(repo_dir: PathLike, local: bool = False) -> Union[RemoteRe
6148
"Use `files` to mount an arbitrary directory:"
6249
" https://dstack.ai/docs/concepts/tasks/#files"
6350
)
51+
except git.GitError as e:
52+
raise CLIError(f"{e.__class__.__name__}: {e}") from e
53+
except RepoError as e:
54+
raise CLIError(str(e)) from e
55+
56+
57+
def get_repo_from_url(repo_url: str) -> RemoteRepo:
58+
try:
59+
return RemoteRepo.from_url(repo_url)
60+
except git.GitError as e:
61+
raise CLIError(f"{e.__class__.__name__}: {e}") from e
6462
except RepoError as e:
6563
raise CLIError(str(e)) from e
6664

0 commit comments

Comments
 (0)