Skip to content
Open
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
21 changes: 17 additions & 4 deletions docs/docs/concepts/exports.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,20 @@ Use `-y` to skip the confirmation prompt.

## Access imported fleets

From the importer project's perspective, exported fleets appear automatically in `dstack fleet list`
with a `<project>/<fleet>` prefix:
From the importer project's perspective, use `dstack import list` (or simply `dstack import`) to list all imports in the project — i.e., all exports from other projects that this project has been granted access to:

<div class="termy">

```shell
$ dstack import list
NAME FLEETS
team-a/my-export my-fleet, another-fleet

```

</div>

Imported fleets also appear in `dstack fleet list` in the `<project>/<fleet>` format:

<div class="termy">

Expand All @@ -139,5 +151,6 @@ Imported fleets can be used for runs just like the project's own fleets.

!!! info "What's next?"
1. Check the [`dstack export` CLI reference](../reference/cli/dstack/export.md)
2. Learn how to manage [fleets](fleets.md)
3. Read about [projects](projects.md) and project roles
1. Check the [`dstack import` CLI reference](../reference/cli/dstack/import.md)
1. Learn how to manage [fleets](fleets.md)
1. Read about [projects](projects.md) and project roles
19 changes: 19 additions & 0 deletions docs/docs/reference/cli/dstack/import.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# dstack import

The `dstack import` commands list resources imported into the project from other projects.
See [Exports](../../../concepts/exports.md) for details.

## dstack import list

The `dstack import list` command lists all imports in the project.

##### Usage

<div class="termy">

```shell
$ dstack import list --help
#GENERATE#
```

</div>
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ nav:
- dstack gateway: docs/reference/cli/dstack/gateway.md
- dstack secret: docs/reference/cli/dstack/secret.md
- dstack export: docs/reference/cli/dstack/export.md
- dstack import: docs/reference/cli/dstack/import.md
- API:
- Python API: docs/reference/api/python/index.md
- REST API: docs/reference/api/rest/index.md
Expand Down
54 changes: 54 additions & 0 deletions src/dstack/_internal/cli/commands/import_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import argparse
from typing import Any, Union

from rich.table import Table

from dstack._internal.cli.commands import APIBaseCommand
from dstack._internal.cli.utils.common import add_row_from_dict, console
from dstack._internal.core.models.imports import Import


class ImportCommand(APIBaseCommand):
NAME = "import"
DESCRIPTION = "Manage imports"

def _register(self):
super()._register()
self._parser.set_defaults(subfunc=self._list)
subparsers = self._parser.add_subparsers(dest="action")

list_parser = subparsers.add_parser(
"list", help="List imports", formatter_class=self._parser.formatter_class
)
list_parser.set_defaults(subfunc=self._list)

def _command(self, args: argparse.Namespace):
super()._command(args)
args.subfunc(args)

def _list(self, args: argparse.Namespace):
imports = self.api.client.imports.list(self.api.project)
print_imports_table(imports)


def print_imports_table(imports: list[Import]):
table = Table(box=None)
table.add_column("NAME", no_wrap=True)
table.add_column("FLEETS")

for imp in imports:
name = f"{imp.export.project_name}/{imp.export.name}"
fleets = (
", ".join([f.name for f in imp.export.exported_fleets])
if imp.export.exported_fleets
else "-"
)

row: dict[Union[str, int], Any] = {
"NAME": name,
"FLEETS": fleets,
}
add_row_from_dict(table, row)

console.print(table)
console.print()
2 changes: 2 additions & 0 deletions src/dstack/_internal/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from dstack._internal.cli.commands.export import ExportCommand
from dstack._internal.cli.commands.fleet import FleetCommand
from dstack._internal.cli.commands.gateway import GatewayCommand
from dstack._internal.cli.commands.import_ import ImportCommand
from dstack._internal.cli.commands.init import InitCommand
from dstack._internal.cli.commands.login import LoginCommand
from dstack._internal.cli.commands.logs import LogsCommand
Expand Down Expand Up @@ -69,6 +70,7 @@ def main():
EventCommand.register(subparsers)
ExportCommand.register(subparsers)
FleetCommand.register(subparsers)
ImportCommand.register(subparsers)
GatewayCommand.register(subparsers)
InitCommand.register(subparsers)
OfferCommand.register(subparsers)
Expand Down
20 changes: 20 additions & 0 deletions src/dstack/_internal/core/models/imports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import uuid

from dstack._internal.core.models.common import CoreModel


class ImportExportedFleet(CoreModel):
id: uuid.UUID
name: str


