From b754cd2873e811a41c675bba9f8c643d118be5c9 Mon Sep 17 00:00:00 2001 From: Victor Skvortsov Date: Fri, 5 Dec 2025 15:39:57 +0500 Subject: [PATCH 1/5] Drop dstack config --- docs/docs/guides/dstack-sky.md | 2 +- docs/docs/reference/cli/dstack/config.md | 25 ------ src/dstack/_internal/cli/commands/config.py | 89 --------------------- src/dstack/_internal/cli/main.py | 2 - 4 files changed, 1 insertion(+), 117 deletions(-) delete mode 100644 docs/docs/reference/cli/dstack/config.md delete mode 100644 src/dstack/_internal/cli/commands/config.py diff --git a/docs/docs/guides/dstack-sky.md b/docs/docs/guides/dstack-sky.md index a51054f70b..e12f1ccef3 100644 --- a/docs/docs/guides/dstack-sky.md +++ b/docs/docs/guides/dstack-sky.md @@ -5,7 +5,7 @@ sign up with [dstack Sky](../guides/dstack-sky.md). ### Set up the CLI -If you've signed up, open your project settings, and copy the `dstack config` command to point the CLI to the project. +If you've signed up, open your project settings, and copy the `dstack project add` command to point the CLI to the project. ![](https://raw.githubusercontent.com/dstackai/static-assets/main/static-assets/images/dstack-sky-project-config.png){ width=800 } diff --git a/docs/docs/reference/cli/dstack/config.md b/docs/docs/reference/cli/dstack/config.md deleted file mode 100644 index 1dad45730c..0000000000 --- a/docs/docs/reference/cli/dstack/config.md +++ /dev/null @@ -1,25 +0,0 @@ -# dstack config - -!!! info "Deprecated" - The `dstack config` is deprecated. Use [`dstack project`](project.md) instead. - -Both the CLI and API need to be configured with the server address, user token, and project name -via `~/.dstack/config.yml`. - -At startup, the server automatically configures CLI and API with the server address, user token, and -the default project name (`main`). This configuration is stored via `~/.dstack/config.yml`. - -To use CLI and API on different machines or projects, use the `dstack config` command. - -## Usage - -
- -```shell -$ dstack config --help -#GENERATE# -``` - -
- -[//]: # (TODO: Provide examples) diff --git a/src/dstack/_internal/cli/commands/config.py b/src/dstack/_internal/cli/commands/config.py deleted file mode 100644 index adff5c8709..0000000000 --- a/src/dstack/_internal/cli/commands/config.py +++ /dev/null @@ -1,89 +0,0 @@ -import argparse - -from requests import HTTPError - -import dstack.api.server -from dstack._internal.cli.commands import BaseCommand -from dstack._internal.cli.utils.common import confirm_ask, console -from dstack._internal.core.errors import CLIError -from dstack._internal.core.services.configs import ConfigManager -from dstack._internal.utils.logging import get_logger - -logger = get_logger(__name__) - - -class ConfigCommand(BaseCommand): - NAME = "config" - DESCRIPTION = "Configure CLI (deprecated; use `dstack project`)" - - def _register(self): - super()._register() - self._parser.add_argument( - "--project", type=str, help="The name of the project to configure" - ) - self._parser.add_argument("--url", type=str, help="Server url") - self._parser.add_argument("--token", type=str, help="User token") - self._parser.add_argument( - "-y", - "--yes", - help="Don't ask for confirmation (e.g. update the config)", - action="store_true", - ) - self._parser.add_argument( - "--remove", action="store_true", help="Delete project configuration" - ) - self._parser.add_argument( - "-n", - "--no", - help="Don't ask for confirmation (e.g. do not update the config)", - action="store_true", - ) - - def _command(self, args: argparse.Namespace): - super()._command(args) - config_manager = ConfigManager() - if args.remove: - config_manager.delete_project(args.project) - config_manager.save() - console.print("[grey58]OK[/]") - return - - if not args.url: - console.print("Specify --url") - exit(1) - elif not args.token: - console.print("Specify --token") - exit(1) - api_client = dstack.api.server.APIClient(base_url=args.url, token=args.token) - try: - api_client.projects.get(args.project) - except HTTPError as e: - if e.response.status_code == 403: - raise CLIError("Forbidden. Ensure the token is valid.") - elif e.response.status_code == 404: - raise CLIError(f"Project '{args.project}' not found.") - else: - raise e - default_project = config_manager.get_project_config() - if ( - default_project is None - or default_project.name != args.project - or default_project.url != args.url - or default_project.token != args.token - ): - set_it_as_default = ( - ( - args.yes - or not default_project - or confirm_ask(f"Set '{args.project}' as your default project?") - ) - if not args.no - else False - ) - config_manager.configure_project( - name=args.project, url=args.url, token=args.token, default=set_it_as_default - ) - config_manager.save() - logger.info( - f"Configuration updated at {config_manager.config_filepath}", {"show_path": False} - ) diff --git a/src/dstack/_internal/cli/main.py b/src/dstack/_internal/cli/main.py index 2ef2979051..4a643b5612 100644 --- a/src/dstack/_internal/cli/main.py +++ b/src/dstack/_internal/cli/main.py @@ -7,7 +7,6 @@ from dstack._internal.cli.commands.apply import ApplyCommand from dstack._internal.cli.commands.attach import AttachCommand from dstack._internal.cli.commands.completion import CompletionCommand -from dstack._internal.cli.commands.config import ConfigCommand from dstack._internal.cli.commands.delete import DeleteCommand from dstack._internal.cli.commands.fleet import FleetCommand from dstack._internal.cli.commands.gateway import GatewayCommand @@ -63,7 +62,6 @@ def main(): subparsers = parser.add_subparsers(metavar="COMMAND") ApplyCommand.register(subparsers) AttachCommand.register(subparsers) - ConfigCommand.register(subparsers) DeleteCommand.register(subparsers) FleetCommand.register(subparsers) GatewayCommand.register(subparsers) From 1b1f192e8d8fddd9c6ad26fbbe2325dac44a2b57 Mon Sep 17 00:00:00 2001 From: Victor Skvortsov Date: Fri, 5 Dec 2025 15:43:25 +0500 Subject: [PATCH 2/5] Drop dstack stats --- src/dstack/_internal/cli/commands/stats.py | 14 -------------- src/dstack/_internal/cli/main.py | 2 -- 2 files changed, 16 deletions(-) delete mode 100644 src/dstack/_internal/cli/commands/stats.py diff --git a/src/dstack/_internal/cli/commands/stats.py b/src/dstack/_internal/cli/commands/stats.py deleted file mode 100644 index 66acae7d04..0000000000 --- a/src/dstack/_internal/cli/commands/stats.py +++ /dev/null @@ -1,14 +0,0 @@ -import argparse - -from dstack._internal.cli.commands.metrics import MetricsCommand -from dstack._internal.utils.logging import get_logger - -logger = get_logger(__name__) - - -class StatsCommand(MetricsCommand): - NAME = "stats" - - def _command(self, args: argparse.Namespace): - logger.warning("`dstack stats` is deprecated in favor of `dstack metrics`") - super()._command(args) diff --git a/src/dstack/_internal/cli/main.py b/src/dstack/_internal/cli/main.py index 4a643b5612..55248f3cf0 100644 --- a/src/dstack/_internal/cli/main.py +++ b/src/dstack/_internal/cli/main.py @@ -18,7 +18,6 @@ from dstack._internal.cli.commands.ps import PsCommand from dstack._internal.cli.commands.secrets import SecretCommand from dstack._internal.cli.commands.server import ServerCommand -from dstack._internal.cli.commands.stats import StatsCommand from dstack._internal.cli.commands.stop import StopCommand from dstack._internal.cli.commands.volume import VolumeCommand from dstack._internal.cli.utils.common import _colors, console @@ -73,7 +72,6 @@ def main(): PsCommand.register(subparsers) SecretCommand.register(subparsers) ServerCommand.register(subparsers) - StatsCommand.register(subparsers) StopCommand.register(subparsers) VolumeCommand.register(subparsers) CompletionCommand.register(subparsers) From 77fd669d6fab3d8f175759b7687abe0b5d72647a Mon Sep 17 00:00:00 2001 From: Victor Skvortsov Date: Fri, 5 Dec 2025 15:44:54 +0500 Subject: [PATCH 3/5] Drop dstack gateway create --- src/dstack/_internal/cli/commands/gateway.py | 40 -------------------- 1 file changed, 40 deletions(-) diff --git a/src/dstack/_internal/cli/commands/gateway.py b/src/dstack/_internal/cli/commands/gateway.py index 384feaba63..31ecef3ddf 100644 --- a/src/dstack/_internal/cli/commands/gateway.py +++ b/src/dstack/_internal/cli/commands/gateway.py @@ -17,8 +17,6 @@ print_gateways_table, ) from dstack._internal.core.errors import CLIError -from dstack._internal.core.models.backends.base import BackendType -from dstack._internal.core.models.gateways import GatewayConfiguration from dstack._internal.utils.logging import get_logger logger = get_logger(__name__) @@ -62,24 +60,6 @@ def _register(self): help="Output in JSON format (equivalent to --format json)", ) - create_parser = subparsers.add_parser( - "create", - help="Add a gateway. Deprecated in favor of `dstack apply` with gateway configuration.", - formatter_class=self._parser.formatter_class, - ) - create_parser.set_defaults(subfunc=self._create) - create_parser.add_argument( - "--backend", choices=["aws", "azure", "gcp", "kubernetes"], required=True - ) - create_parser.add_argument("--region", required=True) - create_parser.add_argument( - "--set-default", action="store_true", help="Set as default gateway for the project" - ) - create_parser.add_argument("--name", help="Set a custom name for the gateway") - create_parser.add_argument( - "--domain", help="Set the domain for the gateway", required=True - ) - delete_parser = subparsers.add_parser( "delete", help="Delete a gateway", formatter_class=self._parser.formatter_class ) @@ -129,26 +109,6 @@ def _list(self, args: argparse.Namespace): except KeyboardInterrupt: pass - def _create(self, args: argparse.Namespace): - logger.warning( - "`dstack gateway create` is deperecated in favor of `dstack apply` with gateway configurations." - ) - with console.status("Creating gateway..."): - configuration = GatewayConfiguration( - name=args.name, - backend=BackendType(args.backend), - region=args.region, - ) - gateway = self.api.client.gateways.create(self.api.project, configuration) - if args.set_default: - self.api.client.gateways.set_default(self.api.project, gateway.name) - if args.domain: - self.api.client.gateways.set_wildcard_domain( - self.api.project, gateway.name, args.domain - ) - gateway = self.api.client.gateways.get(self.api.project, gateway.name) - print_gateways_table([gateway]) - def _delete(self, args: argparse.Namespace): gateway = self.api.client.gateways.get(self.api.project, args.name) print_gateways_table([gateway]) From 3ba029a7913da0592b7423cfe17e6184d21fc5c5 Mon Sep 17 00:00:00 2001 From: Victor Skvortsov Date: Fri, 5 Dec 2025 15:46:04 +0500 Subject: [PATCH 4/5] Drop ProfileRetryPolicy --- src/dstack/_internal/core/models/profiles.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/src/dstack/_internal/core/models/profiles.py b/src/dstack/_internal/core/models/profiles.py index 896049ffa6..f69f094028 100644 --- a/src/dstack/_internal/core/models/profiles.py +++ b/src/dstack/_internal/core/models/profiles.py @@ -98,26 +98,6 @@ def parse_idle_duration(v: Optional[Union[int, str, bool]]) -> Optional[int]: return parse_duration(v) -# Deprecated in favor of ProfileRetry(). -# TODO: Remove when no longer referenced. -class ProfileRetryPolicy(CoreModel): - retry: Annotated[bool, Field(description="Whether to retry the run on failure or not")] = False - duration: Annotated[ - Optional[Union[int, str]], - Field(description="The maximum period of retrying the run, e.g., `4h` or `1d`"), - ] = None - - _validate_duration = validator("duration", pre=True, allow_reuse=True)(parse_duration) - - @root_validator - def _validate_fields(cls, values): - if values["retry"] and "duration" not in values: - values["duration"] = DEFAULT_RETRY_DURATION - if values.get("duration") is not None: - values["retry"] = True - return values - - class RetryEvent(str, Enum): NO_CAPACITY = "no-capacity" INTERRUPTION = "interruption" From b09c67bb25f871362ed00fae4fd08f1dcf6de344 Mon Sep 17 00:00:00 2001 From: Victor Skvortsov Date: Fri, 5 Dec 2025 16:14:42 +0500 Subject: [PATCH 5/5] Fix tests --- .../_internal/cli/commands/test_config.py | 48 ------------------- .../_internal/cli/commands/test_project.py | 42 ++++++++++++++++ 2 files changed, 42 insertions(+), 48 deletions(-) delete mode 100644 src/tests/_internal/cli/commands/test_config.py create mode 100644 src/tests/_internal/cli/commands/test_project.py diff --git a/src/tests/_internal/cli/commands/test_config.py b/src/tests/_internal/cli/commands/test_config.py deleted file mode 100644 index 430e51f8f5..0000000000 --- a/src/tests/_internal/cli/commands/test_config.py +++ /dev/null @@ -1,48 +0,0 @@ -from pathlib import Path -from unittest.mock import patch - -import yaml -from pytest import CaptureFixture - -from dstack._internal.utils.logging import get_logger -from tests._internal.cli.common import run_dstack_cli - - -class TestConfig: - def test_configures_project(self, capsys: CaptureFixture, tmp_path: Path): - cli_config_path = tmp_path / ".dstack" / "config.yml" - logger = get_logger("dstack._internal.cli.commands.config") - with patch.object(logger, "info") as logger_info_mock: - with patch("dstack.api.server.APIClient") as APIClientMock: - api_client_mock = APIClientMock.return_value - api_client_mock.projects.get - exit_code = run_dstack_cli( - [ - "config", - "--url", - "http://127.0.0.1:31313", - "--project", - "project", - "--token", - "token", - ], - home_dir=tmp_path, - ) - APIClientMock.assert_called_once_with( - base_url="http://127.0.0.1:31313", token="token" - ) - logger_info_mock.assert_called_once_with( - f"Configuration updated at {cli_config_path}", {"show_path": False} - ) - assert exit_code == 0 - assert yaml.load(cli_config_path.read_text(), yaml.FullLoader) == { - "projects": [ - { - "default": True, - "name": "project", - "token": "token", - "url": "http://127.0.0.1:31313", - } - ], - "repos": [], - } diff --git a/src/tests/_internal/cli/commands/test_project.py b/src/tests/_internal/cli/commands/test_project.py new file mode 100644 index 0000000000..1572e25740 --- /dev/null +++ b/src/tests/_internal/cli/commands/test_project.py @@ -0,0 +1,42 @@ +from pathlib import Path +from unittest.mock import patch + +import yaml +from pytest import CaptureFixture + +from tests._internal.cli.common import run_dstack_cli + + +class TestProjectAdd: + def test_adds_project(self, capsys: CaptureFixture, tmp_path: Path): + cli_config_path = tmp_path / ".dstack" / "config.yml" + with patch("dstack.api.server.APIClient") as APIClientMock: + api_client_mock = APIClientMock.return_value + exit_code = run_dstack_cli( + [ + "project", + "add", + "--name", + "project", + "--url", + "http://127.0.0.1:31313", + "--token", + "token", + "-y", + ], + home_dir=tmp_path, + ) + APIClientMock.assert_called_once_with(base_url="http://127.0.0.1:31313", token="token") + api_client_mock.projects.get.assert_called_with("project") + assert exit_code == 0 + assert yaml.load(cli_config_path.read_text(), yaml.FullLoader) == { + "projects": [ + { + "default": True, + "name": "project", + "token": "token", + "url": "http://127.0.0.1:31313", + } + ], + "repos": [], + }