Skip to content

Commit

Permalink
Merge pull request #6 from rajeshj11/feat-1780-runbooks-intergation
Browse files Browse the repository at this point in the history
Feat 1780 runbooks intergation
  • Loading branch information
Mubashirshariq authored Oct 8, 2024
2 parents 6138c03 + 9f1fabe commit 99c0226
Show file tree
Hide file tree
Showing 10 changed files with 265 additions and 110 deletions.
1 change: 1 addition & 0 deletions keep-ui/app/runbooks/runbook-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ function SettingsPage() {
<TextInput
{...register("pathToMdFile")}
placeholder="Enter path to markdown files"
required
/>
</div>
</div>
Expand Down
27 changes: 20 additions & 7 deletions keep-ui/utils/hooks/useRunbook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const useRunBookTriggers = (values: any, refresh: number) => {
const [synced, setSynced] = useState(false);
const [fileData, setFileData] = useState<any>({});
const [reposData, setRepoData] = useState<any>([]);
const { pathToMdFile, repoName, userName, providerId, domain } = values || {};
const { pathToMdFile, repoName, userName, providerId, domain, runBookTitle } = values || {};
const { data: session } = useSession();
const { installed_providers, providers } = (providersData?.data ||
{}) as ProvidersResponse;
Expand Down Expand Up @@ -67,18 +67,31 @@ export const useRunBookTriggers = (values: any, refresh: number) => {
if (repoName) {
params.append("repo", repoName);
}
if(runBookTitle){
params.append("title", runBookTitle);

}
//TO DO backend runbook records needs to be created.
const response = await fetcher(
`${baseApiurl}/runbooks/${provider?.type}/${
const response = await fetch(`${baseApiurl}/runbooks/${provider?.type}/${
provider?.id
}/runbook?${params.toString()}`,
session?.accessToken
);
}?${params.toString()}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${session?.accessToken}`,
},
});

if (!response) {
return setError("Something went wrong. try agian after some time");
}
setFileData(response);

if(!response.ok) {
return setError("Something went wrong. try agian after some time");

}
const result = await response.json();
setFileData(result);
setSynced(false);
} catch (err) {
return setError("Something went wrong. try agian after some time");
Expand Down
1 change: 1 addition & 0 deletions keep/api/models/db/migrations/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from keep.api.models.db.topology import *
from keep.api.models.db.user import *
from keep.api.models.db.workflow import *
from keep.api.models.db.runbook import *

target_metadata = SQLModel.metadata

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""add runbook table
Revision ID: 8902e1a17f66
Revises: 01ebe17218c0
Create Date: 2024-10-08 16:09:28.158034
"""

import sqlalchemy as sa
import sqlalchemy_utils
import sqlmodel
from alembic import op
from sqlalchemy.dialects import postgresql

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


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"runbook",
sa.Column("tenant_id", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column("relative_path", sa.Text(), nullable=True),
sa.Column("title", sa.Text(), nullable=True),
sa.Column("id", sqlmodel.sql.sqltypes.GUID(), nullable=False),
sa.Column("repo_id", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("provider_type", sqlmodel.sql.sqltypes.AutoString(), nullable=False),
sa.Column("provider_id", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(["tenant_id"], ["tenant.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
)
op.create_table(
"runbookcontent",
sa.Column("runbook_id", sqlmodel.sql.sqltypes.GUID(), nullable=True),
sa.Column("content", sa.Text(), nullable=True),
sa.Column("link", sa.Text(), nullable=True),
sa.Column("id", sqlmodel.sql.sqltypes.GUID(), nullable=False),
sa.Column("encoding", sqlmodel.sql.sqltypes.AutoString(), nullable=True),
sa.Column("created_at", sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(["runbook_id"], ["runbook.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
)
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("runbookcontent")
op.drop_table("runbook")
# ### end Alembic commands ###
143 changes: 66 additions & 77 deletions keep/api/models/db/runbook.py
Original file line number Diff line number Diff line change
@@ -1,93 +1,82 @@
from uuid import UUID, uuid4
from datetime import datetime
from typing import List, Optional
from uuid import UUID, uuid4

from sqlalchemy import ForeignKey, Column, TEXT, JSON
from sqlmodel import Field, Relationship, SQLModel
from keep.api.models.db.tenant import Tenant
from sqlalchemy import Column, ForeignKey, Text
from pydantic import BaseModel

# Link Model between Runbook and Incident
class RunbookToIncident(SQLModel, table=True):
tenant_id: str = Field(foreign_key="tenant.id")
runbook_id: UUID = Field(foreign_key="runbook.id", primary_key=True)
incident_id: UUID = Field(foreign_key="incident.id", primary_key=True)

incident_id: UUID = Field(
sa_column=Column(
UUID(binary=False),
ForeignKey("incident.id", ondelete="CASCADE"),
primary_key=True,
)
# RunbookContent Model
class RunbookContent(SQLModel, table=True):
id: UUID = Field(default_factory=uuid4, primary_key=True)
runbook_id: UUID = Field(
sa_column=Column(ForeignKey("runbook.id", ondelete="CASCADE")) # Foreign key with CASCADE delete
)
runbook: Optional["Runbook"] = Relationship(back_populates="contents")
content: str = Field(sa_column=Column(Text), nullable=False) # Using SQLAlchemy's Text type
link: str = Field(sa_column=Column(Text), nullable=False) # Using SQLAlchemy's Text type
encoding: Optional[str] = None
created_at: datetime = Field(default_factory=datetime.utcnow) # Timestamp for creation

class Config:
orm_mode = True

# Runbook Model
class Runbook(SQLModel, table=True):
id: UUID = Field(default_factory=uuid4, primary_key=True)
tenant_id: str = Field(foreign_key="tenant.id")
tenant: Tenant = Relationship()
repo_id: str = Field(nullable=False) # Github repo id
relative_path: str = Field(nullable=False) # Relative path to the .md file
title: str = Field(nullable=False) # Title of the runbook
link: str = Field(nullable=False) # Link to the .md file

incidents: List["Incident"] = Relationship(
back_populates="runbooks", link_model=RunbookToIncident
tenant_id: str = Field(
sa_column=Column(ForeignKey("tenant.id", ondelete="CASCADE")) # Foreign key with CASCADE delete
)
provider_type: str
provider_id: Optional[str] = None
created_at: datetime = Field(default_factory=datetime.utcnow)
repo_id: str # Repository ID
relative_path: str = Field(sa_column=Column(Text), nullable=False) # Path in the repo, must be set
title: str = Field(sa_column=Column(Text), nullable=False) # Title of the runbook, must be set
contents: List["RunbookContent"] = Relationship(back_populates="runbook") # Relationship to RunbookContent
provider_type: str # Type of the provider
provider_id: Optional[str] = None # Optional provider ID
created_at: datetime = Field(default_factory=datetime.utcnow) # Timestamp for creation

class Config:
arbitrary_types_allowed = True


# Incident Model
class Incident(SQLModel, table=True):
id: UUID = Field(default_factory=uuid4, primary_key=True)
tenant_id: str = Field(foreign_key="tenant.id")
tenant: Tenant = Relationship()

user_generated_name: Optional[str] = None
ai_generated_name: Optional[str] = None

user_summary: Optional[str] = Field(sa_column=Column(TEXT), nullable=True)
generated_summary: Optional[str] = Field(sa_column=Column(TEXT), nullable=True)

assignee: Optional[str] = None
# severity: int = Field(default=IncidentSeverity.CRITICAL.order)

creation_time: datetime = Field(default_factory=datetime.utcnow)

start_time: Optional[datetime] = None
end_time: Optional[datetime] = None
last_seen_time: Optional[datetime] = None

runbooks: List["Runbook"] = Relationship(
back_populates="incidents", link_model=RunbookToIncident
)

is_predicted: bool = Field(default=False)
is_confirmed: bool = Field(default=False)

alerts_count: int = Field(default=0)
affected_services: List = Field(sa_column=Column(JSON), default_factory=list)
sources: List = Field(sa_column=Column(JSON), default_factory=list)

rule_id: Optional[UUID] = Field(
sa_column=Column(
UUID(binary=False),
ForeignKey("rule.id", ondelete="CASCADE"),
nullable=True,
),
)

rule_fingerprint: str = Field(default="", sa_column=Column(TEXT))
orm_mode = True # Enable ORM mode for compatibility with Pydantic models


class RunbookDto(BaseModel, extra="ignore"):
id: UUID
tenant_id: str
repo_id: str
relative_path: str
title: str
contents: List["RunbookContent"] = []
provider_type: str
provider_id: Optional[str] = None

def __init__(self, **kwargs):
super().__init__(**kwargs)
if "runbooks" not in kwargs:
self.runbooks = []
class RunbookContentDto(BaseModel, extra="ignore"):
id: UUID
content: str
link: str
encoding: Optional[str] = None

@classmethod
def from_orm(cls, content: "RunbookContent") -> "RunbookContentDto":
return cls(
id=content.id,
content=content.content,
link=content.link,
encoding=content.encoding
)

class Config:
arbitrary_types_allowed = True
class RunbookDtoOut(RunbookDto):
contents: List[RunbookContentDto] = []
@classmethod
def from_orm(
cls, runbook: "Runbook"
) -> "RunbookDtoOut":
return cls(
id=runbook.id,
title=runbook.title,
tenant_id=runbook.tenant_id,
repo_id=runbook.repo_id,
relative_path=runbook.relative_path,
provider_type=runbook.provider_type,
provider_id=runbook.provider_id,
contents=[RunbookContentDto.from_orm(content) for content in runbook.contents]
)
47 changes: 44 additions & 3 deletions keep/api/routes/runbooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@
from keep.identitymanager.identitymanagerfactory import IdentityManagerFactory
from keep.providers.providers_factory import ProvidersFactory
from keep.secretmanager.secretmanagerfactory import SecretManagerFactory
from keep.runbooks.runbooks_service import (
RunbookService
)
from keep.api.models.db.runbook import (
RunbookDtoOut
)


logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -45,8 +52,8 @@ def get_repositories(
return provider.pull_repositories()


@router.get("/{provider_type}/{provider_id}/runbook")
def get_repositories(
@router.get("/{provider_type}/{provider_id}")
def get_runbook(
provider_type: str,
provider_id: str,
authenticated_entity: AuthenticatedEntity = Depends(
Expand All @@ -56,6 +63,7 @@ def get_repositories(
repo: str = Query(None),
branch: str = Query(None),
md_path: str = Query(None),
title: str = Query(None),
):
tenant_id = authenticated_entity.tenant_id
logger.info("Getting runbook", extra={"provider_type": provider_type, "provider_id": provider_id})
Expand All @@ -76,4 +84,37 @@ def get_repositories(
context_manager, provider_id, provider_type, provider_config
)

return provider.pull_runbook(repo=repo, branch=branch, md_path=md_path)
return provider.pull_runbook(repo=repo, branch=branch, md_path=md_path, title=title)


@router.post(
"/{provider_type}/{provider_id}",
description="Create a new Runbook",
# response_model=RunbookDtoOut,
)
def create_runbook(
provider_type: str,
provider_id: str,
authenticated_entity: AuthenticatedEntity = Depends(
IdentityManagerFactory.get_auth_verifier(["write:runbook"])
),
session: Session = Depends(get_session),
repo: str = Query(None),
branch: str = Query(None),
md_path: str = Query(None),
title: str = Query(None),
):
tenant_id = authenticated_entity.tenant_id
logger.info("Creating Runbook", extra={tenant_id: tenant_id})
context_manager = ContextManager(tenant_id=tenant_id)
secret_manager = SecretManagerFactory.get_secret_manager(context_manager)
provider_config = secret_manager.read_secret(
f"{tenant_id}_{provider_type}_{provider_id}", is_json=True
)
provider = ProvidersFactory.get_provider(
context_manager, provider_id, provider_type, provider_config
)

runbook_dto= provider.pull_runbook(repo=repo, branch=branch, md_path=md_path, title=title)
return RunbookService.create_runbook(session, tenant_id, runbook_dto)

2 changes: 1 addition & 1 deletion keep/providers/base/base_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -666,7 +666,7 @@ def pull_topology(self) -> list[TopologyServiceInDto]:


class BaseRunBookProvider(BaseProvider):
def pull_runbook(self, repo=None, branch=None, md_path=None):
def pull_runbook(self, repo=None, branch=None, md_path=None, title=None):
raise NotImplementedError("get_runbook() method not implemented")

def pull_repositories(self):
Expand Down
Loading

0 comments on commit 99c0226

Please sign in to comment.