class ImportExport(CoreModel):
id: uuid.UUID
name: str
project_name: str
exported_fleets: list[ImportExportedFleet]


class Import(CoreModel):
id: uuid.UUID
export: ImportExport
2 changes: 2 additions & 0 deletions src/dstack/_internal/server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
fleets,
gateways,
gpus,
imports,
instances,
logs,
metrics,
Expand Down Expand Up @@ -256,6 +257,7 @@ def register_routes(app: FastAPI, ui: bool = True):
app.include_router(events.root_router)
app.include_router(templates.router)
app.include_router(exports.project_router)
app.include_router(imports.project_router)
app.include_router(sshproxy.router)

@app.exception_handler(ForbiddenError)
Expand Down
29 changes: 29 additions & 0 deletions src/dstack/_internal/server/routers/imports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from typing import Annotated

from fastapi import APIRouter, Depends
from sqlalchemy.ext.asyncio import AsyncSession

from dstack._internal.core.models.imports import Import
from dstack._internal.server.db import get_session
from dstack._internal.server.models import ProjectModel, UserModel
from dstack._internal.server.security.permissions import ProjectMember
from dstack._internal.server.services import imports as imports_services
from dstack._internal.server.utils.routers import get_base_api_additional_responses

project_router = APIRouter(
prefix="/api/project/{project_name}/imports",
tags=["imports"],
responses=get_base_api_additional_responses(),
)


@project_router.post("/list", response_model=list[Import])
async def list_imports(
session: Annotated[AsyncSession, Depends(get_session)],
user_project: Annotated[tuple[UserModel, ProjectModel], Depends(ProjectMember())],
):
_, project = user_project
return await imports_services.list_imports(
session=session,
project=project,
)
54 changes: 54 additions & 0 deletions src/dstack/_internal/server/services/imports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import joinedload, selectinload

from dstack._internal.core.models.imports import Import, ImportExport, ImportExportedFleet
from dstack._internal.server.models import (
ExportedFleetModel,
ExportModel,
FleetModel,
ImportModel,
ProjectModel,
)


async def list_imports(session: AsyncSession, project: ProjectModel) -> list[Import]:
res = await session.execute(
select(ImportModel)
.where(ImportModel.project_id == project.id)
.options(
joinedload(ImportModel.export)
.load_only(ExportModel.id, ExportModel.name)
.options(
joinedload(ExportModel.project).load_only(ProjectModel.name),
selectinload(
ExportModel.exported_fleets.and_(
ExportedFleetModel.fleet.has(FleetModel.deleted == False)
)
)
.joinedload(ExportedFleetModel.fleet)
.load_only(FleetModel.id, FleetModel.name),
)
)
.order_by(ImportModel.created_at.desc())
)
imports = res.scalars().all()
return [import_model_to_import(imp) for imp in imports]


def import_model_to_import(import_model: ImportModel) -> Import:
return Import(
id=import_model.id,
export=ImportExport(
id=import_model.export.id,
name=import_model.export.name,
project_name=import_model.export.project.name,
exported_fleets=[
ImportExportedFleet(
id=ef.fleet.id,
name=ef.fleet.name,
)
for ef in import_model.export.exported_fleets
],
),
)
5 changes: 5 additions & 0 deletions src/dstack/api/server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from dstack.api.server._fleets import FleetsAPIClient
from dstack.api.server._gateways import GatewaysAPIClient
from dstack.api.server._gpus import GpusAPIClient
from dstack.api.server._imports import ImportsAPIClient
from dstack.api.server._logs import LogsAPIClient
from dstack.api.server._metrics import MetricsAPIClient
from dstack.api.server._projects import ProjectsAPIClient
Expand Down Expand Up @@ -132,6 +133,10 @@ def volumes(self) -> VolumesAPIClient:
def exports(self) -> ExportsAPIClient:
return ExportsAPIClient(self._request, self._logger)

@property
def imports(self) -> ImportsAPIClient:
return ImportsAPIClient(self._request, self._logger)

@property
def files(self) -> FilesAPIClient:
return FilesAPIClient(self._request, self._logger)
Expand Down
12 changes: 12 additions & 0 deletions src/dstack/api/server/_imports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from typing import List

from pydantic import parse_obj_as

from dstack._internal.core.models.imports import Import
from dstack.api.server._group import APIClientGroup


class ImportsAPIClient(APIClientGroup):
def list(self, project_name: str) -> List[Import]:
resp = self._request(f"/api/project/{project_name}/imports/list")
return parse_obj_as(List[Import.__response__], resp.json())
Loading
Loading