Skip to content

Commit 13c67fd

Browse files
authored
Show a CLI warning when using autocreated fleets (#3060)
* Add run.fleet to API responses * Show warning when using autocreated fleets * Update warning text
1 parent 65771dd commit 13c67fd

File tree

7 files changed

+59
-0
lines changed

7 files changed

+59
-0
lines changed

src/dstack/_internal/cli/services/configurators/run.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
from dstack._internal.utils.path import is_absolute_posix_path
5858
from dstack.api._public.repos import get_ssh_keypair
5959
from dstack.api._public.runs import Run
60+
from dstack.api.server import APIClient
6061
from dstack.api.utils import load_profile
6162

6263
_KNOWN_AMD_GPUS = {gpu.name.lower() for gpu in gpuhunt.KNOWN_AMD_GPUS}
@@ -229,6 +230,9 @@ def apply_configuration(
229230
format_date=local_time,
230231
)
231232
)
233+
234+
_warn_fleet_autocreated(self.api.client, run)
235+
232236
console.print(
233237
f"\n[code]{run.name}[/] provisioning completed [secondary]({run.status.value})[/]"
234238
)
@@ -872,3 +876,16 @@ def render_run_spec_diff(old_spec: RunSpec, new_spec: RunSpec) -> Optional[str]:
872876
item = NestedListItem(spec_field.replace("_", " ").capitalize())
873877
nested_list.children.append(item)
874878
return nested_list.render()
879+
880+
881+
def _warn_fleet_autocreated(api: APIClient, run: Run):
882+
if run._run.fleet is None:
883+
return
884+
fleet = api.fleets.get(project_name=run._project, name=run._run.fleet.name)
885+
if not fleet.spec.autocreated:
886+
return
887+
warn(
888+
f"\nNo existing fleet matched, so the run created a new fleet [code]{fleet.name}[/code].\n"
889+
"Future dstack versions won't create fleets automatically.\n"
890+
"Create a fleet explicitly: https://dstack.ai/docs/concepts/fleets/"
891+
)

src/dstack/_internal/core/models/runs.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -519,10 +519,16 @@ def is_finished(self):
519519
return self in self.finished_statuses()
520520

521521

522+
class RunFleet(CoreModel):
523+
id: UUID4
524+
name: str
525+
526+
522527
class Run(CoreModel):
523528
id: UUID4
524529
project_name: str
525530
user: str
531+
fleet: Optional[RunFleet] = None
526532
submitted_at: datetime
527533
last_processed_at: datetime
528534
status: RunStatus

src/dstack/_internal/server/background/tasks/process_running_jobs.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
from dstack._internal.server.background.tasks.common import get_provisioning_timeout
4242
from dstack._internal.server.db import get_db, get_session_ctx
4343
from dstack._internal.server.models import (
44+
FleetModel,
4445
InstanceModel,
4546
JobModel,
4647
ProbeModel,
@@ -151,6 +152,7 @@ async def _process_running_job(session: AsyncSession, job_model: JobModel):
151152
.options(joinedload(RunModel.project))
152153
.options(joinedload(RunModel.user))
153154
.options(joinedload(RunModel.repo))
155+
.options(joinedload(RunModel.fleet).load_only(FleetModel.id, FleetModel.name))
154156
.options(joinedload(RunModel.jobs))
155157
)
156158
run_model = res.unique().scalar_one()

