Skip to content

Commit 48cbbad

Browse files
authored
/imports/list API and dstack import list CLI (#3682)
Add API and CLI for viewing imports. ```shell $ dstack import list NAME FLEETS team-a/my-export my-fleet, another-fleet ```
1 parent ed81b05 commit 48cbbad

File tree

12 files changed

+436
-4
lines changed

12 files changed

+436
-4
lines changed

docs/docs/concepts/exports.md

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,20 @@ Use `-y` to skip the confirmation prompt.
116116

117117
## Access imported fleets
118118

119-
From the importer project's perspective, exported fleets appear automatically in `dstack fleet list`
120-
with a `<project>/<fleet>` prefix:
119+
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:
120+
121+
<div class="termy">
122+
123+
```shell
124+
$ dstack import list
125+
NAME FLEETS
126+
team-a/my-export my-fleet, another-fleet
127+
128+
```
129+
130+
</div>
131+
132+
Imported fleets also appear in `dstack fleet list` in the `<project>/<fleet>` format:
121133

122134
<div class="termy">
123135

@@ -152,5 +164,6 @@ fleets:
152164
153165
!!! info "What's next?"
154166
1. Check the [`dstack export` CLI reference](../reference/cli/dstack/export.md)
155-
2. Learn how to manage [fleets](fleets.md)
156-
3. Read about [projects](projects.md) and project roles
167+
1. Check the [`dstack import` CLI reference](../reference/cli/dstack/import.md)
168+
1. Learn how to manage [fleets](fleets.md)
169+
1. Read about [projects](projects.md) and project roles
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# dstack import
2+
3+
The `dstack import` commands list resources imported into the project from other projects.
4+
See [Exports](../../../concepts/exports.md) for details.
5+
6+
## dstack import list
7+
8+
The `dstack import list` command lists all imports in the project.
9+
10+
##### Usage
11+
12+
<div class="termy">
13+
14+
```shell
15+
$ dstack import list --help
16+
#GENERATE#
17+
```
18+
19+
</div>

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,7 @@ nav:
273273
- dstack gateway: docs/reference/cli/dstack/gateway.md
274274
- dstack secret: docs/reference/cli/dstack/secret.md
275275
- dstack export: docs/reference/cli/dstack/export.md
276+
- dstack import: docs/reference/cli/dstack/import.md
276277
- API:
277278
- Python API: docs/reference/api/python/index.md
278279
- REST API: docs/reference/api/rest/index.md
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import argparse
2+
from typing import Any, Union
3+
4+
from rich.table import Table
5+
6+
from dstack._internal.cli.commands import APIBaseCommand
7+
from dstack._internal.cli.utils.common import add_row_from_dict, console
8+
from dstack._internal.core.models.imports import Import
9+
10+
11+
class ImportCommand(APIBaseCommand):
12+
NAME = "import"
13+
DESCRIPTION = "Manage imports"
14+
15+
def _register(self):
16+
super()._register()
17+
self._parser.set_defaults(subfunc=self._list)
18+
subparsers = self._parser.add_subparsers(dest="action")
19+
20+
list_parser = subparsers.add_parser(
21+
"list", help="List imports", formatter_class=self._parser.formatter_class
22+
)
23+
list_parser.set_defaults(subfunc=self._list)
24+
25+
def _command(self, args: argparse.Namespace):
26+
super()._command(args)
27+
args.subfunc(args)
28+
29+
def _list(self, args: argparse.Namespace):
30+
imports = self.api.client.imports.list(self.api.project)
31+
print_imports_table(imports)
32+
33+
34+
def print_imports_table(imports: list[Import]):
35+
table = Table(box=None)
36+
table.add_column("NAME", no_wrap=True)
37+
table.add_column("FLEETS")
38+
39+
for imp in imports:
40+
name = f"{imp.export.project_name}/{imp.export.name}"
41+
fleets = (
42+
", ".join([f.name for f in imp.export.exported_fleets])
43+
if imp.export.exported_fleets
44+
else "-"
45+
)
46+
47+
row: dict[Union[str, int], Any] = {
48+
"NAME": name,
49+
"FLEETS": fleets,
50+
}
51+
add_row_from_dict(table, row)
52+
53+
console.print(table)
54+
console.print()

src/dstack/_internal/cli/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from dstack._internal.cli.commands.export import ExportCommand
1313
from dstack._internal.cli.commands.fleet import FleetCommand
1414
from dstack._internal.cli.commands.gateway import GatewayCommand
15+
from dstack._internal.cli.commands.import_ import ImportCommand
1516
from dstack._internal.cli.commands.init import InitCommand
1617
from dstack._internal.cli.commands.login import LoginCommand
1718
from dstack._internal.cli.commands.logs import LogsCommand
@@ -69,6 +70,7 @@ def main():
6970
EventCommand.register(subparsers)
7071
ExportCommand.register(subparsers)
7172
FleetCommand.register(subparsers)
73+
ImportCommand.register(subparsers)
7274
GatewayCommand.register(subparsers)
7375
InitCommand.register(subparsers)
7476
OfferCommand.register(subparsers)
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import uuid
2+
3+
from dstack._internal.core.models.common import CoreModel
4+
5+
6+
class ImportExportedFleet(CoreModel):
7+
id: uuid.UUID
8+
name: str
9+
10+
11+
class ImportExport(CoreModel):
12+
id: uuid.UUID
13+
name: str
14+
project_name: str
15+
exported_fleets: list[ImportExportedFleet]
16+
17+
18+
class Import(CoreModel):
19+
id: uuid.UUID
20+
export: ImportExport

src/dstack/_internal/server/app.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
fleets,
3737
gateways,
3838
gpus,
39+
imports,
3940
instances,
4041
logs,
4142
metrics,
@@ -256,6 +257,7 @@ def register_routes(app: FastAPI, ui: bool = True):
256257
app.include_router(events.root_router)
257258
app.include_router(templates.router)
258259
app.include_router(exports.project_router)
260+
app.include_router(imports.project_router)
259261
app.include_router(sshproxy.router)
260262

261263
@app.exception_handler(ForbiddenError)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from typing import Annotated
2+
3+
from fastapi import APIRouter, Depends
4+
from sqlalchemy.ext.asyncio import AsyncSession
5+
6+
from dstack._internal.core.models.imports import Import
7+
from dstack._internal.server.db import get_session
8+
from dstack._internal.server.models import ProjectModel, UserModel
9+
from dstack._internal.server.security.permissions import ProjectMember
10+
from dstack._internal.server.services import imports as imports_services
11+
from dstack._internal.server.utils.routers import get_base_api_additional_responses
12+
13+
project_router = APIRouter(
14+
prefix="/api/project/{project_name}/imports",
15+
tags=["imports"],
16+
responses=get_base_api_additional_responses(),
17+
)
18+
19+
20+
@project_router.post("/list", response_model=list[Import])
21+
async def list_imports(
22+
session: Annotated[AsyncSession, Depends(get_session)],
23+
user_project: Annotated[tuple[UserModel, ProjectModel], Depends(ProjectMember())],
24+
):
25+
_, project = user_project
26+
return await imports_services.list_imports(
27+
session=session,
28+
project=project,
29+
)
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from sqlalchemy import select
2+
from sqlalchemy.ext.asyncio import AsyncSession
3+
from sqlalchemy.orm import joinedload, selectinload
4+
5+
from dstack._internal.core.models.imports import Import, ImportExport, ImportExportedFleet
6+
from dstack._internal.server.models import (
7+
ExportedFleetModel,
8+
ExportModel,
9+
FleetModel,
10+
ImportModel,
11+
ProjectModel,
12+
)
13+
14+
15+
async def list_imports(session: AsyncSession, project: ProjectModel) -> list[Import]:
16+
res = await session.execute(
17+
select(ImportModel)
18+
.where(ImportModel.project_id == project.id)
19+
.options(
20+
joinedload(ImportModel.export)
21+
.load_only(ExportModel.id, ExportModel.name)
22+
.options(
23+
joinedload(ExportModel.project).load_only(ProjectModel.name),
24+
selectinload(
25+
ExportModel.exported_fleets.and_(
26+
ExportedFleetModel.fleet.has(FleetModel.deleted == False)
27+
)
28+
)
29+
.joinedload(ExportedFleetModel.fleet)
30+
.load_only(FleetModel.id, FleetModel.name),
31+
)
32+
)
33+
.order_by(ImportModel.created_at.desc())
34+
)
35+
imports = res.scalars().all()
36+
return [import_model_to_import(imp) for imp in imports]
37+
38+
39+
def import_model_to_import(import_model: ImportModel) -> Import:
40+
return Import(
41+
id=import_model.id,
42+
export=ImportExport(
43+
id=import_model.export.id,
44+
name=import_model.export.name,
45+
project_name=import_model.export.project.name,
46+
exported_fleets=[
47+
ImportExportedFleet(
48+
id=ef.fleet.id,
49+
name=ef.fleet.name,
50+
)
51+
for ef in import_model.export.exported_fleets
52+
],
53+
),
54+
)

src/dstack/api/server/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from dstack.api.server._fleets import FleetsAPIClient
2323
from dstack.api.server._gateways import GatewaysAPIClient
2424
from dstack.api.server._gpus import GpusAPIClient
25+
from dstack.api.server._imports import ImportsAPIClient
2526
from dstack.api.server._logs import LogsAPIClient
2627
from dstack.api.server._metrics import MetricsAPIClient
2728
from dstack.api.server._projects import ProjectsAPIClient
@@ -132,6 +133,10 @@ def volumes(self) -> VolumesAPIClient:
132133
def exports(self) -> ExportsAPIClient:
133134
return ExportsAPIClient(self._request, self._logger)
134135

136+
@property
137+
def imports(self) -> ImportsAPIClient:
138+
return ImportsAPIClient(self._request, self._logger)
139+
135140
@property
136141
def files(self) -> FilesAPIClient:
137142
return FilesAPIClient(self._request, self._logger)

0 commit comments

Comments
 (0)