Skip to content

Commit

Permalink
feature: create topologies service, move existing method there and ad…
Browse files Browse the repository at this point in the history
…d new methods to handle TopologyApplications
  • Loading branch information
Kiryous committed Sep 22, 2024
1 parent c41f345 commit bbcd431
Show file tree
Hide file tree
Showing 4 changed files with 331 additions and 64 deletions.
49 changes: 0 additions & 49 deletions keep/api/core/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -2782,55 +2782,6 @@ def update_incident_name(tenant_id: str, incident_id: UUID, name: str) -> Incide
return incident


# Fetch all topology data
def get_all_topology_data(
tenant_id: str,
provider_id: Optional[str] = None,
service: Optional[str] = None,
environment: Optional[str] = None,
) -> List[TopologyServiceDtoOut]:
with Session(engine) as session:
query = select(TopologyService).where(TopologyService.tenant_id == tenant_id)

# @tb: let's filter by service only for now and take care of it when we handle multilpe
# services and environments and cmdbs
# the idea is that we show the service topology regardless of the underlying provider/env
# if provider_id is not None and service is not None and environment is not None:
if service is not None:
query = query.where(
TopologyService.service == service,
# TopologyService.source_provider_id == provider_id,
# TopologyService.environment == environment,
)

service_instance = session.exec(query).first()
if not service_instance:
return []

services = session.exec(
select(TopologyServiceDependency)
.where(
TopologyServiceDependency.depends_on_service_id
== service_instance.id
)
.options(joinedload(TopologyServiceDependency.service))
).all()
services = [service_instance, *[service.service for service in services]]
else:
# Fetch services for the tenant
services = session.exec(
query.options(
selectinload(TopologyService.dependencies).selectinload(
TopologyServiceDependency.dependent_service
)
)
).all()

service_dtos = [TopologyServiceDtoOut.from_orm(service) for service in services]

return service_dtos


def get_topology_data_by_dynamic_matcher(
tenant_id: str, matchers_value: dict[str, str]
) -> TopologyService | None:
Expand Down
68 changes: 63 additions & 5 deletions keep/api/models/db/topology.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
from datetime import datetime
from typing import List, Optional
from uuid import UUID, uuid4

from pydantic import BaseModel
from sqlalchemy import DateTime, ForeignKey
from sqlmodel import JSON, Column, Field, Relationship, SQLModel, func

class TopologyServiceApplication(SQLModel, table=True):
service_id: int = Field(foreign_key="topologyservice.id", primary_key=True)
application_id: UUID = Field(foreign_key="topologyapplication.id", primary_key=True)

class TopologyApplication(SQLModel, table=True):
id: UUID = Field(default_factory=uuid4, primary_key=True)
tenant_id: str = Field(sa_column=Column(ForeignKey("tenant.id")))
name: str
description: Optional[str] = None
services: List["TopologyService"] = Relationship(
back_populates="applications",
link_model=TopologyServiceApplication
)

class TopologyService(SQLModel, table=True):
id: Optional[int] = Field(primary_key=True, default=None)
Expand All @@ -17,7 +31,6 @@ class TopologyService(SQLModel, table=True):
display_name: str
description: Optional[str]
team: Optional[str]
application: Optional[str]
email: Optional[str]
slack: Optional[str]
ip_address: Optional[str] = None
Expand All @@ -41,11 +54,15 @@ class TopologyService(SQLModel, table=True):
},
)

applications: List[TopologyApplication] = Relationship(
back_populates="services",
link_model=TopologyServiceApplication
)

class Config:
orm_mode = True
unique_together = ["tenant_id", "service", "environment", "source_provider_id"]


class TopologyServiceDependency(SQLModel, table=True):
id: Optional[int] = Field(primary_key=True, default=None)
service_id: int = Field(
Expand Down Expand Up @@ -86,7 +103,6 @@ class TopologyServiceDtoBase(BaseModel, extra="ignore"):
environment: str = "unknown"
description: Optional[str] = None
team: Optional[str] = None
application: Optional[str] = None
email: Optional[str] = None
slack: Optional[str] = None
ip_address: Optional[str] = None
Expand All @@ -105,13 +121,55 @@ class TopologyServiceDependencyDto(BaseModel, extra="ignore"):
protocol: Optional[str] = "unknown"


class TopologyApplicationDto(BaseModel, extra="ignore"):
id: UUID
name: str
description: Optional[str] = None
services: List[TopologyService] = Relationship(
back_populates="applications",
link_model="TopologyServiceApplication"
)


class TopologyServiceDtoIn(BaseModel, extra="ignore"):
id: int


class TopologyApplicationDtoIn(BaseModel, extra="ignore"):
id: Optional[UUID] = None
name: str
description: Optional[str] = None
services: List[TopologyServiceDtoIn] = []


class TopologyApplicationServiceDto(BaseModel, extra="ignore"):
id: int
name: str
service: str

class TopologyApplicationDtoOut(TopologyApplicationDto):
services: List[TopologyApplicationServiceDto] = []

@classmethod
def from_orm(cls, application: "TopologyApplication") -> "TopologyApplicationDtoOut":
return cls(
id=application.id,
name=application.name,
description=application.description,
services=[
TopologyApplicationServiceDto(id=service.id, name=service.display_name, service=service.service) for service in application.services if service.id is not None
]
)


class TopologyServiceDtoOut(TopologyServiceDtoBase):
id: int
dependencies: List[TopologyServiceDependencyDto]
application_ids: List[UUID]
updated_at: Optional[datetime]

@classmethod
def from_orm(cls, service: "TopologyService") -> "TopologyServiceDtoOut":
def from_orm(cls, service: "TopologyService", application_ids: List[UUID]) -> "TopologyServiceDtoOut":
return cls(
id=service.id,
source_provider_id=service.source_provider_id,
Expand All @@ -122,7 +180,6 @@ def from_orm(cls, service: "TopologyService") -> "TopologyServiceDtoOut":
environment=service.environment,
description=service.description,
team=service.team,
application=service.application,
email=service.email,
slack=service.slack,
ip_address=service.ip_address,
Expand All @@ -137,5 +194,6 @@ def from_orm(cls, service: "TopologyService") -> "TopologyServiceDtoOut":
)
for dep in service.dependencies
],
application_ids=application_ids,
updated_at=service.updated_at,
)
96 changes: 86 additions & 10 deletions keep/api/routes/topology.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@
from typing import List, Optional

