Skip to content

Commit 2a5b06e

Browse files
authored
Add declarative repo configuration (#3023)
* `repos[].local_path,url` are equivalent to `--repo` option, the CLI option overrides the YAML configuration * Legacy flow with implicitly loaded repos still supported but deprecated * Repo info is no longer written to `config.yml`, but existing records are used for legacy flow * `dstack init` is extended with `--repo`/`-P`, as in `dstack apply` Updated docs and `repos[].path` support will be added in separate PRs. Part-of: #2851
1 parent ebcd2c4 commit 2a5b06e

File tree

19 files changed

+474
-214
lines changed

19 files changed

+474
-214
lines changed

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

Lines changed: 1 addition & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import argparse
2-
from pathlib import Path
32

43
from argcomplete import FilesCompleter # type: ignore[attr-defined]
54

@@ -9,12 +8,7 @@
98
get_apply_configurator_class,
109
load_apply_configuration,
1110
)
12-
from dstack._internal.cli.services.repos import (
13-
init_default_virtual_repo,
14-
init_repo,
15-
register_init_repo_args,
16-
)
17-
from dstack._internal.cli.utils.common import console, warn
11+
from dstack._internal.cli.utils.common import console
1812
from dstack._internal.core.errors import CLIError
1913
from dstack._internal.core.models.configurations import ApplyConfigurationType
2014

@@ -66,37 +60,6 @@ def _register(self):
6660
help="Exit immediately after submitting configuration",
6761
action="store_true",
6862
)
69-
self._parser.add_argument(
70-
"--ssh-identity",
71-
metavar="SSH_PRIVATE_KEY",
72-
help="The private SSH key path for SSH tunneling",
73-
type=Path,
74-
dest="ssh_identity_file",
75-
)
76-
repo_group = self._parser.add_argument_group("Repo Options")
77-
repo_group.add_argument(
78-
"-P",
79-
"--repo",
80-
help=("The repo to use for the run. Can be a local path or a Git repo URL."),
81-
dest="repo",
82-
)
83-
repo_group.add_argument(
84-
"--repo-branch",
85-
help="The repo branch to use for the run",
86-
dest="repo_branch",
87-
)
88-
repo_group.add_argument(
89-
"--repo-hash",
90-
help="The hash of the repo commit to use for the run",
91-
dest="repo_hash",
92-
)
93-
repo_group.add_argument(
94-
"--no-repo",
95-
help="Do not use any repo for the run",
96-
dest="no_repo",
97-
action="store_true",
98-
)
99-
register_init_repo_args(repo_group)
10063

10164
def _command(self, args: argparse.Namespace):
10265
try:
@@ -117,26 +80,6 @@ def _command(self, args: argparse.Namespace):
11780
super()._command(args)
11881
if not args.yes and args.configuration_file == APPLY_STDIN_NAME:
11982
raise CLIError("Cannot read configuration from stdin if -y/--yes is not specified")
120-
if args.repo and args.no_repo:
121-
raise CLIError("Either --repo or --no-repo can be specified")
122-
if args.local:
123-
warn(
124-
"Local repos are deprecated since 0.19.25 and will be removed soon."
125-
" Consider using `files` instead: https://dstack.ai/docs/concepts/tasks/#files"
126-
)
127-
repo = None
128-
if args.repo:
129-
repo = init_repo(
130-
api=self.api,
131-
repo_path=args.repo,
132-
repo_branch=args.repo_branch,
133-
repo_hash=args.repo_hash,
134-
local=args.local,
135-
git_identity_file=args.git_identity_file,
136-
oauth_token=args.gh_token,
137-
)
138-
elif args.no_repo:
139-
repo = init_default_virtual_repo(api=self.api)
14083
configuration_path, configuration = load_apply_configuration(args.configuration_file)
14184
configurator_class = get_apply_configurator_class(configuration.type)
14285
configurator = configurator_class(api_client=self.api)
@@ -148,7 +91,6 @@ def _command(self, args: argparse.Namespace):
14891
command_args=args,
14992
configurator_args=known,
15093
unknown_args=unknown,
151-
repo=repo,
15294
)
15395
except KeyboardInterrupt:
15496
console.print("\nOperation interrupted by user. Exiting...")

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