src/dstack/_internal/server/background/tasks/process_runs.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
)
2222
from dstack._internal.server.db import get_db, get_session_ctx
2323
from dstack._internal.server.models import (
24+
FleetModel,
2425
InstanceModel,
2526
JobModel,
2627
ProjectModel,
@@ -145,6 +146,7 @@ async def _process_run(session: AsyncSession, run_model: RunModel):
145146
.execution_options(populate_existing=True)
146147
.options(joinedload(RunModel.project).load_only(ProjectModel.id, ProjectModel.name))
147148
.options(joinedload(RunModel.user).load_only(UserModel.name))
149+
.options(joinedload(RunModel.fleet).load_only(FleetModel.id, FleetModel.name))
148150
.options(
149151
selectinload(RunModel.jobs)
150152
.joinedload(JobModel.instance)

src/dstack/_internal/server/services/runs.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
JobTerminationReason,
4444
ProbeSpec,
4545
Run,
46+
RunFleet,
4647
RunPlan,
4748
RunSpec,
4849
RunStatus,
@@ -58,6 +59,7 @@
5859
from dstack._internal.server import settings
5960
from dstack._internal.server.db import get_db
6061
from dstack._internal.server.models import (
62+
FleetModel,
6163
JobModel,
6264
ProbeModel,
6365
ProjectModel,
@@ -227,6 +229,7 @@ async def list_projects_run_models(
227229
select(RunModel)
228230
.where(*filters)
229231
.options(joinedload(RunModel.user).load_only(UserModel.name))
232+
.options(joinedload(RunModel.fleet).load_only(FleetModel.id, FleetModel.name))
230233
.options(selectinload(RunModel.jobs).joinedload(JobModel.probes))
231234
.order_by(*order_by)
232235
.limit(limit)
@@ -269,6 +272,7 @@ async def get_run_by_name(
269272
RunModel.deleted == False,
270273
)
271274
.options(joinedload(RunModel.user))
275+
.options(joinedload(RunModel.fleet).load_only(FleetModel.id, FleetModel.name))
272276
.options(selectinload(RunModel.jobs).joinedload(JobModel.probes))
273277
)
274278
run_model = res.scalar()
@@ -289,6 +293,7 @@ async def get_run_by_id(
289293
RunModel.id == run_id,
290294
)
291295
.options(joinedload(RunModel.user))
296+
.options(joinedload(RunModel.fleet).load_only(FleetModel.id, FleetModel.name))
292297
.options(selectinload(RunModel.jobs).joinedload(JobModel.probes))
293298
)
294299
run_model = res.scalar()
@@ -709,10 +714,12 @@ def run_model_to_run(
709714

710715
status_message = _get_run_status_message(run_model)
711716
error = _get_run_error(run_model)
717+
fleet = _get_run_fleet(run_model)
712718
run = Run(
713719
id=run_model.id,
714720
project_name=run_model.project.name,
715721
user=run_model.user.name,
722+
fleet=fleet,
716723
submitted_at=run_model.submitted_at,
717724
last_processed_at=run_model.last_processed_at,
718725
status=run_model.status,
@@ -821,6 +828,15 @@ def _get_run_error(run_model: RunModel) -> Optional[str]:
821828
return run_model.termination_reason.to_error()
822829

823830

831+
def _get_run_fleet(run_model: RunModel) -> Optional[RunFleet]:
832+
if run_model.fleet is None:
833+
return None
834+
return RunFleet(
835+
id=run_model.fleet.id,
836+
name=run_model.fleet.name,
837+
)
838+
839+
824840
async def _get_pool_offers(
825841
session: AsyncSession,
826842
project: ProjectModel,

src/dstack/_internal/server/testing/common.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,7 @@ async def create_run(
285285
project: ProjectModel,
286286
repo: RepoModel,
287287
user: UserModel,
288+
fleet: Optional[FleetModel] = None,
288289
run_name: str = "test-run",
289290
status: RunStatus = RunStatus.SUBMITTED,
290291
termination_reason: Optional[RunTerminationReason] = None,
@@ -310,6 +311,7 @@ async def create_run(
310311
project_id=project.id,
311312
repo_id=repo.id,
312313
user_id=user.id,
314+
fleet_id=fleet.id if fleet else None,
313315
submitted_at=submitted_at,
314316
run_name=run_name,
315317
status=status,

src/tests/_internal/server/routers/test_runs.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
from dstack._internal.server.services.runs import run_model_to_run
5050
from dstack._internal.server.testing.common import (
5151
create_backend,
52+
create_fleet,
5253
create_gateway,
5354
create_gateway_compute,
5455
create_instance,
@@ -337,6 +338,7 @@ def get_dev_env_run_dict(
337338
"id": run_id,
338339
"project_name": project_name,
339340
"user": username,
341+
"fleet": None,
340342
"submitted_at": submitted_at,
341343
"last_processed_at": last_processed_at,
342344
"status": "submitted",
@@ -558,6 +560,7 @@ async def test_returns_40x_if_not_authenticated(
558560
async def test_lists_runs(self, test_db, session: AsyncSession, client: AsyncClient):
559561
user = await create_user(session=session, global_role=GlobalRole.USER)
560562
project = await create_project(session=session, owner=user)
563+
fleet = await create_fleet(session=session, project=project)
561564
await add_project_member(
562565
session=session, project=project, user=user, project_role=ProjectRole.USER
563566
)
@@ -571,6 +574,7 @@ async def test_lists_runs(self, test_db, session: AsyncSession, client: AsyncCli
571574
project=project,
572575
repo=repo,
573576
user=user,
577+
fleet=fleet,
574578
submitted_at=run1_submitted_at,
575579
)
576580
run1_spec = RunSpec.parse_raw(run1.run_spec)
@@ -587,6 +591,7 @@ async def test_lists_runs(self, test_db, session: AsyncSession, client: AsyncCli
587591
project=project,
588592
repo=repo,
589593
user=user,
594+
fleet=fleet,
590595
submitted_at=run2_submitted_at,
591596
)
592597
run2_spec = RunSpec.parse_raw(run2.run_spec)
@@ -601,6 +606,10 @@ async def test_lists_runs(self, test_db, session: AsyncSession, client: AsyncCli
601606
"id": str(run1.id),
602607
"project_name": project.name,
603608
"user": user.name,
609+
"fleet": {
610+
"id": str(fleet.id),
611+
"name": fleet.name,
612+
},
604613
"submitted_at": run1_submitted_at.isoformat(),
605614
"last_processed_at": run1_submitted_at.isoformat(),
606615
"status": "submitted",
@@ -660,6 +669,10 @@ async def test_lists_runs(self, test_db, session: AsyncSession, client: AsyncCli
660669
"id": str(run2.id),
661670
"project_name": project.name,
662671
"user": user.name,
672+
"fleet": {
673+
"id": str(fleet.id),
674+
"name": fleet.name,
675+
},
663676
"submitted_at": run2_submitted_at.isoformat(),
664677
"last_processed_at": run2_submitted_at.isoformat(),
665678
"status": "submitted",
@@ -784,6 +797,7 @@ async def test_limits_job_submissions(
784797
"id": str(run.id),
785798
"project_name": project.name,
786799
"user": user.name,
800+
"fleet": None,
787801
"submitted_at": run_submitted_at.isoformat(),
788802
"last_processed_at": run_submitted_at.isoformat(),
789803
"status": "submitted",

0 commit comments

Comments
 (0)