from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import JSONResponse
from sqlmodel import Session

from keep.api.core.db import ( # Assuming this function exists to fetch topology data
get_all_topology_data,
)
from keep.api.models.db.topology import TopologyServiceDtoOut
from keep.api.core.db import get_session
from keep.api.models.db.topology import TopologyApplicationDtoIn, TopologyApplicationDtoOut, TopologyServiceDtoOut
from keep.identitymanager.authenticatedentity import AuthenticatedEntity
from keep.identitymanager.identitymanagerfactory import IdentityManagerFactory
from keep.topologies.topologies_service import TopologiesService

logger = logging.getLogger(__name__)
router = APIRouter()
Expand All @@ -26,6 +27,7 @@ def get_topology_data(
authenticated_entity: AuthenticatedEntity = Depends(
IdentityManagerFactory.get_auth_verifier(["read:topology"])
),
session: Session = Depends(get_session),
) -> List[TopologyServiceDtoOut]:
tenant_id = authenticated_entity.tenant_id
logger.info("Getting topology data", extra={tenant_id: tenant_id})
Expand All @@ -41,17 +43,91 @@ def get_topology_data(
# )

try:
topology_data = get_all_topology_data(
tenant_id, provider_id, service_id, environment
topology_data = TopologiesService.get_all_topology_data(
tenant_id, session, provider_id, service_id, environment, includeEmptyDeps
)
if not includeEmptyDeps:
topology_data = [
topology for topology in topology_data if topology.dependencies
]
return topology_data
except Exception:
logger.exception("Failed to get topology data")
raise HTTPException(
status_code=400,
detail="Unknown exception when getting topology data, please contact us",
)


@router.get("/applications", description="Get all applications", response_model=List[TopologyApplicationDtoOut])
def get_applications(
authenticated_entity: AuthenticatedEntity = Depends(
IdentityManagerFactory.get_auth_verifier(["read:topology"])
),
session: Session = Depends(get_session),
) -> List[TopologyApplicationDtoOut]:
tenant_id = authenticated_entity.tenant_id
logger.info("Getting applications", extra={"tenant_id": tenant_id})
try:
return TopologiesService.get_applications_by_tenant_id(tenant_id, session)
except Exception as e:
logger.exception(f"Failed to get applications: {str(e)}")
raise HTTPException(
status_code=500,
detail="Unknown exception when getting applications, please contact us",
)

@router.post("/applications", description="Create a new application", response_model=TopologyApplicationDtoOut)
def create_application(
application: TopologyApplicationDtoIn,
authenticated_entity: AuthenticatedEntity = Depends(
IdentityManagerFactory.get_auth_verifier(["write:topology"])
),
session: Session = Depends(get_session),
) -> TopologyApplicationDtoOut:
tenant_id = authenticated_entity.tenant_id
logger.info("Creating application", extra={tenant_id: tenant_id})
try:
return TopologiesService.create_application_by_tenant_id(tenant_id, application, session)
except Exception:
logger.exception("Failed to create application")
raise HTTPException(
status_code=400,
detail="Unknown exception when creating application, please contact us",
)

@router.put("/applications/{application_id}", description="Update an application", response_model=TopologyApplicationDtoOut)
def update_application(
application_id: str,
application: TopologyApplicationDtoIn,
authenticated_entity: AuthenticatedEntity = Depends(
IdentityManagerFactory.get_auth_verifier(["write:topology"])
),
session: Session = Depends(get_session),
) -> TopologyApplicationDtoOut:
tenant_id = authenticated_entity.tenant_id
logger.info("Updating application", extra={tenant_id: tenant_id})
try:
return TopologiesService.update_application_by_id(tenant_id, application_id, application, session)
except Exception:
logger.exception("Failed to update application")
raise HTTPException(
status_code=400,
detail="Unknown exception when updating application, please contact us",
)

@router.delete("/applications/{application_id}", description="Delete an application")
def delete_application(
application_id: str,
authenticated_entity: AuthenticatedEntity = Depends(
IdentityManagerFactory.get_auth_verifier(["write:topology"])
),
session: Session = Depends(get_session),
):
tenant_id = authenticated_entity.tenant_id
logger.info("Deleting application", extra={tenant_id: tenant_id})
try:
TopologiesService.delete_application_by_id(tenant_id, application_id, session)
return JSONResponse(status_code=200, content={"message": "Application deleted successfully"})
except Exception:
logger.exception("Failed to delete application")
raise HTTPException(
status_code=400,
detail="Unknown exception when deleting application, please contact us",
)
Loading

0 comments on commit bbcd431

Please sign in to comment.