Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions frontend/src/pages/Fleets/List/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ export const useFilters = (localStorePrefix = 'fleet-list-page') => {
return {
...params,
only_active: onlyActive,
include_imported: true,
} as Partial<TFleetListRequestParams>;
}, [propertyFilterQuery, onlyActive]);

Expand Down
1 change: 1 addition & 0 deletions frontend/src/pages/Instances/List/hooks/useFilters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export const useFilters = (localStorePrefix = 'instances-list-page') => {
return {
...params,
only_active: onlyActive,
include_imported: true,
} as Partial<TInstanceListRequestParams>;
}, [propertyFilterQuery, onlyActive]);

Expand Down
1 change: 1 addition & 0 deletions frontend/src/types/fleet.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ declare type TSpotPolicy = 'spot' | 'on-demand' | 'auto';
declare type TFleetListRequestParams = TBaseRequestListParams & {
project_name?: string;
only_active?: boolean;
include_imported?: boolean;
};

declare interface ISSHHostParamsRequest {
Expand Down
1 change: 1 addition & 0 deletions frontend/src/types/instance.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ declare type TInstanceListRequestParams = TBaseRequestListParams & {
project_names?: string[];
fleet_ids?: string[];
only_active?: boolean;
include_imported?: boolean;
};

declare type TInstanceStatus =
Expand Down
4 changes: 2 additions & 2 deletions src/dstack/_internal/cli/commands/fleet.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ def _command(self, args: argparse.Namespace):
args.subfunc(args)

def _list(self, args: argparse.Namespace):
fleets = self.api.client.fleets.list(self.api.project)
fleets = self.api.client.fleets.list(self.api.project, include_imported=True)
if not args.watch:
print_fleets_table(fleets, verbose=args.verbose)
return
Expand All @@ -103,7 +103,7 @@ def _list(self, args: argparse.Namespace):
while True:
live.update(get_fleets_table(fleets, verbose=args.verbose))
time.sleep(LIVE_TABLE_PROVISION_INTERVAL_SECS)
fleets = self.api.client.fleets.list(self.api.project)
fleets = self.api.client.fleets.list(self.api.project, include_imported=True)
except KeyboardInterrupt:
pass

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
"""Add resource exports imports

Revision ID: ea7cca121fcb
Revises: 46150101edec
Create Date: 2026-03-02 13:45:57.118841+00:00

"""

import sqlalchemy as sa
import sqlalchemy_utils
from alembic import op

import dstack._internal.server.models

# revision identifiers, used by Alembic.
revision = "ea7cca121fcb"
down_revision = "46150101edec"
branch_labels = None
depends_on = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"resource_exports",
sa.Column("id", sqlalchemy_utils.types.uuid.UUIDType(binary=False), nullable=False),
sa.Column("name", sa.String(length=100), nullable=False),
sa.Column(
"project_id", sqlalchemy_utils.types.uuid.UUIDType(binary=False), nullable=False
),
sa.Column("created_at", dstack._internal.server.models.NaiveDateTime(), nullable=False),
sa.ForeignKeyConstraint(
["project_id"],
["projects.id"],
name=op.f("fk_resource_exports_project_id_projects"),
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_resource_exports")),
sa.UniqueConstraint("project_id", "name", name="uq_resource_exports_project_id_name"),
)
with op.batch_alter_table("resource_exports", schema=None) as batch_op:
batch_op.create_index(
batch_op.f("ix_resource_exports_project_id"), ["project_id"], unique=False
)

op.create_table(
"exported_fleets",
sa.Column("id", sqlalchemy_utils.types.uuid.UUIDType(binary=False), nullable=False),
sa.Column(
"resource_export_id",
sqlalchemy_utils.types.uuid.UUIDType(binary=False),
nullable=False,
),
sa.Column("fleet_id", sqlalchemy_utils.types.uuid.UUIDType(binary=False), nullable=False),
sa.ForeignKeyConstraint(
["fleet_id"],
["fleets.id"],
name=op.f("fk_exported_fleets_fleet_id_fleets"),
ondelete="CASCADE",
),
sa.ForeignKeyConstraint(
["resource_export_id"],
["resource_exports.id"],
name=op.f("fk_exported_fleets_resource_export_id_resource_exports"),
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_exported_fleets")),
sa.UniqueConstraint(
"resource_export_id", "fleet_id", name="uq_exported_fleets_resource_export_id_fleet_id"
),
)
with op.batch_alter_table("exported_fleets", schema=None) as batch_op:
batch_op.create_index(
batch_op.f("ix_exported_fleets_fleet_id"), ["fleet_id"], unique=False
)
batch_op.create_index(
batch_op.f("ix_exported_fleets_resource_export_id"),
["resource_export_id"],
unique=False,
)

op.create_table(
"resource_imports",
sa.Column("id", sqlalchemy_utils.types.uuid.UUIDType(binary=False), nullable=False),
sa.Column(
"project_id", sqlalchemy_utils.types.uuid.UUIDType(binary=False), nullable=False
),
sa.Column(
"resource_export_id",
sqlalchemy_utils.types.uuid.UUIDType(binary=False),
nullable=False,
),
sa.Column("created_at", dstack._internal.server.models.NaiveDateTime(), nullable=False),
sa.ForeignKeyConstraint(
["project_id"],
["projects.id"],
name=op.f("fk_resource_imports_project_id_projects"),
ondelete="CASCADE",
),
sa.ForeignKeyConstraint(
["resource_export_id"],
["resource_exports.id"],
name=op.f("fk_resource_imports_resource_export_id_resource_exports"),
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_resource_imports")),
sa.UniqueConstraint(
"project_id",
"resource_export_id",
name="uq_resource_imports_project_id_resource_export_id",
),
)
with op.batch_alter_table("resource_imports", schema=None) as batch_op:
batch_op.create_index(
batch_op.f("ix_resource_imports_project_id"), ["project_id"], unique=False
)
batch_op.create_index(
batch_op.f("ix_resource_imports_resource_export_id"),
["resource_export_id"],
unique=False,
)

# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("resource_imports", schema=None) as batch_op:
batch_op.drop_index(batch_op.f("ix_resource_imports_resource_export_id"))
batch_op.drop_index(batch_op.f("ix_resource_imports_project_id"))

op.drop_table("resource_imports")
with op.batch_alter_table("exported_fleets", schema=None) as batch_op:
batch_op.drop_index(batch_op.f("ix_exported_fleets_resource_export_id"))
batch_op.drop_index(batch_op.f("ix_exported_fleets_fleet_id"))

op.drop_table("exported_fleets")
with op.batch_alter_table("resource_exports", schema=None) as batch_op:
batch_op.drop_index(batch_op.f("ix_resource_exports_project_id"))

op.drop_table("resource_exports")
# ### end Alembic commands ###
68 changes: 68 additions & 0 deletions src/dstack/_internal/server/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -978,3 +978,71 @@ class EventTargetModel(BaseModel):
)
entity_id: Mapped[uuid.UUID] = mapped_column(UUIDType(binary=False), index=True)
entity_name: Mapped[str] = mapped_column(String(200))


class ResourceExportModel(BaseModel):
__tablename__ = "resource_exports"
__table_args__ = (
UniqueConstraint("project_id", "name", name="uq_resource_exports_project_id_name"),
)

id: Mapped[uuid.UUID] = mapped_column(
UUIDType(binary=False), primary_key=True, default=uuid.uuid4
)
name: Mapped[str] = mapped_column(String(100))
project_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("projects.id", ondelete="CASCADE"), index=True
)
project: Mapped["ProjectModel"] = relationship()
created_at: Mapped[datetime] = mapped_column(NaiveDateTime, default=get_current_datetime)
resource_imports: Mapped[List["ResourceImportModel"]] = relationship(
back_populates="resource_export"
)
exported_fleets: Mapped[List["ExportedFleetModel"]] = relationship(
back_populates="resource_export"
)


class ResourceImportModel(BaseModel):
__tablename__ = "resource_imports"
__table_args__ = (
UniqueConstraint(
"project_id",
"resource_export_id",
name="uq_resource_imports_project_id_resource_export_id",
),
)

id: Mapped[uuid.UUID] = mapped_column(
UUIDType(binary=False), primary_key=True, default=uuid.uuid4
)
project_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("projects.id", ondelete="CASCADE"), index=True
)
project: Mapped["ProjectModel"] = relationship()
resource_export_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("resource_exports.id", ondelete="CASCADE"), index=True
)
resource_export: Mapped["ResourceExportModel"] = relationship()
created_at: Mapped[datetime] = mapped_column(NaiveDateTime, default=get_current_datetime)


