diff --git a/api/api.py b/api/api.py index 41738b8..911f1cc 100644 --- a/api/api.py +++ b/api/api.py @@ -1,53 +1,110 @@ import asyncio -from os import system -from time import sleep -from typing import Any, Dict -from fastapi import FastAPI -from fastapi.responses import StreamingResponse -from pydantic import BaseModel +import uuid +from wsgiref.util import request_uri import ansible_runner +import os +from typing import Any, Dict +from fastapi import FastAPI, HTTPException, Request +from fastapi.responses import StreamingResponse +from pydantic import BaseModel, Field +from ansible_runner import Runner +from dataclasses import dataclass + + +class Link(BaseModel): + href: str + class DeployArgs(BaseModel): - extra_vars: Dict[str, Any] + service: str + inventory: str | None = None + vars: Dict[str, Any] = {} + + +class DeploymentLinks(BaseModel): + logs: Link + self: Link + + +@dataclass +class Deployment: + id: str + runner: Runner | None = None + + +class DeploymentDTO(BaseModel): + id: str + status: str + links: DeploymentLinks = Field(..., alias="_links") + + def from_deployment(deployment: Deployment, request: Request): + return DeploymentDTO( + id=deployment.id, + status=deployment.runner.status, + _links={ + "self": {"href": request.url_for("deployment", id=deployment.id)}, + "logs": {"href": request.url_for("deployment_logs", id=deployment.id)}, + }, + ) + + +deployments: Dict[str, Deployment] = {} app = FastAPI() -@app.post("/deploy/{service}") -async def deploy(service: str, args: DeployArgs): - finished = False - lines = [] - def finish_callback(_): - nonlocal finished - finished = True +@app.get("/deployment/{id}/logs") +async def deployment_logs(id: str): + if id not in deployments: + raise HTTPException(status_code=404, detail="Deployment was not found") - def event_callback(data: Dict): - if 'stdout' in data: - lines.append(data['stdout']) + runner = deployments.get(id).runner + + async def stream(): + stdout = runner.stdout + + while True: + while line := stdout.readline(): + if line == "": + break + yield line + + if runner.status != "running": + break - async def logs(): - nonlocal lines - while not finished: - for line in lines: - yield line.rstrip() + "\n" - lines = [] await asyncio.sleep(0.1) - ansible_runner.run_async( - playbook='deploy.yaml', - extravars={ - 'services': [service], - **args.extra_vars - }, - private_data_dir='/home/api-server', - project_dir='/var/project', - inventory='inventory/m2.ini', - event_handler=event_callback, - finished_callback=finish_callback, - settings={ - 'suppress_ansible_output': True - } + return StreamingResponse(stream(), media_type="text/plain") + + +@app.get("/deployment") +async def deployment_list(request: Request): + return [ + DeploymentDTO.from_deployment(deployment, request) + for deployment in deployments.values() + ] + + +@app.get("/deployment/{id}") +async def deployment(id: str, request: Request): + return DeploymentDTO.from_deployment(deployments[id], request) + + +@app.post("/deployment") +async def deploy(args: DeployArgs, request: Request): + ident = str(uuid.uuid4()) + + _, runner = ansible_runner.run_async( + playbook="deploy.yaml", + ident=ident, + extravars={"services": [args.service], **args.vars}, + private_data_dir="/home/api-server", + project_dir=os.environ.get("API_PROJECT_DIR", "/var/project"), + inventory=args.inventory or os.environ.get("API_INVENTORY"), + settings={"suppress_ansible_output": True}, ) - return StreamingResponse(logs(), media_type='text/plain') + deployment = Deployment(ident, runner) + deployments[ident] = deployment + return DeploymentDTO.from_deployment(deployment, request)