2022-07-22 12:05:59 +00:00
|
|
|
import asyncio
|
2022-07-22 14:32:49 +00:00
|
|
|
import logging
|
2022-12-01 16:53:33 +00:00
|
|
|
import os
|
2023-11-07 15:14:56 +01:00
|
|
|
import sys
|
|
|
|
|
from contextlib import asynccontextmanager
|
|
|
|
|
from typing import Any, AsyncIterator, Dict, List, Optional, Protocol, Tuple, cast
|
2022-12-01 14:20:37 +00:00
|
|
|
|
2023-11-07 15:14:56 +01:00
|
|
|
import json_logging
|
2022-07-22 12:05:59 +00:00
|
|
|
import uvicorn
|
2022-12-01 16:31:04 +00:00
|
|
|
from fastapi import Depends, FastAPI, HTTPException, Request, status
|
|
|
|
|
from fastapi.security import (
|
|
|
|
|
HTTPAuthorizationCredentials,
|
|
|
|
|
HTTPBasic,
|
|
|
|
|
HTTPBasicCredentials,
|
|
|
|
|
HTTPBearer,
|
|
|
|
|
)
|
2023-11-07 16:04:51 +01:00
|
|
|
from prometheus_fastapi_instrumentator import Instrumentator
|
2022-07-22 12:05:59 +00:00
|
|
|
|
2023-11-07 15:14:56 +01:00
|
|
|
from ops_bot import alertmanager, aws, pagerduty
|
2022-12-01 16:31:04 +00:00
|
|
|
from ops_bot.config import BotSettings, RoutingKey, load_config
|
2022-12-01 13:47:27 +00:00
|
|
|
from ops_bot.gitlab import hook as gitlab_hook
|
2022-12-01 16:31:04 +00:00
|
|
|
from ops_bot.matrix import MatrixClient
|
2022-07-22 12:05:59 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
async def get_matrix_service(request: Request) -> MatrixClient:
|
|
|
|
|
"""A helper to fetch the matrix client from the app state"""
|
|
|
|
|
return cast(MatrixClient, request.app.state.matrix_client)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def matrix_main(matrix_client: MatrixClient) -> None:
|
|
|
|
|
"""Execs the matrix client asyncio task"""
|
|
|
|
|
workers = [asyncio.create_task(matrix_client.start())]
|
|
|
|
|
await asyncio.gather(*workers)
|
|
|
|
|
|
|
|
|
|
|
2023-11-07 15:14:56 +01:00
|
|
|
@asynccontextmanager
|
|
|
|
|
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
|
2026-03-05 15:55:47 +01:00
|
|
|
config_fname = os.environ.get("BOT_CONFIG_FILE", "config.json")
|
2022-12-01 16:53:33 +00:00
|
|
|
bot_settings = load_config(config_fname)
|
2022-12-01 16:31:04 +00:00
|
|
|
c = MatrixClient(settings=bot_settings.matrix, join_rooms=bot_settings.get_rooms())
|
2022-07-22 12:05:59 +00:00
|
|
|
app.state.matrix_client = c
|
|
|
|
|
app.state.bot_settings = bot_settings
|
|
|
|
|
asyncio.create_task(matrix_main(c))
|
2023-11-07 15:14:56 +01:00
|
|
|
yield
|
|
|
|
|
await app.state.matrix_client.shutdown()
|
2022-07-22 12:05:59 +00:00
|
|
|
|
|
|
|
|
|
2026-03-05 15:55:47 +01:00
|
|
|
# start_http_server(9000)
|
2023-11-07 15:14:56 +01:00
|
|
|
app = FastAPI(lifespan=lifespan)
|
2023-11-07 16:02:10 +01:00
|
|
|
instrumentator = Instrumentator().instrument(app)
|
2023-11-07 15:14:56 +01:00
|
|
|
bearer_security = HTTPBearer(auto_error=False)
|
|
|
|
|
basic_security = HTTPBasic(auto_error=False)
|
|
|
|
|
|
|
|
|
|
log = logging.getLogger("ops_bot")
|
|
|
|
|
log.addHandler(logging.StreamHandler(sys.stdout))
|
|
|
|
|
json_logging.init_fastapi(enable_json=True)
|
|
|
|
|
json_logging.init_request_instrument(app)
|
2022-07-22 12:05:59 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/")
|
|
|
|
|
async def root() -> Dict[str, str]:
|
|
|
|
|
return {"message": "Hello World"}
|
|
|
|
|
|
|
|
|
|
|
2022-12-01 16:31:04 +00:00
|
|
|
async def bearer_token_authorizer(
|
|
|
|
|
route: RoutingKey,
|
|
|
|
|
request: Request,
|
|
|
|
|
basic_credentials: Optional[HTTPBasicCredentials],
|
|
|
|
|
bearer_credentials: Optional[HTTPAuthorizationCredentials],
|
|
|
|
|
) -> bool:
|
|
|
|
|
|
|
|
|
|
bearer_token: Optional[str] = route.secret_token
|
|
|
|
|
return (
|
|
|
|
|
bearer_credentials is not None
|
|
|
|
|
and bearer_credentials.credentials == bearer_token
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def nop_authorizer(
|
|
|
|
|
route: RoutingKey,
|
|
|
|
|
request: Request,
|
|
|
|
|
basic_credentials: Optional[HTTPBasicCredentials],
|
|
|
|
|
bearer_credentials: Optional[HTTPAuthorizationCredentials],
|
2022-07-22 12:05:59 +00:00
|
|
|
) -> bool:
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
2022-12-01 16:31:04 +00:00
|
|
|
def get_route(bot_settings: BotSettings, path_key: str) -> Optional[RoutingKey]:
|
|
|
|
|
# find path_key in bot_settings.routing_keys
|
|
|
|
|
for route in bot_settings.routing_keys:
|
|
|
|
|
if route.path_key == path_key:
|
|
|
|
|
return route
|
|
|
|
|
return None
|
2022-07-22 12:05:59 +00:00
|
|
|
|
|
|
|
|
|
2022-12-01 16:31:04 +00:00
|
|
|
class Authorizer(Protocol):
|
|
|
|
|
async def __call__(
|
|
|
|
|
self,
|
|
|
|
|
route: RoutingKey,
|
|
|
|
|
request: Request,
|
|
|
|
|
basic_credentials: Optional[HTTPBasicCredentials],
|
|
|
|
|
bearer_credentials: Optional[HTTPAuthorizationCredentials],
|
2026-03-05 16:08:31 +01:00
|
|
|
) -> bool: ...
|
2022-11-30 15:21:09 +00:00
|
|
|
|
|
|
|
|
|
2022-12-01 16:31:04 +00:00
|
|
|
class ParseHandler(Protocol):
|
|
|
|
|
async def __call__(
|
|
|
|
|
self,
|
|
|
|
|
route: RoutingKey,
|
|
|
|
|
payload: Any,
|
|
|
|
|
request: Request,
|
2026-03-05 16:08:31 +01:00
|
|
|
) -> List[Tuple[str, str]]: ...
|
2022-07-22 12:05:59 +00:00
|
|
|
|
|
|
|
|
|
2022-12-01 16:31:04 +00:00
|
|
|
handlers: Dict[str, Tuple[Authorizer, ParseHandler]] = {
|
|
|
|
|
"gitlab": (gitlab_hook.authorize, gitlab_hook.parse_event),
|
|
|
|
|
"pagerduty": (bearer_token_authorizer, pagerduty.parse_pagerduty_event),
|
|
|
|
|
"aws-sns": (nop_authorizer, aws.parse_sns_event),
|
2023-11-07 15:14:56 +01:00
|
|
|
"alertmanager": (nop_authorizer, alertmanager.parse_alertmanager_event),
|
2022-12-01 16:31:04 +00:00
|
|
|
}
|
2022-11-30 15:21:09 +00:00
|
|
|
|
2022-12-01 14:20:37 +00:00
|
|
|
|
2022-12-01 16:31:04 +00:00
|
|
|
@app.post("/hook/{routing_key}")
|
|
|
|
|
async def webhook_handler(
|
2022-12-01 13:47:27 +00:00
|
|
|
request: Request,
|
2022-12-01 16:31:04 +00:00
|
|
|
routing_key: str,
|
|
|
|
|
basic_credentials: Optional[HTTPBasicCredentials] = Depends(basic_security),
|
|
|
|
|
bearer_credentials: Optional[HTTPAuthorizationCredentials] = Depends(
|
|
|
|
|
bearer_security
|
|
|
|
|
),
|
2022-12-01 14:20:37 +00:00
|
|
|
matrix_client: MatrixClient = Depends(get_matrix_service),
|
2022-12-01 13:47:27 +00:00
|
|
|
) -> Dict[str, str]:
|
2022-12-01 16:31:04 +00:00
|
|
|
route = get_route(request.app.state.bot_settings, routing_key)
|
|
|
|
|
|
|
|
|
|
if not route:
|
|
|
|
|
logging.error(f"unknown routing key {routing_key}")
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=status.HTTP_404_NOT_FOUND, detail="Unknown routing key"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
handler: Optional[Tuple[Authorizer, ParseHandler]] = handlers.get(route.hook_type)
|
|
|
|
|
if not handler:
|
|
|
|
|
raise HTTPException(
|
|
|
|
|
status_code=status.HTTP_404_NOT_FOUND, detail="Unknown hook type"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
authorizer, parse_handler = handler
|
|
|
|
|
|
|
|
|
|
if not await authorizer(
|
|
|
|
|
route,
|
|
|
|
|
request=request,
|
|
|
|
|
bearer_credentials=bearer_credentials,
|
|
|
|
|
basic_credentials=basic_credentials,
|
|
|
|
|
):
|
2022-12-01 13:47:27 +00:00
|
|
|
raise HTTPException(
|
2022-12-01 16:31:04 +00:00
|
|
|
status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials"
|
2022-12-01 13:47:27 +00:00
|
|
|
)
|
2022-12-01 16:31:04 +00:00
|
|
|
|
|
|
|
|
payload: Any = await request.json()
|
|
|
|
|
|
|
|
|
|
messages = await parse_handler(route, payload, request=request)
|
2022-12-01 13:47:27 +00:00
|
|
|
for msg_plain, msg_formatted in messages:
|
|
|
|
|
await matrix_client.room_send(
|
2022-12-01 16:31:04 +00:00
|
|
|
route.room_id,
|
2022-12-01 13:47:27 +00:00
|
|
|
msg_plain,
|
|
|
|
|
message_formatted=msg_formatted,
|
|
|
|
|
)
|
2022-12-01 16:31:04 +00:00
|
|
|
|
2022-12-01 13:47:27 +00:00
|
|
|
return {"status": "ok"}
|
|
|
|
|
|
2022-11-30 15:21:09 +00:00
|
|
|
|
2022-07-22 12:05:59 +00:00
|
|
|
def start_dev() -> None:
|
2026-03-05 15:55:47 +01:00
|
|
|
uvicorn.run("ops_bot.main:app", port=1112, host="127.0.0.1", reload=True)
|
2022-07-22 12:05:59 +00:00
|
|
|
|
|
|
|
|
|
2023-11-07 16:02:10 +01:00
|
|
|
def main() -> None:
|
2026-03-05 15:55:47 +01:00
|
|
|
host = os.environ.get("BOT_LISTEN_HOST", "127.0.0.1")
|
|
|
|
|
port = int(os.environ.get("BOT_LISTEN_PORT", "1111"))
|
|
|
|
|
uvicorn.run(app, port=port, host=host) # nosec B104
|
2022-07-22 12:53:34 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
2023-11-07 16:02:10 +01:00
|
|
|
main()
|