Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement dstack apply for gateways #1223

Merged
merged 5 commits into from
May 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ jobs:
run: pip install .
- name: Generate json schema
run: |
python -c "from dstack._internal.core.models.configurations import RunConfiguration; print(RunConfiguration.schema_json(indent=2))" > configuration.json
python -c "from dstack._internal.core.models.configurations import DstackConfiguration; print(DstackConfiguration.schema_json(indent=2))" > configuration.json
python -c "from dstack._internal.core.models.profiles import ProfilesConfig; print(ProfilesConfig.schema_json(indent=2))" > profiles.json
- name: Upload json schema to S3
run: |
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ jobs:
run: pip install .
- name: Generate json schema
run: |
python -c "from dstack._internal.core.models.configurations import RunConfiguration; print(RunConfiguration.schema_json(indent=2))" > configuration.json
python -c "from dstack._internal.core.models.configurations import DstackConfiguration; print(DstackConfiguration.schema_json(indent=2))" > configuration.json
python -c "from dstack._internal.core.models.profiles import ProfilesConfig; print(ProfilesConfig.schema_json(indent=2))" > profiles.json
- name: Upload json schema to S3
run: |
Expand Down
18 changes: 18 additions & 0 deletions docs/docs/reference/cli/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,24 @@ $ dstack run . --help

If there are large files, consider creating a `.gitignore` file to exclude them for better performance.

### dstack apply

This command applies a given configuration. If a resources does not exist, `dstack apply` creates the resource.
If a resource exists, `dstack apply` updates the resource in-place or re-creates the resource if the update is not possible.

<div class="termy">

```shell
$ dstack apply --help
#GENERATE#
```

</div>

!!! info "NOTE:"
The `dstack apply` command currently supports only `gateway` configurations.
Support for other configuration types is coming soon.

### dstack ps

This command shows the status of runs.
Expand Down
7 changes: 4 additions & 3 deletions docs/docs/reference/dstack.yml/dev-environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

The `dev-environment` configuration type allows running [dev environments](../../concepts/dev-environments.md).

