Skip to content

Commit e44751e

Browse files
authored
Deprecate local repos (#2984)
Closes: #2974
1 parent b75bb5c commit e44751e

File tree

12 files changed

+150
-73
lines changed

12 files changed

+150
-73
lines changed

docs/docs/concepts/repos.md

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ This allows accessing the directory files from within the run.
77
## Initialize a repo
88

99
To use a directory with `dstack apply`, it must first be initialized as a repo by running [`dstack init`](../reference/cli/dstack/init.md).
10-
The directory can be either a regular local directory or a cloned Git repo.
10+
The directory must be a cloned Git repo.
1111

1212
[`dstack init`](../reference/cli/dstack/init.md) is not required if you pass `-P` (or `--repo`) to [`dstack apply`](../reference/cli/dstack/apply.md) (see below).
1313

1414
### Git credentials
1515

16-
If the directory is a cloned Git repo, [`dstack init`](../reference/cli/dstack/init.md) grants the `dstack` server access by uploading the current user's default
16+
[`dstack init`](../reference/cli/dstack/init.md) grants the `dstack` server access by uploading the current user's default
1717
Git credentials, ensuring that dstack can clone the Git repo when running the container.
1818

1919
To use custom credentials, pass them directly with `--token` (GitHub token) or `--git-identity` (path to a private SSH
@@ -24,21 +24,19 @@ key).
2424
2525
### .gitignore and folder size
2626

27-
If the directory is cloned Git repo, [`dstack apply`](../reference/cli/dstack/apply.md) uploads to the `dstack` server only local changes.
28-
If the directory is not a cloned Git repo, it uploads the entire directory.
27+
[`dstack apply`](../reference/cli/dstack/apply.md) uploads to the `dstack` server only local changes.
2928

3029
Uploads are limited to 2MB. Use `.gitignore` to exclude unnecessary files from being uploaded.
3130
You can set the `DSTACK_SERVER_CODE_UPLOAD_LIMIT` environment variable to increase the default server limit.
3231
Increasing the limit is recommended only if you [configure an object storage](../guides/server-deployment.md).
3332

34-
### Initialize as a local directory
33+
### Use a local directory instead of a Git repo
3534

36-
If the directory is a cloned Git repo but you want to initialize it as a regular local directory,
37-
use `--local` with [`dstack init`](../reference/cli/dstack/init.md).
35+
If the directory is not a cloned Git repo, use [`files`](../reference/dstack.yml/task.md#_files).
3836

3937
## Specify the repo
4038

41-
By default, `dstack apply` uses the current directory as a repo and requires `dstack init`.
39+
By default, `dstack apply` uses the current directory as a repo if it is already initialized.
4240
You can change this by explicitly specifying the repo to use for `dstack apply`.
4341

4442
### Pass the repo path
@@ -47,8 +45,8 @@ To use a specific directory as the repo, specify its path using `-P` (or `--repo
4745

4846
<div class="termy">
4947

50-
```shell
51-
$ dstack apply -f .dstack.yml -P ../parent_dir
48+
```shell
49+
$ dstack apply -f .dstack.yml -P ../parent_dir
5250
```
5351

5452
</div>
@@ -73,7 +71,7 @@ If you use a private Git repo, you can pass Git credentials to `dstack apply` us
7371

7472
### Do not use a repo
7573

76-
To run a configuration without a repo (the `/workflow` directory inside the container will be empty), use `--no-repo`:
74+
To run a configuration without a repo (the `/workflow` directory inside the container will be empty) if it is already initialized, use `--no-repo`:
7775

7876
<div class="termy">
7977

docs/docs/reference/cli/dstack/apply.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
This command applies a given configuration. If a resource does not exist, `dstack apply` creates the resource.
44
If a resource exists, `dstack apply` updates the resource in-place or re-creates the resource if the update is not possible.
55

6-
When applying run configurations, `dstack apply` requires that you run `dstack init` first,
6+
To mount a Git repo to the run's container, `dstack apply` requires that you run `dstack init` first,
77
or specify a repo to work with via `-P` (or `--repo`), or specify `--no-repo` if you don't need any repo for the run.
88

99
## Usage
@@ -17,4 +17,9 @@ $ dstack apply --help
1717

1818
</div>
1919

20+
## User SSH key
21+
22+
By default, `dstack` uses its own SSH key to attach to runs (`~/.dstack/ssh/id_rsa`).
23+
It is possible to override this key via the `--ssh-identity` argument.
24+
2025
[//]: # (TODO: Provide examples)

docs/docs/reference/cli/dstack/attach.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,9 @@ $ dstack attach --help
1313

1414
</div>
1515

16+
## User SSH key
17+
18+
By default, `dstack` uses its own SSH key to attach to runs (`~/.dstack/ssh/id_rsa`).
19+
It is possible to override this key via the `--ssh-identity` argument.
20+
1621
[//]: # (TODO: Provide examples)
Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
# dstack init
22

33
This command initializes the current directory as a `dstack` [repo](../../../concepts/repos.md).
4+
The directory must be a cloned Git repository.
45

56
**Git credentials**
67

7-
If the directory is a cloned Git repository, `dstack init` ensures that `dstack` can access it.
8-
By default, the command uses the user's default Git credentials. These can be overridden with
8+
`dstack init` ensures that `dstack` can access a remote Git repository.
9+
By default, the command uses the user's default Git credentials. These can be overridden with
910
`--git-identity` (private SSH key) or `--token` (OAuth token).
1011

1112
<div class="termy">
@@ -16,10 +17,3 @@ $ dstack init --help
1617
```
1718

1819
</div>
19-
20-
**User SSH key**
21-
22-
By default, `dstack` uses its own SSH key to access instances (`~/.dstack/ssh/id_rsa`).
23-
It is possible to override this key via the `--ssh-identity` argument.
24-
25-
[//]: # (TODO: Mention that it's optional, provide reference to `dstack apply`)

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

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

34
from argcomplete import FilesCompleter
45

@@ -13,7 +14,7 @@
1314
init_repo,
1415
register_init_repo_args,
1516
)
16-
from dstack._internal.cli.utils.common import console
17+
from dstack._internal.cli.utils.common import console, warn
1718
from dstack._internal.core.errors import CLIError
1819
from dstack._internal.core.models.configurations import ApplyConfigurationType
1920

@@ -65,6 +66,13 @@ def _register(self):
6566
help="Exit immediately after submitting configuration",
6667
action="store_true",
6768
)
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+
)
6876
repo_group = self._parser.add_argument_group("Repo Options")
6977
repo_group.add_argument(
7078
"-P",
@@ -111,6 +119,11 @@ def _command(self, args: argparse.Namespace):
111119
raise CLIError("Cannot read configuration from stdin if -y/--yes is not specified")
112120
if args.repo and args.no_repo:
113121
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+
)
114127
repo = None
115128
if args.repo:
116129
repo = init_repo(
@@ -121,7 +134,6 @@ def _command(self, args: argparse.Namespace):
121134
local=args.local,
122135
git_identity_file=args.git_identity_file,
123136
oauth_token=args.gh_token,
124-
ssh_identity_file=args.ssh_identity_file,
125137
)
126138
elif args.no_repo:
127139
repo = init_default_virtual_repo(api=self.api)

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

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44

55
from dstack._internal.cli.commands import BaseCommand
66
from dstack._internal.cli.services.repos import init_repo, register_init_repo_args
7-
from dstack._internal.cli.utils.common import configure_logging, console
7+
from dstack._internal.cli.utils.common import configure_logging, confirm_ask, console, warn
8+
from dstack._internal.core.errors import ConfigurationError
9+
from dstack._internal.core.models.repos.base import RepoType
10+
from dstack._internal.core.services.configs import ConfigManager
811
from dstack.api import Client
912

1013

@@ -19,12 +22,55 @@ def _register(self):
1922
default=os.getenv("DSTACK_PROJECT"),
2023
)
2124
register_init_repo_args(self._parser)
25+
# Deprecated since 0.19.25, ignored
26+
self._parser.add_argument(
27+
"--ssh-identity",
28+
metavar="SSH_PRIVATE_KEY",
29+
help=argparse.SUPPRESS,
30+
type=Path,
31+
dest="ssh_identity_file",
32+
)
33+
# A hidden mode for transitional period only, remove it with local repos
34+
self._parser.add_argument(
35+
"--remove",
36+
help=argparse.SUPPRESS,
37+
action="store_true",
38+
)
2239

2340
def _command(self, args: argparse.Namespace):
2441
configure_logging()
42+
if args.remove:
43+
config_manager = ConfigManager()
44+
repo_path = Path.cwd()
45+
repo_config = config_manager.get_repo_config(repo_path)
46+
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")
50+
console.print(
51+
f"You are about to remove the local repo {repo_path}\n"
52+
"Only the record about the repo will be removed,"
53+
" the repo files will remain intact\n"
54+
)
55+
if not confirm_ask("Remove the local repo?"):
56+
return
57+
config_manager.delete_repo_config(repo_config.repo_id)
58+
config_manager.save()
59+
console.print("Local repo has been removed")
60+
return
2561
api = Client.from_config(
2662
project_name=args.project, ssh_identity_file=args.ssh_identity_file
2763
)
64+
if args.local:
65+
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"
68+
)
69+
if args.ssh_identity_file:
70+
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"
73+
)
2874
init_repo(
2975
api=api,
3076
repo_path=Path.cwd(),
@@ -33,6 +79,5 @@ def _command(self, args: argparse.Namespace):
3379
local=args.local,
3480
git_identity_file=args.git_identity_file,
3581
oauth_token=args.gh_token,
36-
ssh_identity_file=args.ssh_identity_file,
3782
)
3883
console.print("OK")

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

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@
1515
BaseApplyConfigurator,
1616
)
1717
from dstack._internal.cli.services.profile import apply_profile_args, register_profile_args
18+
from dstack._internal.cli.services.repos import init_default_virtual_repo
1819
from dstack._internal.cli.utils.common import (
1920
confirm_ask,
2021
console,
22+
warn,
2123
)
2224
from dstack._internal.cli.utils.rich import MultiItemStatus
2325
from dstack._internal.cli.utils.run import get_runs_table, print_run_plan
@@ -40,6 +42,7 @@
4042
TaskConfiguration,
4143
)
4244
from dstack._internal.core.models.repos.base import Repo
45+
from dstack._internal.core.models.repos.local import LocalRepo
4346
from dstack._internal.core.models.resources import CPUSpec
4447
from dstack._internal.core.models.runs import JobStatus, JobSubmission, RunSpec, RunStatus
4548
from dstack._internal.core.services.configs import ConfigManager
@@ -76,17 +79,42 @@ def apply_configuration(
7679
self.apply_args(conf, configurator_args, unknown_args)
7780
self.validate_gpu_vendor_and_image(conf)
7881
self.validate_cpu_arch_and_image(conf)
79-
if repo is None:
80-
repo = self.api.repos.load(Path.cwd())
8182
config_manager = ConfigManager()
82-
if repo.repo_dir is not None:
83-
repo_config = config_manager.get_repo_config_or_error(repo.repo_dir)
84-
self.api.ssh_identity_file = repo_config.ssh_key_path
85-
else:
86-
self.api.ssh_identity_file = get_ssh_keypair(
87-
command_args.ssh_identity_file,
88-
config_manager.dstack_key_path,
89-
)
83+
if repo is None:
84+
repo_path = Path.cwd()
85+
repo_config = config_manager.get_repo_config(repo_path)
86+
if repo_config is None:
87+
warn(
88+
"The repo is not initialized. Starting from 0.19.25, repos are optional\n"
89+
"There are three options:\n"
90+
" - Run `dstack init` to initialize the current directory as a repo\n"
91+
" - Specify `--repo`\n"
92+
" - Specify `--no-repo` to not use any repo and supress this warning"
93+
" (this will be the default in the future versions)"
94+
)
95+
if not command_args.yes and not confirm_ask("Continue without the repo?"):
96+
console.print("\nExiting...")
97+
return
98+
repo = init_default_virtual_repo(self.api)
99+
else:
100+
# Unlikely, but may raise ConfigurationError if the repo does not exist
101+
# on the server side (stale entry in `config.yml`)
102+
repo = self.api.repos.load(repo_path)
103+
if isinstance(repo, LocalRepo):
104+
warn(
105+
f"{repo.repo_dir} is a local repo.\n"
106+
"Local repos are deprecated since 0.19.25"
107+
" and will be removed soon\n"
108+
"There are two options:\n"
109+
" - Migrate to `files`: https://dstack.ai/docs/concepts/tasks/#files\n"
110+
" - Specify `--no-repo` if you don't need the repo at all\n"
111+
"In either case, you can run `dstack init --remove` to remove the repo"
112+
" (only the record about the repo, not its files) and this warning"
113+
)
114+
self.api.ssh_identity_file = get_ssh_keypair(
115+
command_args.ssh_identity_file,
116+
config_manager.dstack_key_path,
117+
)
90118
profile = load_profile(Path.cwd(), configurator_args.profile)
91119
with console.status("Getting apply plan..."):
92120
run_plan = self.api.runs.get_run_plan(

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

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1+
import argparse
12
from pathlib import Path
23
from typing import Optional
34

45
from dstack._internal.cli.services.configurators.base import ArgsParser
56
from dstack._internal.core.errors import CLIError
6-
from dstack._internal.core.models.repos.base import Repo, RepoType
7+
from dstack._internal.core.models.repos.base import Repo
78
from dstack._internal.core.models.repos.remote import GitRepoURL, RemoteRepo, RepoError
89
from dstack._internal.core.models.repos.virtual import VirtualRepo
9-
from dstack._internal.core.services.configs import ConfigManager
1010
from dstack._internal.core.services.repos import get_default_branch
1111
from dstack._internal.utils.path import PathLike
1212
from dstack.api._public import Client
@@ -28,49 +28,31 @@ def register_init_repo_args(parser: ArgsParser):
2828
type=str,
2929
dest="git_identity_file",
3030
)
31-
parser.add_argument(
32-
"--ssh-identity",
33-
metavar="SSH_PRIVATE_KEY",
34-
help="The private SSH key path for SSH tunneling",
35-
type=Path,
36-
dest="ssh_identity_file",
37-
)
31+
# Deprecated since 0.19.25
3832
parser.add_argument(
3933
"--local",
4034
action="store_true",
41-
help="Do not use Git",
35+
help=argparse.SUPPRESS,
4236
)
4337

4438

4539
def init_repo(
4640
api: Client,
47-
repo_path: Optional[PathLike],
41+
repo_path: PathLike,
4842
repo_branch: Optional[str],
4943
repo_hash: Optional[str],
5044
local: bool,
5145
git_identity_file: Optional[PathLike],
5246
oauth_token: Optional[str],
53-
ssh_identity_file: Optional[PathLike],
5447
) -> Repo:
55-
init = True
56-
if repo_path is None:
57-
init = False
58-
repo_path = Path.cwd()
5948
if Path(repo_path).exists():
6049
repo = api.repos.load(
6150
repo_dir=repo_path,
6251
local=local,
63-
init=init,
52+
init=True,
6453
git_identity_file=git_identity_file,
6554
oauth_token=oauth_token,
6655
)
67-
if ssh_identity_file:
68-
ConfigManager().save_repo_config(
69-
repo_path=repo.get_repo_dir_or_error(),
70-
repo_id=repo.repo_id,
71-
repo_type=RepoType(repo.run_repo_data.repo_type),
72-
ssh_key_path=ssh_identity_file,
73-
)
7456
elif isinstance(repo_path, str):
7557
try:
7658
GitRepoURL.parse(repo_path)

src/dstack/_internal/cli/utils/common.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,10 @@ def add_row_from_dict(table: Table, data: Dict[Union[str, int], Any], **kwargs):
103103
else:
104104
row.append("")
105105
table.add_row(*row, **kwargs)
106+
107+
108+
def warn(message: str):
109+
if not message.endswith("\n"):
110+
# Additional blank line for better visibility if there are more than one warning
111+
message = f"{message}\n"
112+
console.print(f"[warning][bold]{message}[/]")

0 commit comments

Comments
 (0)