diff --git a/frontend/src/pages/Events/List/hooks/useColumnDefinitions.tsx b/frontend/src/pages/Events/List/hooks/useColumnDefinitions.tsx
index ad337cf5f1..d6e5b846ea 100644
--- a/frontend/src/pages/Events/List/hooks/useColumnDefinitions.tsx
+++ b/frontend/src/pages/Events/List/hooks/useColumnDefinitions.tsx
@@ -125,6 +125,19 @@ export const useColumnsDefinitions = () => {
);
+ case 'gateway':
+ return (
+
+ Gateway{' '}
+ {target.project_name && (
+
+ {target.project_name}
+
+ )}
+ /{target.name}
+
+ );
+
default:
return '---';
}
diff --git a/frontend/src/pages/Events/List/hooks/useFilters.ts b/frontend/src/pages/Events/List/hooks/useFilters.ts
index a3d510718f..d463770b30 100644
--- a/frontend/src/pages/Events/List/hooks/useFilters.ts
+++ b/frontend/src/pages/Events/List/hooks/useFilters.ts
@@ -18,6 +18,7 @@ type RequestParamsKeys = keyof Pick<
| 'target_runs'
| 'target_jobs'
| 'target_volumes'
+ | 'target_gateways'
| 'within_projects'
| 'within_fleets'
| 'within_runs'
@@ -33,6 +34,7 @@ const filterKeys: Record = {
TARGET_RUNS: 'target_runs',
TARGET_JOBS: 'target_jobs',
TARGET_VOLUMES: 'target_volumes',
+ TARGET_GATEWAYS: 'target_gateways',
WITHIN_PROJECTS: 'within_projects',
WITHIN_FLEETS: 'within_fleets',
WITHIN_RUNS: 'within_runs',
@@ -50,6 +52,7 @@ const multipleChoiseKeys: RequestParamsKeys[] = [
'target_runs',
'target_jobs',
'target_volumes',
+ 'target_gateways',
'within_projects',
'within_fleets',
'within_runs',
@@ -65,6 +68,7 @@ const targetTypes = [
{ label: 'Run', value: 'run' },
{ label: 'Job', value: 'job' },
{ label: 'Volume', value: 'volume' },
+ { label: 'Gateway', value: 'gateway' },
];
export const useFilters = () => {
@@ -162,6 +166,11 @@ export const useFilters = () => {
operators: ['='],
propertyLabel: 'Target volumes',
},
+ {
+ key: filterKeys.TARGET_GATEWAYS,
+ operators: ['='],
+ propertyLabel: 'Target gateways',
+ },
{
key: filterKeys.WITHIN_PROJECTS,
diff --git a/frontend/src/types/event.d.ts b/frontend/src/types/event.d.ts
index 3aadfa1f31..618ea6673f 100644
--- a/frontend/src/types/event.d.ts
+++ b/frontend/src/types/event.d.ts
@@ -1,4 +1,4 @@
-declare type TEventTargetType = 'project' | 'user' | 'fleet' | 'instance' | 'run' | 'job' | 'volume';
+declare type TEventTargetType = 'project' | 'user' | 'fleet' | 'instance' | 'run' | 'job' | 'volume' | 'gateway';
declare type TEventListRequestParams = Omit & {
prev_recorded_at?: string;
@@ -9,6 +9,7 @@ declare type TEventListRequestParams = Omit EventListFilters:
api.client.volumes.get(project_name=api.project, name=name).id
for name in args.target_volumes
]
+ elif args.target_gateways:
+ filters.target_gateways = []
+ for name in args.target_gateways:
+ id = api.client.gateways.get(api.project, name).id
+ if id is None:
+ # TODO(0.21): Remove this check once `Gateway.id` is required.
+ raise CLIError(
+ "Cannot determine gateway ID, most likely due to an outdated dstack server."
+ " Update the server to 0.20.7 or higher or remove --target-gateway."
+ )
+ filters.target_gateways.append(id)
if args.within_fleets:
filters.within_fleets = [
diff --git a/src/dstack/_internal/cli/services/events.py b/src/dstack/_internal/cli/services/events.py
index c2903065c9..11f764bd15 100644
--- a/src/dstack/_internal/cli/services/events.py
+++ b/src/dstack/_internal/cli/services/events.py
@@ -17,6 +17,7 @@ class EventListFilters:
target_fleets: Optional[list[uuid.UUID]] = None
target_runs: Optional[list[uuid.UUID]] = None
target_volumes: Optional[list[uuid.UUID]] = None
+ target_gateways: Optional[list[uuid.UUID]] = None
within_projects: Optional[list[uuid.UUID]] = None
within_fleets: Optional[list[uuid.UUID]] = None
within_runs: Optional[list[uuid.UUID]] = None
diff --git a/src/dstack/_internal/core/models/events.py b/src/dstack/_internal/core/models/events.py
index 6dae2dc178..289c4fc674 100644
--- a/src/dstack/_internal/core/models/events.py
+++ b/src/dstack/_internal/core/models/events.py
@@ -17,6 +17,7 @@ class EventTargetType(str, Enum):
RUN = "run"
JOB = "job"
VOLUME = "volume"
+ GATEWAY = "gateway"
class EventTarget(CoreModel):
diff --git a/src/dstack/_internal/core/models/gateways.py b/src/dstack/_internal/core/models/gateways.py
index 2dfeb5b181..b342c0a73b 100644
--- a/src/dstack/_internal/core/models/gateways.py
+++ b/src/dstack/_internal/core/models/gateways.py
@@ -1,4 +1,5 @@
import datetime
+import uuid
from enum import Enum
from typing import Dict, Optional, Union
@@ -93,6 +94,9 @@ class GatewaySpec(CoreModel):
class Gateway(CoreModel):
+ # ID is only optional on the client side for compatibility with pre-0.20.7 servers.
+ # TODO(0.21): Make required.
+ id: Optional[uuid.UUID] = None
name: str
configuration: GatewayConfiguration
created_at: datetime.datetime
diff --git a/src/dstack/_internal/server/routers/events.py b/src/dstack/_internal/server/routers/events.py
index be75cccbb4..4250eb4d7a 100644
--- a/src/dstack/_internal/server/routers/events.py
+++ b/src/dstack/_internal/server/routers/events.py
@@ -45,6 +45,7 @@ async def list_events(
target_runs=body.target_runs,
target_jobs=body.target_jobs,
target_volumes=body.target_volumes,
+ target_gateways=body.target_gateways,
within_projects=body.within_projects,
within_fleets=body.within_fleets,
within_runs=body.within_runs,
diff --git a/src/dstack/_internal/server/schemas/events.py b/src/dstack/_internal/server/schemas/events.py
index 66ea2e3404..30f7fe3244 100644
--- a/src/dstack/_internal/server/schemas/events.py
+++ b/src/dstack/_internal/server/schemas/events.py
@@ -91,6 +91,17 @@ class ListEventsRequest(CoreModel):
max_items=MAX_FILTER_ITEMS,
),
] = None
+ target_gateways: Annotated[
+ Optional[list[uuid.UUID]],
+ Field(
+ description=(
+ "List of gateway IDs."
+ " The response will only include events that target the specified gateways"
+ ),
+ min_items=MIN_FILTER_ITEMS,
+ max_items=MAX_FILTER_ITEMS,
+ ),
+ ] = None
within_projects: Annotated[
Optional[list[uuid.UUID]],
Field(
diff --git a/src/dstack/_internal/server/services/events.py b/src/dstack/_internal/server/services/events.py
index 80d81734b5..c6d35a4577 100644
--- a/src/dstack/_internal/server/services/events.py
+++ b/src/dstack/_internal/server/services/events.py
@@ -14,6 +14,7 @@
EventModel,
EventTargetModel,
FleetModel,
+ GatewayModel,
InstanceModel,
JobModel,
MemberModel,
@@ -87,6 +88,7 @@ def __post_init__(self):
def from_model(
model: Union[
FleetModel,
+ GatewayModel,
InstanceModel,
JobModel,
ProjectModel,
@@ -102,6 +104,13 @@ def from_model(
id=model.id,
name=model.name,
)
+ if isinstance(model, GatewayModel):
+ return Target(
+ type=EventTargetType.GATEWAY,
+ project_id=model.project_id or model.project.id,
+ id=model.id,
+ name=model.name,
+ )
if isinstance(model, InstanceModel):
return Target(
type=EventTargetType.INSTANCE,
@@ -222,6 +231,7 @@ async def list_events(
target_runs: Optional[list[uuid.UUID]],
target_jobs: Optional[list[uuid.UUID]],
target_volumes: Optional[list[uuid.UUID]],
+ target_gateways: Optional[list[uuid.UUID]],
within_projects: Optional[list[uuid.UUID]],
within_fleets: Optional[list[uuid.UUID]],
within_runs: Optional[list[uuid.UUID]],
@@ -298,6 +308,13 @@ async def list_events(
EventTargetModel.entity_id.in_(target_volumes),
)
)
+ if target_gateways is not None:
+ target_filters.append(
+ and_(
+ EventTargetModel.entity_type == EventTargetType.GATEWAY,
+ EventTargetModel.entity_id.in_(target_gateways),
+ )
+ )
if within_projects is not None:
target_filters.append(EventTargetModel.entity_project_id.in_(within_projects))
if within_fleets is not None:
diff --git a/src/dstack/_internal/server/services/gateways/__init__.py b/src/dstack/_internal/server/services/gateways/__init__.py
index 4ab80a8331..cf41b53973 100644
--- a/src/dstack/_internal/server/services/gateways/__init__.py
+++ b/src/dstack/_internal/server/services/gateways/__init__.py
@@ -558,6 +558,7 @@ def gateway_model_to_gateway(gateway_model: GatewayModel) -> Gateway:
configuration = get_gateway_configuration(gateway_model)
configuration.default = gateway_model.project.default_gateway_id == gateway_model.id
return Gateway(
+ id=gateway_model.id,
name=gateway_model.name,
ip_address=ip_address,
instance_id=instance_id,
diff --git a/src/dstack/api/server/_events.py b/src/dstack/api/server/_events.py
index d9bf828394..d403fb2427 100644
--- a/src/dstack/api/server/_events.py
+++ b/src/dstack/api/server/_events.py
@@ -30,6 +30,7 @@ def list(
*,
# NOTE: New parameters go here. Avoid positional parameters, they can break compatibility.
target_volumes: Optional[list[UUID]] = None,
+ target_gateways: Optional[list[UUID]] = None,
) -> list[Event]:
if prev_recorded_at is not None:
# Time zones other than UTC are misinterpreted by the server:
@@ -43,6 +44,7 @@ def list(
target_runs=target_runs,
target_jobs=target_jobs,
target_volumes=target_volumes,
+ target_gateways=target_gateways,
within_projects=within_projects,
within_fleets=within_fleets,
within_runs=within_runs,
diff --git a/src/tests/_internal/server/routers/test_gateways.py b/src/tests/_internal/server/routers/test_gateways.py
index b909c7d729..70f6b22b7e 100644
--- a/src/tests/_internal/server/routers/test_gateways.py
+++ b/src/tests/_internal/server/routers/test_gateways.py
@@ -17,6 +17,7 @@
create_user,
get_auth_headers,
)
+from dstack._internal.server.testing.matchers import SomeUUID4Str
class TestListAndGetGateways:
@@ -54,6 +55,7 @@ async def test_list(self, test_db, session: AsyncSession, client: AsyncClient):
assert response.status_code == 200
assert response.json() == [
{
+ "id": SomeUUID4Str(),
"backend": backend.type.value,
"created_at": response.json()[0]["created_at"],
"default": False,
@@ -107,6 +109,7 @@ async def test_get(self, test_db, session: AsyncSession, client: AsyncClient):
)
assert response.status_code == 200
assert response.json() == {
+ "id": SomeUUID4Str(),
"backend": backend.type.value,
"created_at": response.json()["created_at"],
"default": False,
@@ -189,6 +192,7 @@ async def test_create_gateway(self, test_db, session: AsyncSession, client: Asyn
)
assert response.status_code == 200
assert response.json() == {
+ "id": SomeUUID4Str(),
"name": "test",
"backend": "aws",
"region": "us",
@@ -243,6 +247,7 @@ async def test_create_gateway_without_name(
g.assert_called_once()
assert response.status_code == 200
assert response.json() == {
+ "id": SomeUUID4Str(),
"name": "random-name",
"backend": "aws",
"region": "us",
@@ -347,6 +352,7 @@ async def test_set_default_gateway(self, test_db, session: AsyncSession, client:
)
assert response.status_code == 200
assert response.json() == {
+ "id": SomeUUID4Str(),
"backend": backend.type.value,
"created_at": response.json()["created_at"],
"default": True,
@@ -471,6 +477,7 @@ def get_backend(project, backend_type):
assert response.status_code == 200
assert response.json() == [
{
+ "id": str(gateway_gcp.id),
"backend": backend_gcp.type.value,
"created_at": response.json()[0]["created_at"],
"default": False,
@@ -542,6 +549,7 @@ async def test_set_wildcard_domain(self, test_db, session: AsyncSession, client:
)
assert response.status_code == 200
assert response.json() == {
+ "id": SomeUUID4Str(),
"backend": backend.type.value,
"created_at": response.json()["created_at"],
"status": "submitted",