From c39457244b95ae1bea9777ea74bb836518b8d37c Mon Sep 17 00:00:00 2001
From: Kacper Donat <kadet1090@gmail.com>
Date: Tue, 15 Nov 2022 18:56:49 +0100
Subject: [PATCH] api-server: Separate /logs endpoint

---
 api/api.py | 114 ++++++++++++++++++++++++++++++++++++++---------------
 1 file changed, 83 insertions(+), 31 deletions(-)

diff --git a/api/api.py b/api/api.py
index 41738b8..738a2f5 100644
--- a/api/api.py
+++ b/api/api.py
@@ -1,53 +1,105 @@
 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(
+    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': [service],
-            **args.extra_vars
+            'services': [args.service],
+            **args.vars
         },
         private_data_dir='/home/api-server',
-        project_dir='/var/project',
-        inventory='inventory/m2.ini',
-        event_handler=event_callback,
-        finished_callback=finish_callback,
+        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)