Lines changed: 56 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
import argparse
22
import os
33
from pathlib import Path
4+
from typing import Optional
45

56
from dstack._internal.cli.commands import BaseCommand
6-
from dstack._internal.cli.services.repos import init_repo, register_init_repo_args
7+
from dstack._internal.cli.services.repos import (
8+
get_repo_from_dir,
9+
get_repo_from_url,
10+
is_git_repo_url,
11+
register_init_repo_args,
12+
)
713
from dstack._internal.cli.utils.common import configure_logging, confirm_ask, console, warn
814
from dstack._internal.core.errors import ConfigurationError
9-
from dstack._internal.core.models.repos.base import RepoType
1015
from dstack._internal.core.services.configs import ConfigManager
1116
from dstack.api import Client
1217

@@ -21,6 +26,15 @@ def _register(self):
2126
help="The name of the project",
2227
default=os.getenv("DSTACK_PROJECT"),
2328
)
29+
self._parser.add_argument(
30+
"-P",
31+
"--repo",
32+
help=(
33+
"The repo to initialize. Can be a local path or a Git repo URL."
34+
" Defaults to the current working directory."
35+
),
36+
dest="repo",
37+
)
2438
register_init_repo_args(self._parser)
2539
# Deprecated since 0.19.25, ignored
2640
self._parser.add_argument(
@@ -30,7 +44,7 @@ def _register(self):
3044
type=Path,
3145
dest="ssh_identity_file",
3246
)
33-
# A hidden mode for transitional period only, remove it with local repos
47+
# A hidden mode for transitional period only, remove it with repos in `config.yml`
3448
self._parser.add_argument(
3549
"--remove",
3650
help=argparse.SUPPRESS,
@@ -39,44 +53,62 @@ def _register(self):
3953

4054
def _command(self, args: argparse.Namespace):
4155
configure_logging()
56+
57+
repo_path: Optional[Path] = None
58+
repo_url: Optional[str] = None
59+
repo_arg: Optional[str] = args.repo
60+
if repo_arg is not None:
61+
if is_git_repo_url(repo_arg):
62+
repo_url = repo_arg
63+
else:
64+
repo_path = Path(repo_arg).expanduser().resolve()
65+
else:
66+
repo_path = Path.cwd()
67+
4268
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
4372
config_manager = ConfigManager()
44-
repo_path = Path.cwd()
4573
repo_config = config_manager.get_repo_config(repo_path)
4674
if repo_config is None:
47-
raise ConfigurationError("The repo is not initialized, nothing to remove")
48-
if repo_config.repo_type != RepoType.LOCAL:
49-
raise ConfigurationError("`dstack init --remove` is for local repos only")
75+
raise ConfigurationError("Repo record not found, nothing to remove")
5076
console.print(
51-
f"You are about to remove the local repo {repo_path}\n"
77+
f"You are about to remove the repo {repo_path}\n"
5278
"Only the record about the repo will be removed,"
5379
" the repo files will remain intact\n"
5480
)
55-
if not confirm_ask("Remove the local repo?"):
81+
if not confirm_ask("Remove the repo?"):
5682
return
5783
config_manager.delete_repo_config(repo_config.repo_id)
5884
config_manager.save()
59-
console.print("Local repo has been removed")
85+
console.print("Repo has been removed")
6086
return
61-
api = Client.from_config(
62-
project_name=args.project, ssh_identity_file=args.ssh_identity_file
63-
)
64-
if args.local:
87+
88+
local: bool = args.local
89+
if local:
6590
warn(
66-
"Local repos are deprecated since 0.19.25 and will be removed soon."
67-
" Consider using `files` instead: https://dstack.ai/docs/concepts/tasks/#files"
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"
6893
)
6994
if args.ssh_identity_file:
7095
warn(
71-
"`--ssh-identity` in `dstack init` is deprecated and ignored since 0.19.25."
72-
" Use this option with `dstack apply` and `dstack attach` instead"
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"
7399
)
74-
init_repo(
75-
api=api,
76-
repo_path=Path.cwd(),
77-
repo_branch=None,
78-
repo_hash=None,
79-
local=args.local,
100+
101+
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 = get_repo_from_url(repo_url, repo_branch="master")
105+
elif repo_path is not None:
106+
repo = get_repo_from_dir(repo_path, local=local)
107+
else:
108+
assert False, "should not reach here"
109+
api = Client.from_config(project_name=args.project)
110+
api.repos.init(
111+
repo=repo,
80112
git_identity_file=args.git_identity_file,
81113
oauth_token=args.gh_token,
82114
)

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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import argparse
22
import os
33
from abc import ABC, abstractmethod
4-
from typing import Generic, List, Optional, TypeVar, Union, cast
4+
from typing import Generic, List, TypeVar, Union, cast
55

66
from dstack._internal.cli.services.args import env_var
77
from dstack._internal.core.errors import ConfigurationError
@@ -10,7 +10,6 @@
1010
ApplyConfigurationType,
1111
)
1212
from dstack._internal.core.models.envs import Env, EnvSentinel, EnvVarTuple
13-
from dstack._internal.core.models.repos.base import Repo
1413
from dstack.api._public import Client
1514

1615
ArgsParser = Union[argparse._ArgumentGroup, argparse.ArgumentParser]
@@ -32,7 +31,6 @@ def apply_configuration(
3231
command_args: argparse.Namespace,
3332
configurator_args: argparse.Namespace,
3433
unknown_args: List[str],
35-
repo: Optional[Repo] = None,
3634
):
3735
"""
3836
Implements `dstack apply` for a given configuration type.
@@ -43,7 +41,6 @@ def apply_configuration(
4341
command_args: The args parsed by `dstack apply`.
4442
configurator_args: The known args parsed by `cls.get_parser()`.
4543
unknown_args: The unknown args after parsing by `cls.get_parser()`.
46-
repo: The repo to use with apply.
4744
"""
4845
pass
4946

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535
InstanceGroupPlacement,
3636
)
3737
from dstack._internal.core.models.instances import InstanceAvailability, InstanceStatus, SSHKey
38-
from dstack._internal.core.models.repos.base import Repo
3938
from dstack._internal.core.services.diff import diff_models
4039
from dstack._internal.utils.common import local_time
4140
from dstack._internal.utils.logging import get_logger
@@ -56,7 +55,6 @@ def apply_configuration(
5655
command_args: argparse.Namespace,
5756
configurator_args: argparse.Namespace,
5857
unknown_args: List[str],
59-
repo: Optional[Repo] = None,
6058
):
6159
self.apply_args(conf, configurator_args, unknown_args)
6260
profile = load_profile(Path.cwd(), None)

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import argparse
22
import time
3-
from typing import List, Optional
3+
from typing import List
44

55
from rich.table import Table
66

@@ -21,7 +21,6 @@
2121
GatewaySpec,
2222
GatewayStatus,
2323
)
24-
from dstack._internal.core.models.repos.base import Repo
2524
from dstack._internal.core.services.diff import diff_models
2625
from dstack._internal.utils.common import local_time
2726
from dstack.api._public import Client
@@ -37,7 +36,6 @@ def apply_configuration(
3736
command_args: argparse.Namespace,
3837
configurator_args: argparse.Namespace,
3938
unknown_args: List[str],
40-
repo: Optional[Repo] = None,
4139
):
4240
self.apply_args(conf, configurator_args, unknown_args)
4341
spec = GatewaySpec(

0 commit comments

Comments
 (0)