From 32c4c98b1a1e0b691d8e49370160395a8a672f86 Mon Sep 17 00:00:00 2001 From: Victor Skvortsov Date: Mon, 21 Apr 2025 14:28:09 +0500 Subject: [PATCH 1/2] Support reading apply configurations from stdin --- src/dstack/_internal/cli/commands/apply.py | 13 ++++++++++--- .../cli/services/configurators/__init__.py | 8 ++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/dstack/_internal/cli/commands/apply.py b/src/dstack/_internal/cli/commands/apply.py index d3420441e8..0677de8442 100644 --- a/src/dstack/_internal/cli/commands/apply.py +++ b/src/dstack/_internal/cli/commands/apply.py @@ -1,10 +1,10 @@ import argparse -from pathlib import Path from argcomplete import FilesCompleter from dstack._internal.cli.commands import APIBaseCommand from dstack._internal.cli.services.configurators import ( + APPLY_STDIN_NAME, get_apply_configurator_class, load_apply_configuration, ) @@ -40,9 +40,12 @@ def _register(self): self._parser.add_argument( "-f", "--file", - type=Path, metavar="FILE", - help="The path to the configuration file. Defaults to [code]$PWD/.dstack.yml[/]", + help=( + "The path to the configuration file." + " Specify [code]-[/] to read configuration from stdin." + " Defaults to [code]$PWD/.dstack.yml[/]" + ), dest="configuration_file", ).completer = FilesCompleter(allowednames=["*.yml", "*.yaml"]) self._parser.add_argument( @@ -104,6 +107,10 @@ def _command(self, args: argparse.Namespace): return super()._command(args) + if not args.yes and args.configuration_file == APPLY_STDIN_NAME: + # FIXME: probably does not work since dstack apply can ask questions futher, + # e.g. whether to terminate a resource on ctrl+c or not + raise CLIError("Cannot read configuration from stdin if -y/--yes is not specified") if args.repo and args.no_repo: raise CLIError("Either --repo or --no-repo can be specified") repo = None diff --git a/src/dstack/_internal/cli/services/configurators/__init__.py b/src/dstack/_internal/cli/services/configurators/__init__.py index a5049a8b32..cba23ee31a 100644 --- a/src/dstack/_internal/cli/services/configurators/__init__.py +++ b/src/dstack/_internal/cli/services/configurators/__init__.py @@ -1,3 +1,4 @@ +import sys from pathlib import Path from typing import Dict, Optional, Tuple, Type @@ -20,6 +21,9 @@ parse_apply_configuration, ) +APPLY_STDIN_NAME = "-" + + apply_configurators_mapping: Dict[ApplyConfigurationType, Type[BaseApplyConfigurator]] = { cls.TYPE: cls for cls in [ @@ -62,6 +66,8 @@ def load_apply_configuration( raise ConfigurationError( "No configuration file specified via `-f` and no default .dstack.yml configuration found" ) + elif configuration_file == APPLY_STDIN_NAME: + configuration_path = sys.stdin.fileno() else: configuration_path = Path(configuration_file) if not configuration_path.exists(): @@ -71,4 +77,6 @@ def load_apply_configuration( conf = parse_apply_configuration(yaml.safe_load(f)) except OSError: 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 From ab65e6aeb3f9f3e9a0a6ddadd2e13f3f8b778b60 Mon Sep 17 00:00:00 2001 From: Victor Skvortsov Date: Fri, 1 Aug 2025 11:12:26 +0500 Subject: [PATCH 2/2] Never ask confirm with --yes --- src/dstack/_internal/cli/commands/apply.py | 2 -- src/dstack/_internal/cli/services/configurators/fleet.py | 2 +- src/dstack/_internal/cli/services/configurators/gateway.py | 2 +- src/dstack/_internal/cli/services/configurators/run.py | 4 +++- src/dstack/_internal/cli/services/configurators/volume.py | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/dstack/_internal/cli/commands/apply.py b/src/dstack/_internal/cli/commands/apply.py index 0677de8442..44e957feb7 100644 --- a/src/dstack/_internal/cli/commands/apply.py +++ b/src/dstack/_internal/cli/commands/apply.py @@ -108,8 +108,6 @@ def _command(self, args: argparse.Namespace): super()._command(args) if not args.yes and args.configuration_file == APPLY_STDIN_NAME: - # FIXME: probably does not work since dstack apply can ask questions futher, - # e.g. whether to terminate a resource on ctrl+c or not raise CLIError("Cannot read configuration from stdin if -y/--yes is not specified") if args.repo and args.no_repo: raise CLIError("Either --repo or --no-repo can be specified") diff --git a/src/dstack/_internal/cli/services/configurators/fleet.py b/src/dstack/_internal/cli/services/configurators/fleet.py index 2a7eeb4d59..cb9d7a2b87 100644 --- a/src/dstack/_internal/cli/services/configurators/fleet.py +++ b/src/dstack/_internal/cli/services/configurators/fleet.py @@ -151,7 +151,7 @@ def _apply_plan(self, plan: FleetPlan, command_args: argparse.Namespace): time.sleep(LIVE_TABLE_PROVISION_INTERVAL_SECS) fleet = self.api.client.fleets.get(self.api.project, fleet.name) except KeyboardInterrupt: - if confirm_ask("Delete the fleet before exiting?"): + if not command_args.yes and confirm_ask("Delete the fleet before exiting?"): with console.status("Deleting fleet..."): self.api.client.fleets.delete( project_name=self.api.project, names=[fleet.name] diff --git a/src/dstack/_internal/cli/services/configurators/gateway.py b/src/dstack/_internal/cli/services/configurators/gateway.py index a2411f974f..8651c79ce8 100644 --- a/src/dstack/_internal/cli/services/configurators/gateway.py +++ b/src/dstack/_internal/cli/services/configurators/gateway.py @@ -121,7 +121,7 @@ def apply_configuration( time.sleep(LIVE_TABLE_PROVISION_INTERVAL_SECS) gateway = self.api.client.gateways.get(self.api.project, gateway.name) except KeyboardInterrupt: - if confirm_ask("Delete the gateway before exiting?"): + if not command_args.yes and confirm_ask("Delete the gateway before exiting?"): with console.status("Deleting gateway..."): self.api.client.gateways.delete( project_name=self.api.project, diff --git a/src/dstack/_internal/cli/services/configurators/run.py b/src/dstack/_internal/cli/services/configurators/run.py index 7adc3b90be..4a554a9d72 100644 --- a/src/dstack/_internal/cli/services/configurators/run.py +++ b/src/dstack/_internal/cli/services/configurators/run.py @@ -218,7 +218,9 @@ def apply_configuration( exit(1) except KeyboardInterrupt: try: - if not confirm_ask(f"\nStop the run [code]{run.name}[/] before detaching?"): + if command_args.yes or not confirm_ask( + f"\nStop the run [code]{run.name}[/] before detaching?" + ): console.print("Detached") abort_at_exit = False return diff --git a/src/dstack/_internal/cli/services/configurators/volume.py b/src/dstack/_internal/cli/services/configurators/volume.py index 77e34b7880..2a085477ed 100644 --- a/src/dstack/_internal/cli/services/configurators/volume.py +++ b/src/dstack/_internal/cli/services/configurators/volume.py @@ -110,7 +110,7 @@ def apply_configuration( time.sleep(LIVE_TABLE_PROVISION_INTERVAL_SECS) volume = self.api.client.volumes.get(self.api.project, volume.name) except KeyboardInterrupt: - if confirm_ask("Delete the volume before exiting?"): + if not command_args.yes and confirm_ask("Delete the volume before exiting?"): with console.status("Deleting volume..."): self.api.client.volumes.delete( project_name=self.api.project, names=[volume.name]