Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: topology applications api #1984

Merged
Show file tree
Hide file tree
Changes from 3 commits
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
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""Topology applications

Revision ID: 01ebe17218c0
Revises: 5d7ae55efc6a
Create Date: 2024-09-22 14:16:17.078591

"""

import sqlalchemy as sa
import sqlmodel
from alembic import op

# revision identifiers, used by Alembic.
revision = "01ebe17218c0"
down_revision = "5d7ae55efc6a"
branch_labels = None
depends_on = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"topologyapplication",
sa.Column("tenant_id", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column("id", sqlmodel.sql.sqltypes.GUID(), nullable=False),
sa.Column("name", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("description", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.ForeignKeyConstraint(
Matvey-Kuk marked this conversation as resolved.
Show resolved Hide resolved
["tenant_id"],
["tenant.id"],
),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"topologyserviceapplication",
sa.Column("service_id", sa.Integer(), nullable=False),
sa.Column("application_id", sqlmodel.sql.sqltypes.GUID(), nullable=False),
sa.ForeignKeyConstraint(
["application_id"],
["topologyapplication.id"],
),
sa.ForeignKeyConstraint(
["service_id"],
["topologyservice.id"],
),
sa.PrimaryKeyConstraint("service_id", "application_id"),
)

with op.batch_alter_table("topologyservice", schema=None) as batch_op:
batch_op.drop_column("application")

# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("topologyservice", schema=None) as batch_op:
batch_op.add_column(sa.Column("application", sa.VARCHAR(), nullable=True))

op.drop_table("topologyserviceapplication")
op.drop_table("topologyapplication")
# ### end Alembic commands ###
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
Kiryous marked this conversation as resolved.
Show resolved Hide resolved
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])
Kiryous marked this conversation as resolved.
Show resolved Hide resolved
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:
Kiryous marked this conversation as resolved.
Show resolved Hide resolved
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)
Kiryous marked this conversation as resolved.
Show resolved Hide resolved
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:
Kiryous marked this conversation as resolved.
Show resolved Hide resolved
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)
Kiryous marked this conversation as resolved.
Show resolved Hide resolved
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:
Kiryous marked this conversation as resolved.
Show resolved Hide resolved
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")
Kiryous marked this conversation as resolved.
Show resolved Hide resolved
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:
Kiryous marked this conversation as resolved.
Show resolved Hide resolved
logger.exception("Failed to delete application")
raise HTTPException(
status_code=400,
detail="Unknown exception when deleting application, please contact us",
)
Loading
Loading