class ExportedFleetModel(BaseModel):
__tablename__ = "exported_fleets"
__table_args__ = (
UniqueConstraint(
"resource_export_id", "fleet_id", name="uq_exported_fleets_resource_export_id_fleet_id"
),
)

id: Mapped[uuid.UUID] = mapped_column(
UUIDType(binary=False), primary_key=True, default=uuid.uuid4
)
resource_export_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("resource_exports.id", ondelete="CASCADE"), index=True
)
resource_export: Mapped["ResourceExportModel"] = relationship()
fleet_id: Mapped[uuid.UUID] = mapped_column(
ForeignKey("fleets.id", ondelete="CASCADE"), index=True
)
fleet: Mapped["FleetModel"] = relationship()
27 changes: 22 additions & 5 deletions src/dstack/_internal/server/routers/fleets.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from dstack._internal.core.models.fleets import Fleet, FleetPlan
from dstack._internal.server.compatibility.common import patch_offers_list
from dstack._internal.server.db import get_session
from dstack._internal.server.deps import Project
from dstack._internal.server.models import ProjectModel, UserModel
from dstack._internal.server.schemas.fleets import (
ApplyFleetPlanRequest,
Expand All @@ -18,8 +19,13 @@
GetFleetPlanRequest,
GetFleetRequest,
ListFleetsRequest,
ListProjectFleetsRequest,
)
from dstack._internal.server.security.permissions import (
Authenticated,
ProjectMember,
check_can_access_fleet,
)
from dstack._internal.server.security.permissions import Authenticated, ProjectMember
from dstack._internal.server.utils.routers import (
CustomORJSONResponse,
get_base_api_additional_responses,
Expand Down Expand Up @@ -58,6 +64,7 @@ async def list_fleets(
user=user,
project_name=body.project_name,
only_active=body.only_active,
include_imported=body.include_imported,
prev_created_at=body.prev_created_at,
prev_id=body.prev_id,
limit=body.limit,
Expand All @@ -68,6 +75,7 @@ async def list_fleets(

@project_router.post("/list", response_model=List[Fleet])
async def list_project_fleets(
body: Optional[ListProjectFleetsRequest] = None,
session: AsyncSession = Depends(get_session),
user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectMember()),
):
Expand All @@ -76,25 +84,34 @@ async def list_project_fleets(
Includes only active fleet instances. To list all fleet instances, use `/api/instances/list`.
"""
_, project = user_project
if body is None:
body = ListProjectFleetsRequest()
return CustomORJSONResponse(
await fleets_services.list_project_fleets(session=session, project=project)
await fleets_services.list_project_fleets(
session=session,
project=project,
include_imported=body.include_imported,
)
)


@project_router.post("/get", response_model=Fleet)
async def get_fleet(
body: GetFleetRequest,
session: AsyncSession = Depends(get_session),
user_project: Tuple[UserModel, ProjectModel] = Depends(ProjectMember()),
user: UserModel = Depends(Authenticated()),
project: ProjectModel = Depends(Project()),
):
"""
Returns a fleet given `name` or `id`.
If given `name`, does not return deleted fleets.
If given `id`, returns deleted fleets.
"""
_, project = user_project
await check_can_access_fleet(
session=session, user=user, fleet_project=project, fleet_name_or_id=body.get_name_or_id()
)
fleet = await fleets_services.get_fleet(
session=session, project=project, name=body.name, fleet_id=body.id
session=session, project=project, name_or_id=body.get_name_or_id()
)
if fleet is None:
raise ResourceNotExistsError()
Expand Down
15 changes: 12 additions & 3 deletions src/dstack/_internal/server/routers/instances.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,19 @@
from dstack._internal.core.errors import ResourceNotExistsError
from dstack._internal.core.models.instances import Instance
from dstack._internal.server.db import get_session
from dstack._internal.server.deps import Project
from dstack._internal.server.models import ProjectModel, UserModel
from dstack._internal.server.schemas.instances import (
GetInstanceHealthChecksRequest,
GetInstanceHealthChecksResponse,
GetInstanceRequest,
ListInstancesRequest,
)
from dstack._internal.server.security.permissions import Authenticated, ProjectMember
from dstack._internal.server.security.permissions import (
Authenticated,
ProjectMember,
check_can_access_instance,
)
from dstack._internal.server.utils.routers import (
CustomORJSONResponse,
get_base_api_additional_responses,
Expand Down Expand Up @@ -52,6 +57,7 @@ async def list_instances(
project_names=body.project_names,
fleet_ids=body.fleet_ids,
only_active=body.only_active,
include_imported=body.include_imported,
prev_created_at=body.prev_created_at,
prev_id=body.prev_id,
limit=body.limit,
Expand Down Expand Up @@ -83,12 +89,15 @@ async def get_instance_health_checks(
async def get_instance(
body: GetInstanceRequest,
session: Annotated[AsyncSession, Depends(get_session)],
user_project: Annotated[tuple[UserModel, ProjectModel], Depends(ProjectMember())],
user: Annotated[UserModel, Depends(Authenticated())],
project: Annotated[ProjectModel, Depends(Project())],
):
"""
Returns an instance given its ID.
"""
_, project = user_project
await check_can_access_instance(
session=session, user=user, instance_project=project, instance_id=body.id
)
instance = await instances_services.get_instance(
session=session, project=project, instance_id=body.id
)
Expand Down
Loading