> Configuration files must have a name ending with `.dstack.yml` (e.g., `.dstack.yml` or `dev.dstack.yml` are both acceptable)
> and can be located in the project's root directory or any nested folder.
> Any configuration can be run via [`dstack run . -f PATH`](../cli/index.md#dstack-run).
!!! info "Filename"
Configuration files must have a name ending with `.dstack.yml` (e.g., `.dstack.yml` or `serve.dstack.yml` are both acceptable)
and can be located in the project's root directory or any nested folder.
Any configuration can be run via [`dstack run`](../cli/index.md#dstack-run).

## Examples

Expand Down
31 changes: 31 additions & 0 deletions docs/docs/reference/dstack.yml/gateway.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# gateway

The `gateway` configuration type allows creating and updating [gateways](../../concepts/services.md).

!!! info "Filename"
Configuration files must have a name ending with `.dstack.yml` (e.g., `.dstack.yml` or `serve.dstack.yml` are both acceptable)
and can be located in the project's root directory or any nested folder.
Any configuration can be applied via [`dstack apply`](../cli/index.md#dstack-apply).

## Examples

<div editor-title="gateway.dstack.yml">

```yaml
type: gateway
name: example-gateway
backend: aws
region: eu-west-1
domain: '*.example.com'
```

</div>


## Root reference

#SCHEMA# dstack._internal.core.models.gateways.GatewayConfiguration
overrides:
show_root_heading: false
type:
required: true
7 changes: 4 additions & 3 deletions docs/docs/reference/dstack.yml/task.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

The `task` configuration type allows running [tasks](../../concepts/tasks.md).

> Configuration files must have a name ending with `.dstack.yml` (e.g., `.dstack.yml` or `train.dstack.yml` are both acceptable)
> and can be located in the project's root directory or any nested folder.
> Any configuration can be run via [`dstack run . -f PATH`](../cli/index.md#dstack-run).
!!! info "Filename"
Configuration files must have a name ending with `.dstack.yml` (e.g., `.dstack.yml` or `serve.dstack.yml` are both acceptable)
and can be located in the project's root directory or any nested folder.
Any configuration can be run via [`dstack run`](../cli/index.md#dstack-run).

## Examples

Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ nav:
- dev-environment: docs/reference/dstack.yml/dev-environment.md
- task: docs/reference/dstack.yml/task.md
- service: docs/reference/dstack.yml/service.md
- gateway: docs/reference/dstack.yml/gateway.md
- profiles.yml: docs/reference/profiles.yml.md
- CLI: docs/reference/cli/index.md
- server/config.yml: docs/reference/server/config.yml.md
Expand Down
60 changes: 60 additions & 0 deletions src/dstack/_internal/cli/commands/apply.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import argparse
from pathlib import Path

import yaml

from dstack._internal.cli.commands import APIBaseCommand
from dstack._internal.cli.services.configurators import (
get_apply_configurator_class,
)
from dstack._internal.cli.utils.common import cli_error
from dstack._internal.core.errors import ConfigurationError
from dstack._internal.core.models.configurations import (
AnyApplyConfiguration,
parse_apply_configuration,
)


class ApplyCommand(APIBaseCommand):
NAME = "apply"
DESCRIPTION = "Apply dstack configuration"

def _register(self):
super()._register()
self._parser.add_argument(
"configuration_file",
help="The path to the configuration file",
)
self._parser.add_argument(
"--force",
help="Force apply when no changes detected",
action="store_true",
)
self._parser.add_argument(
"-y",
"--yes",
help="Do not ask for confirmation",
action="store_true",
)

def _command(self, args: argparse.Namespace):
super()._command(args)
try:
configuration = _load_configuration(args.configuration_file)
except ConfigurationError as e:
raise cli_error(e)
configurator_class = get_apply_configurator_class(configuration.type)
configurator = configurator_class(api_client=self.api)
configurator.apply_configuration(conf=configuration, args=args)


def _load_configuration(configuration_file: str) -> AnyApplyConfiguration:
configuration_path = Path(configuration_file)
if not configuration_path.exists():
raise ConfigurationError(f"Configuration file {configuration_file} does not exist")
try:
with open(configuration_path, "r") as f:
conf = parse_apply_configuration(yaml.safe_load(f))
except OSError:
raise ConfigurationError(f"Failed to load configuration from {configuration_path}")
return conf
2 changes: 1 addition & 1 deletion src/dstack/_internal/cli/commands/pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

from dstack._internal.cli.commands import APIBaseCommand
from dstack._internal.cli.services.args import cpu_spec, disk_spec, gpu_spec, memory_spec
from dstack._internal.cli.services.configurators.profile import (
from dstack._internal.cli.services.profile import (
apply_profile_args,
register_profile_args,
)
Expand Down
18 changes: 9 additions & 9 deletions src/dstack/_internal/cli/commands/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,18 @@
from typing import Optional

from dstack._internal.cli.commands import APIBaseCommand
from dstack._internal.cli.services.configurators.profile import (
apply_profile_args,
register_profile_args,
)
from dstack._internal.cli.services.configurators.run import (
BaseRunConfigurator,
run_configurators_mapping,
)
from dstack._internal.cli.services.profile import (
apply_profile_args,
register_profile_args,
)
from dstack._internal.cli.utils.common import confirm_ask, console
from dstack._internal.cli.utils.run import print_run_plan
from dstack._internal.core.errors import CLIError, ConfigurationError, ServerClientError
from dstack._internal.core.models.configurations import ConfigurationType
from dstack._internal.core.models.configurations import RunConfigurationType
from dstack._internal.core.models.runs import JobTerminationReason
from dstack._internal.core.services.configs import ConfigManager
from dstack._internal.utils.logging import get_logger
Expand All @@ -39,7 +39,7 @@ def _register(self):
"-h",
"--help",
nargs="?",
type=ConfigurationType,
type=RunConfigurationType,
default=NOTSET,
help="Show this help message and exit. TYPE is one of [code]task[/], [code]dev-environment[/], [code]service[/]",
dest="help",
Expand Down Expand Up @@ -83,7 +83,7 @@ def _register(self):
def _command(self, args: argparse.Namespace):
if args.help is not NOTSET:
if args.help is not None:
run_configurators_mapping[ConfigurationType(args.help)].register(self._parser)
run_configurators_mapping[RunConfigurationType(args.help)].register(self._parser)
else:
BaseRunConfigurator.register(self._parser)
self._parser.print_help()
Expand All @@ -102,7 +102,7 @@ def _command(self, args: argparse.Namespace):
apply_profile_args(args, conf)
logger.debug("Configuration loaded: %s", configuration_path)
parser = argparse.ArgumentParser()
configurator = run_configurators_mapping[ConfigurationType(conf.type)]
configurator = run_configurators_mapping[RunConfigurationType(conf.type)]
configurator.register(parser)
known, unknown = parser.parse_known_args(args.unknown)
configurator.apply(known, unknown, conf)
Expand Down Expand Up @@ -176,7 +176,7 @@ def _command(self, args: argparse.Namespace):
)

if run.status in (RunStatus.RUNNING, RunStatus.DONE):
if run._run.run_spec.configuration.type == ConfigurationType.SERVICE.value:
if run._run.run_spec.configuration.type == RunConfigurationType.SERVICE.value:
console.print(
f"Service is published at [link={run.service_url}]{run.service_url}[/]\n"
)
Expand Down
2 changes: 2 additions & 0 deletions src/dstack/_internal/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from rich.markup import escape
from rich_argparse import RichHelpFormatter

from dstack._internal.cli.commands.apply import ApplyCommand
from dstack._internal.cli.commands.config import ConfigCommand
from dstack._internal.cli.commands.gateway import GatewayCommand
from dstack._internal.cli.commands.init import InitCommand
Expand Down Expand Up @@ -50,6 +51,7 @@ def main():
parser.set_defaults(func=lambda _: parser.print_help())

subparsers = parser.add_subparsers(metavar="COMMAND")
ApplyCommand.register(subparsers)
ConfigCommand.register(subparsers)
GatewayCommand.register(subparsers)
PoolCommand.register(subparsers)
Expand Down
13 changes: 13 additions & 0 deletions src/dstack/_internal/cli/services/configurators/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from typing import Dict, Type

from dstack._internal.cli.services.configurators.base import BaseApplyConfigurator
from dstack._internal.cli.services.configurators.gateway import GatewayConfigurator
from dstack._internal.core.models.configurations import ApplyConfigurationType

apply_configurators_mapping: Dict[ApplyConfigurationType, Type[BaseApplyConfigurator]] = {
cls.TYPE: cls for cls in [GatewayConfigurator]
}


def get_apply_configurator_class(configurator_type: str) -> Type[BaseApplyConfigurator]:
return apply_configurators_mapping[ApplyConfigurationType(configurator_type)]
28 changes: 28 additions & 0 deletions src/dstack/_internal/cli/services/configurators/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import argparse
from abc import ABC, abstractmethod
from typing import List

from dstack._internal.core.models.configurations import (
AnyApplyConfiguration,
ApplyConfigurationType,
)
from dstack.api._public import Client


class BaseApplyConfigurator(ABC):
TYPE: ApplyConfigurationType

def __init__(self, api_client: Client):
self.api_client = api_client

@abstractmethod
def apply_configuration(self, conf: AnyApplyConfiguration, args: argparse.Namespace):
pass

def register_args(self, parser: argparse.ArgumentParser):
pass

def apply_args(
self, args: argparse.Namespace, unknown: List[str], conf: AnyApplyConfiguration
):
pass
58 changes: 58 additions & 0 deletions src/dstack/_internal/cli/services/configurators/gateway.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import argparse

from dstack._internal.cli.services.configurators.base import BaseApplyConfigurator
from dstack._internal.cli.utils.common import confirm_ask, console
from dstack._internal.cli.utils.gateway import print_gateways_table
from dstack._internal.core.errors import ResourceNotExistsError
from dstack._internal.core.models.configurations import ApplyConfigurationType
from dstack._internal.core.models.gateways import GatewayConfiguration


class GatewayConfigurator(BaseApplyConfigurator):
TYPE: ApplyConfigurationType = ApplyConfigurationType.GATEWAY

def apply_configuration(self, conf: GatewayConfiguration, args: argparse.Namespace):
# TODO: Show apply plan
# TODO: Update gateway in-place when domain/default change
confirmed = False
if conf.name is not None:
try:
gateway = self.api_client.client.gateways.get(
project_name=self.api_client.project, gateway_name=conf.name
)
except ResourceNotExistsError:
pass
else:
if gateway.configuration == conf:
if not args.force:
console.print(
"Gateway configuration has not changed. Use --force to recreate the gateway."
)
return
if not args.yes and not confirm_ask(
"Gateway configuration has not changed. Re-create the gateway?"
):
console.print("\nExiting...")
return
elif not args.yes and not confirm_ask(
f"Gateway [code]{conf.name}[/] already exist. Re-create the gateway?"
):
console.print("\nExiting...")
return
confirmed = True
with console.status("Deleting gateway..."):
self.api_client.client.gateways.delete(
project_name=self.api_client.project, gateways_names=[conf.name]
)
if not confirmed and not args.yes:
if not confirm_ask(
f"Gateway [code]{conf.name}[/] does not exist yet. Create the gateway?"
):
console.print("\nExiting...")
return
with console.status("Creating gateway..."):
gateway = self.api_client.client.gateways.create(
project_name=self.api_client.project,
configuration=conf,
)
print_gateways_table([gateway])
Loading
Loading