Build log endpoints
This commit is contained in:
parent
b80508eed9
commit
8a4797bf02
10 changed files with 77 additions and 18 deletions
|
|
@ -106,8 +106,8 @@ actually deploy. It expects the following to hold true:
|
|||
value;
|
||||
- a deployment starts with 0 replicas and is created with a
|
||||
`runboat/init-status=todo` label, as well as a `runboat/cleanup` finalizer;
|
||||
- the intialization job has a `runboat/job-kind=initialize` label;
|
||||
- the cleanup job has a `runboat/job-kind=cleanup` label.
|
||||
- the intialization job and pods have a `runboat/job-kind=initialize` label;
|
||||
- the cleanup job and pods have a `runboat/job-kind=cleanup` label.
|
||||
- the following annotations are set on deployments:
|
||||
|
||||
- `runboat/repo`: the repository in owner/repo format;
|
||||
|
|
@ -136,7 +136,6 @@ Advanced prototype (min required to open the project):
|
|||
|
||||
MVP:
|
||||
|
||||
- build/log and build/init-log api endpoints
|
||||
- better error handling in API (return 400 on user errors)
|
||||
- more tests
|
||||
- look at other TODO in code to see if anything important remains
|
||||
|
|
@ -147,6 +146,7 @@ MVP:
|
|||
|
||||
More:
|
||||
|
||||
- streaming build/log and build/init-log api endpoints
|
||||
- shiny UI
|
||||
- websocket stream of build changes, for a dynamic UI
|
||||
- handle PR close (delete all builds for PR)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ classifiers = [
|
|||
"License :: OSI Approved :: MIT License",
|
||||
]
|
||||
dependencies = [
|
||||
"ansi2html",
|
||||
"fastapi",
|
||||
"gunicorn",
|
||||
"httpx",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
# frozen requirements generated by pip-deepfreeze
|
||||
ansi2html==1.6.0
|
||||
anyio==3.3.4
|
||||
asgiref==3.4.1
|
||||
cachetools==4.2.4
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import datetime
|
||||
from typing import Optional
|
||||
|
||||
from ansi2html import Ansi2HTMLConverter
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.responses import StreamingResponse
|
||||
from fastapi.responses import HTMLResponse
|
||||
from pydantic import BaseModel
|
||||
from starlette.status import HTTP_404_NOT_FOUND
|
||||
|
||||
from . import github, models
|
||||
from .controller import controller
|
||||
|
|
@ -120,22 +122,26 @@ async def build(name: str):
|
|||
|
||||
@router.get(
|
||||
"/builds/{name}/init-log",
|
||||
response_class=StreamingResponse,
|
||||
responses={200: {"content": {"text/plain": {}}}},
|
||||
response_class=HTMLResponse,
|
||||
)
|
||||
async def init_log(name: str):
|
||||
# build = await _build_by_name(name)
|
||||
...
|
||||
build = await _build_by_name(name)
|
||||
log = await build.init_log()
|
||||
if not log:
|
||||
raise HTTPException(status_code=HTTP_404_NOT_FOUND, detail="No log found.")
|
||||
return Ansi2HTMLConverter().convert(log)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/builds/{name}/log",
|
||||
response_class=StreamingResponse,
|
||||
responses={200: {"content": {"text/plain": {}}}},
|
||||
response_class=HTMLResponse,
|
||||
)
|
||||
async def log(name: str):
|
||||
# build = _build_by_name(name)
|
||||
...
|
||||
build = await _build_by_name(name)
|
||||
log = await build.log()
|
||||
if not log:
|
||||
raise HTTPException(status_code=HTTP_404_NOT_FOUND, detail="No log found.")
|
||||
return Ansi2HTMLConverter().convert(log)
|
||||
|
||||
|
||||
@router.post("/builds/{name}/start")
|
||||
|
|
|
|||
|
|
@ -255,3 +255,34 @@ async def delete_job(build_name: str, job_kind: DeploymentMode) -> None:
|
|||
"--wait=false",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@sync_to_async
|
||||
def log(build_name: str, job_kind: DeploymentMode | None) -> str:
|
||||
"""Return the buil log.
|
||||
|
||||
The pod for which the log is returned is the first that matches the
|
||||
build_name (via its runboat/build label) and job_kind (via its
|
||||
runboat/job-kind label).
|
||||
"""
|
||||
corev1 = client.CoreV1Api()
|
||||
pods = corev1.list_namespaced_pod(
|
||||
namespace=settings.build_namespace, label_selector=f"runboat/build={build_name}"
|
||||
).items
|
||||
pod = None
|
||||
for pod in pods:
|
||||
if job_kind is None:
|
||||
if "runboat/job-kind" not in pod.metadata.labels:
|
||||
break
|
||||
else:
|
||||
if pod.metadata.labels.get("runboat/job-kind") == job_kind:
|
||||
break
|
||||
else:
|
||||
# no matching pod found
|
||||
return
|
||||
return corev1.read_namespaced_pod_log(
|
||||
pod.metadata.name,
|
||||
namespace=settings.build_namespace,
|
||||
tail_lines=None if job_kind else None,
|
||||
follow=False,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ metadata:
|
|||
runboat/job-kind: cleanup
|
||||
spec:
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
runboat/job-kind: cleanup
|
||||
spec:
|
||||
containers:
|
||||
- name: cleanup
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ metadata:
|
|||
runboat/job-kind: initialize
|
||||
spec:
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
runboat/job-kind: initialize
|
||||
spec:
|
||||
containers:
|
||||
- name: initalize
|
||||
|
|
|
|||
|
|
@ -156,6 +156,12 @@ class Build(BaseModel):
|
|||
def live_link(self) -> str:
|
||||
return f"{self.webui_link}?live"
|
||||
|
||||
async def init_log(self) -> str:
|
||||
return await k8s.log(self.name, job_kind=k8s.DeploymentMode.initialize)
|
||||
|
||||
async def log(self) -> str:
|
||||
return await k8s.log(self.name, job_kind=None)
|
||||
|
||||
@classmethod
|
||||
async def deploy(
|
||||
cls, repo: str, target_branch: str, pr: int | None, git_commit: str
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import asyncio
|
||||
import functools
|
||||
import re
|
||||
from concurrent.futures.thread import ThreadPoolExecutor
|
||||
from functools import wraps
|
||||
|
|
@ -12,8 +13,9 @@ def slugify(s: str | int) -> str:
|
|||
|
||||
def sync_to_async(func):
|
||||
@wraps(func)
|
||||
async def inner(*args):
|
||||
return await asyncio.get_running_loop().run_in_executor(_pool, func, *args)
|
||||
async def inner(*args, **kwargs):
|
||||
f = functools.partial(func, *args, **kwargs)
|
||||
return await asyncio.get_running_loop().run_in_executor(_pool, f)
|
||||
|
||||
return inner
|
||||
|
||||
|
|
@ -27,12 +29,12 @@ def sync_to_async_iterator(iterator_func):
|
|||
raise StopAsyncIteration()
|
||||
|
||||
@sync_to_async
|
||||
def async_iterator_func(*args):
|
||||
return iterator_func(*args)
|
||||
def async_iterator_func(*args, **kwargs):
|
||||
return iterator_func(*args, **kwargs)
|
||||
|
||||
@wraps(iterator_func)
|
||||
async def inner(*args):
|
||||
iterator = await async_iterator_func(*args)
|
||||
async def inner(*args, **kwargs):
|
||||
iterator = await async_iterator_func(*args, **kwargs)
|
||||
while True:
|
||||
try:
|
||||
item = await async_next(iterator)
|
||||
|
|
|
|||
|
|
@ -19,6 +19,12 @@
|
|||
{% endif %}
|
||||
<p>Commit: <a href="{{ build.repo_commit_link }}">{{ build.git_commit }}</a></p>
|
||||
<p>Status: {{ build.status }}</p>
|
||||
<p>
|
||||
Logs:
|
||||
<a href="/api/v1/builds/{{ build.name }}/init-log" target="_blank">build log</a>
|
||||
|
|
||||
<a href="/api/v1/builds/{{ build.name }}/log" target="_blank">run log</a>
|
||||
</p>
|
||||
{% if build.status == 'started' %}
|
||||
<p><a href="{{ build.deploy_link }}">=> live</a></p>
|
||||
<button onclick="stop()">stop</button>
|
||||
|
|
|
|||
Loading…
Reference in a new issue