diff --git a/src/dstack/_internal/cli/commands/endpoint.py b/src/dstack/_internal/cli/commands/endpoint.py index 0bf75b42b..be609af5f 100644 --- a/src/dstack/_internal/cli/commands/endpoint.py +++ b/src/dstack/_internal/cli/commands/endpoint.py @@ -15,13 +15,16 @@ EndpointPresetStore, load_endpoint_configuration, ) -from dstack._internal.cli.services.profile import apply_profile_args, register_profile_args +from dstack._internal.cli.services.profile import ( + apply_profile_args, + load_profile_from_args, + register_profile_args, +) from dstack._internal.cli.utils.common import confirm_ask, console from dstack._internal.core.errors import CLIError from dstack._internal.core.models.profiles import ProfileParams from dstack._internal.core.services import is_valid_dstack_resource_name from dstack.api import Client -from dstack.api.utils import load_profile class EndpointCommand(BaseCommand): @@ -255,7 +258,7 @@ def _get_effective_configuration( args: argparse.Namespace, ) -> EndpointConfiguration: _apply_name(configuration, args.name) - profile = load_profile(Path.cwd(), args.profile) + profile = load_profile_from_args(args=args, repo_dir=Path.cwd()) for field in ProfileParams.__fields__: if getattr(configuration, field) is None: setattr(configuration, field, getattr(profile, field)) diff --git a/src/dstack/_internal/cli/commands/offer.py b/src/dstack/_internal/cli/commands/offer.py index 92157ad9b..9ca84ed71 100644 --- a/src/dstack/_internal/cli/commands/offer.py +++ b/src/dstack/_internal/cli/commands/offer.py @@ -6,7 +6,7 @@ from dstack._internal.cli.services.configurators.run import ( BaseRunConfigurator, ) -from dstack._internal.cli.services.profile import register_profile_args +from dstack._internal.cli.services.profile import load_profile_from_args, register_profile_args from dstack._internal.cli.services.resources import register_resources_args from dstack._internal.cli.utils.common import console from dstack._internal.cli.utils.gpu import print_gpu_json, print_gpu_table @@ -15,7 +15,6 @@ from dstack._internal.core.models.configurations import ApplyConfigurationType, TaskConfiguration from dstack._internal.core.models.gpus import GpuGroup from dstack._internal.core.models.runs import RunSpec -from dstack.api.utils import load_profile class OfferConfigurator(BaseRunConfigurator): @@ -81,7 +80,7 @@ def _command(self, args: argparse.Namespace): configurator = OfferConfigurator(api_client=self.api) configurator.apply_args(conf, args) - profile = load_profile(Path.cwd(), profile_name=args.profile) + profile = load_profile_from_args(args=args, repo_dir=Path.cwd()) run_spec = RunSpec( configuration=conf, diff --git a/src/dstack/_internal/cli/services/configurators/run.py b/src/dstack/_internal/cli/services/configurators/run.py index 3541bd19b..f71193bd5 100644 --- a/src/dstack/_internal/cli/services/configurators/run.py +++ b/src/dstack/_internal/cli/services/configurators/run.py @@ -17,7 +17,11 @@ ApplyEnvVarsConfiguratorMixin, BaseApplyConfigurator, ) -from dstack._internal.cli.services.profile import apply_profile_args, register_profile_args +from dstack._internal.cli.services.profile import ( + apply_profile_args, + load_profile_from_args, + register_profile_args, +) from dstack._internal.cli.services.repos import ( get_repo_from_dir, get_repo_from_url, @@ -68,7 +72,6 @@ from dstack._internal.utils.nested_list import NestedList, NestedListItem from dstack._internal.utils.path import is_absolute_posix_path from dstack.api._public.runs import Run -from dstack.api.utils import load_profile _KNOWN_AMD_GPUS = {gpu.name.lower() for gpu in gpuhunt.KNOWN_AMD_GPUS} _KNOWN_NVIDIA_GPUS = {gpu.name.lower() for gpu in gpuhunt.KNOWN_NVIDIA_GPUS} @@ -132,7 +135,7 @@ def get_plan( repo = self.get_repo(conf, configuration_path, configurator_args) if repo is None: repo = init_default_virtual_repo(api=self.api) - profile = load_profile(Path.cwd(), configurator_args.profile) + profile = load_profile_from_args(args=configurator_args, repo_dir=Path.cwd()) with console.status("Getting apply plan..."): run_plan = self.api.runs.get_run_plan( configuration=conf, diff --git a/src/dstack/_internal/cli/services/profile.py b/src/dstack/_internal/cli/services/profile.py index 9c00b55fe..0ef420d77 100644 --- a/src/dstack/_internal/cli/services/profile.py +++ b/src/dstack/_internal/cli/services/profile.py @@ -1,14 +1,22 @@ import argparse import os +from dstack._internal.core.errors import CLIError from dstack._internal.core.models.profiles import ( CreationPolicy, + Profile, ProfileParams, ProfileRetry, SpotPolicy, parse_duration, parse_max_duration, ) +from dstack._internal.utils.env import environ +from dstack._internal.utils.path import PathLike +from dstack.api.utils import load_profile + +_PROFILE_ENV_VAR = "DSTACK_PROFILE" +_NO_PROFILE_ENV_VAR = "DSTACK_NO_PROFILE" def register_profile_args(parser: argparse.ArgumentParser): @@ -17,13 +25,22 @@ def register_profile_args(parser: argparse.ArgumentParser): CLI arguments that override `profiles.yml` settings. """ profile_group = parser.add_argument_group("Profile") - profile_group.add_argument( + profile_exc = profile_group.add_mutually_exclusive_group() + profile_exc.add_argument( "--profile", metavar="NAME", - help="The name of the profile. Defaults to [code]$DSTACK_PROFILE[/]", - default=os.getenv("DSTACK_PROFILE"), + help=f"The name of the profile. Defaults to [code]${_PROFILE_ENV_VAR}[/]", dest="profile", ) + profile_exc.add_argument( + "--no-profile", + help=( + "Don't load any profile." + f" Enabled by default if [code]${_NO_PROFILE_ENV_VAR}[/] is set and [code]--profile[/] is not specified" + ), + action="store_true", + dest="no_profile", + ) profile_group.add_argument( "--max-price", metavar="PRICE", @@ -131,6 +148,21 @@ def register_profile_args(parser: argparse.ArgumentParser): ) +def load_profile_from_args(args: argparse.Namespace, repo_dir: PathLike) -> Profile: + # precedence: --no-profile > --profile=name > DSTACK_NO_PROFILE=1 > DSTACK_PROFILE=name + if args.no_profile: + return _build_dummy_no_profile() + if args.profile is not None: + return load_profile(repo_dir=repo_dir, profile_name=args.profile) + try: + no_profile_from_env = environ.get_bool(_NO_PROFILE_ENV_VAR, default=False) + except ValueError as e: + raise CLIError(str(e)) from e + if no_profile_from_env: + return _build_dummy_no_profile() + return load_profile(repo_dir=repo_dir, profile_name=os.getenv(_PROFILE_ENV_VAR)) + + def apply_profile_args( args: argparse.Namespace, profile_settings: ProfileParams, @@ -178,3 +210,7 @@ def max_duration(v: str) -> int: def retry_duration(v: str) -> int: return parse_duration(v) + + +def _build_dummy_no_profile() -> Profile: + return Profile(name="no-profile")