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

Add --ports to dstack run #573

Merged
merged 2 commits into from
Jul 18, 2023
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
23 changes: 17 additions & 6 deletions cli/dstack/_internal/configurators/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ def get_parser(
if parser is None:
parser = argparse.ArgumentParser(prog=prog, formatter_class=RichHelpFormatter)

parser.add_argument(
"-p", "--ports", metavar="PORT", type=port_mapping, nargs=argparse.ONE_OR_MORE
)

spot_group = parser.add_mutually_exclusive_group()
spot_group.add_argument(
"--spot", action="store_const", dest="spot_policy", const=job.SpotPolicy.SPOT
Expand Down Expand Up @@ -76,6 +80,9 @@ def get_parser(
return parser

def apply_args(self, args: argparse.Namespace):
if args.ports is not None:
self.conf.ports = list(ports.merge_ports(self.conf.ports, args.ports).values())

if args.spot_policy is not None:
self.profile.spot_policy = args.spot_policy

Expand Down Expand Up @@ -212,8 +219,8 @@ def app_specs(self) -> List[job.AppSpec]:
for i, pm in enumerate(ports.filter_reserved_ports(self.ports())):
specs.append(
job.AppSpec(
port=pm.port,
map_to_port=pm.map_to_port,
port=pm.container_port,
map_to_port=pm.local_port,
app_name=f"app_{i}",
)
)
Expand All @@ -226,13 +233,12 @@ def python(self) -> str:
return PythonVersion(f"{version_info.major}.{version_info.minor}").value

def ports(self) -> Dict[int, ports.PortMapping]:
mapping = [ports.PortMapping(p) for p in self.conf.ports]
ports.unique_ports_constraint([pm.port for pm in mapping])
ports.unique_ports_constraint([pm.container_port for pm in self.conf.ports])
ports.unique_ports_constraint(
[pm.map_to_port for pm in mapping if pm.map_to_port is not None],
[pm.local_port for pm in self.conf.ports if pm.local_port is not None],
error="Mapped port {} is already in use",
)
return {pm.port: pm for pm in mapping}
return {pm.container_port: pm for pm in self.conf.ports}

def env(self) -> Dict[str, str]:
return self.conf.env
Expand Down Expand Up @@ -288,3 +294,8 @@ def validate_local_path(path: str, home: Optional[str], working_dir: str) -> str

class HomeDirUnsetError(DstackError):
pass


def port_mapping(v: str) -> ports.PortMapping:
# argparse uses __name__ for handling ValueError
return ports.PortMapping.parse(v)
53 changes: 13 additions & 40 deletions cli/dstack/_internal/configurators/ports.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import argparse
import re
from typing import Dict, Iterator, List, Optional, Union
from typing import Dict, Iterator, List, Optional

from dstack._internal.core.configuration import PortMapping
from dstack._internal.core.error import DstackError

RESERVED_PORTS_START = 10000
Expand All @@ -16,46 +15,20 @@ class PortUsedError(DstackError):
pass


class PortMapping:
"""
Valid formats:
- 1234
- "1234"
- "1234:5678"
"""

def __init__(self, v: Union[str, int]):
self.port: int
self.map_to_port: Optional[int] = None

if isinstance(v, int):
self.port = v
return
r = re.search(r"^(\d+)(?::(\d+))?$", v)
if r is None:
raise argparse.ArgumentTypeError(f"{v} is not a valid port or port mapping")
port, map_to_port = r.groups()
self.port = int(port)
if map_to_port is not None:
self.map_to_port = int(map_to_port)

def __repr__(self):
s = str(self.port)
if self.map_to_port is not None:
s += f":{self.map_to_port}"
return f'PortMapping("{s}")'


def merge_ports(schema: List[PortMapping], args: List[PortMapping]) -> Dict[int, PortMapping]:
unique_ports_constraint([pm.port for pm in schema], error="Schema port {} is already in use")
unique_ports_constraint([pm.port for pm in args], error="Args port {} is already in use")
unique_ports_constraint(
[pm.container_port for pm in schema], error="Schema port {} is already in use"
)
unique_ports_constraint(
[pm.container_port for pm in args], error="Args port {} is already in use"
)

ports = {pm.port: pm for pm in schema}
ports = {pm.container_port: pm for pm in schema}
for pm in args: # override schema
ports[pm.port] = pm
ports[pm.container_port] = pm

unique_ports_constraint(
[pm.map_to_port for pm in ports.values() if pm.map_to_port is not None],
[pm.local_port for pm in ports.values() if pm.local_port is not None],
error="Mapped port {} is already in use",
)
return ports
Expand All @@ -71,12 +44,12 @@ def unique_ports_constraint(ports: List[int], error: str = "Port {} is already i

def get_map_to_port(ports: Dict[int, PortMapping], port: int) -> Optional[int]:
if port in ports:
return ports[port].map_to_port
return ports[port].local_port
return None


def filter_reserved_ports(ports: Dict[int, PortMapping]) -> Iterator[PortMapping]:
for i in ports.values():
if RESERVED_PORTS_START <= i.port <= RESERVED_PORTS_END:
if RESERVED_PORTS_START <= i.container_port <= RESERVED_PORTS_END:
continue
yield i
38 changes: 37 additions & 1 deletion cli/dstack/_internal/core/configuration.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import re
from enum import Enum
from typing import Dict, List, Optional, Union

from pydantic import BaseModel, Extra, Field, conint, constr, validator
from typing_extensions import Annotated, Literal

CommandsList = List[str]
ValidPort = conint(gt=0, le=65536)


class PythonVersion(str, Enum):
Expand All @@ -25,6 +27,28 @@ class RegistryAuth(ForbidExtra):
password: Annotated[str, Field(description="Password or access token")]


class PortMapping(ForbidExtra):
local_port: Optional[ValidPort] = None
container_port: ValidPort

@classmethod
def parse(cls, v: str) -> "PortMapping":
"""
Possible values:
- 8080
- :8080
- 80:8080
"""
r = re.search(r"^(?:(\d+)?:)?(\d+)?$", v)
if not r:
raise ValueError(v)
local_port, container_port = r.groups()
return PortMapping(
local_port=None if local_port is None else int(local_port),
container_port=container_port,
)


class Artifact(ForbidExtra):
path: Annotated[
str, Field(description="The path to the folder that must be stored as an output artifact")
Expand Down Expand Up @@ -52,7 +76,7 @@ class BaseConfiguration(ForbidExtra):
Field(description="The major version of Python\nMutually exclusive with the image"),
]
ports: Annotated[
List[Union[constr(regex=r"^[0-9]+:[0-9]+$"), conint(gt=0, le=65536)]],
List[Union[constr(regex=r"^(?:([0-9]+)?:)?[0-9]+$"), ValidPort, PortMapping]],
Field(description="Port numbers/mapping to expose"),
] = []
env: Annotated[
Expand All @@ -78,6 +102,18 @@ def convert_python(cls, v, values) -> Optional[PythonVersion]:
return PythonVersion(v)
return v

@validator("ports")
def convert_ports(cls, v) -> List[PortMapping]:
ports = []
for i in v:
if isinstance(i, int):
ports.append(PortMapping(container_port=i))
elif isinstance(i, str):
ports.append(PortMapping.parse(i))
else:
ports.append(i)
return ports

@validator("env")
def convert_env(cls, v) -> Dict[str, str]:
if isinstance(v, list):
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/reference/cli/run.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ The following arguments are optional:
- `-d`, `--detach` – (Optional) Run in the detached mode. Means, the command doesn't
poll logs and run status.

[//]: # (- `-p PORT [PORT ...]`, `--port PORT [PORT ...]` – &#40;Optional&#41; Requests ports or define mappings for them &#40;`APP_PORT:LOCAL_PORT`&#41;)
[//]: # (- `-t TAG`, `--tag TAG` – &#40;Optional&#41; A tag name. Warning, if the tag exists, it will be overridden.)
- `-p PORT [PORT ...]`, `--ports PORT [PORT ...]` – (Optional) Requests ports or define mappings for them (`LOCAL_PORT:CONTAINER_PORT`)
- `ARGS` – (Optional) Use `ARGS` to pass custom run arguments

Spot policy (the arguments are mutually exclusive):
Expand Down
Loading