Commit 23b7c789 authored by Max Rees's avatar Max Rees

cgi: implement /-/builders page

parent 2d12438b
......@@ -5,6 +5,7 @@ import collections # defaultdict
import configparser # ConfigParser
from pathlib import Path
BUILDERS = "@builders"
DEFAULT = "@default"
def _config_map(s):
......
......@@ -4,11 +4,14 @@
# See LICENSE for more information.
import datetime # datetime, timezone
import http # HTTPStatus
import json # load
import os # environ
import shutil # copyfileobj
import sqlite3 # connect, OperationalError
import sys # exit, path, stdout
import time # time
import urllib.parse # parse_qs, urlencode
import urllib.request # Request, urlopen
from pathlib import Path
import cgitb # enable
......@@ -21,7 +24,7 @@ SRCDIR = Path(__file__).parent.parent
CACHE = Path("/var/tmp/apkvitrine")
CACHE_ENABLED = CACHE.is_dir()
sys.path.insert(0, str(SRCDIR))
import apkvitrine # DEFAULT
import apkvitrine # BUILDERS, DEFAULT
import apkvitrine.models # build_search, Pkg
ENV = jinja2.Environment(
......@@ -97,6 +100,12 @@ def notfound(**kwargs):
def badreq(**kwargs):
error(http.HTTPStatus.BAD_REQUEST, **kwargs)
def redirect(location):
response(
http.HTTPStatus.TEMPORARY_REDIRECT,
headers={"Location": ENV.globals["base"] + "/" + str(location)},
)
def pkg_paginate(conf, query, db, sql):
query["limit"] = conf.getint("pagination")
try:
......@@ -124,8 +133,71 @@ def pkg_versions(conf, db, pkgs):
return versions, sorted(repos), sorted(arches)
def gl_runner_info(token, endpoint, api):
request = urllib.request.Request(
url=f"{endpoint}/{api}",
method="GET",
headers={"PRIVATE-TOKEN": token},
)
with urllib.request.urlopen(request) as response:
return json.load(response)
def page_builders(conf, path, query):
if apkvitrine.BUILDERS not in conf:
notfound()
return
bconf = conf[apkvitrine.BUILDERS]
builders = gl_runner_info(
bconf["gl_api_token"],
bconf["gl_api_url"],
"runners",
)
builders = [i["id"] for i in builders]
for i, builder in enumerate(builders):
builders[i] = apkvitrine.models.Builder(gl_runner_info(
bconf["gl_api_token"],
bconf["gl_api_url"],
f"../../runners/{builder}",
))
jobs = gl_runner_info(
bconf["gl_api_token"],
bconf["gl_api_url"],
f"../../runners/{builder}/jobs?status=running&order_by=id&per_page=1",
)
if jobs:
builders[i].running_job = apkvitrine.models.Job(jobs[0])
jobs = gl_runner_info(
bconf["gl_api_token"],
bconf["gl_api_url"],
f"../../runners/{builder}/jobs?status=success&order_by=id&per_page=1",
)
if jobs:
builders[i].success_job = apkvitrine.models.Job(jobs[0])
jobs = gl_runner_info(
bconf["gl_api_token"],
bconf["gl_api_url"],
f"../../runners/{builder}/jobs?status=failed&order_by=id&per_page=1",
)
if jobs:
builders[i].fail_job = apkvitrine.models.Job(jobs[0])
ok()
response = ENV.get_template("builders.tmpl").render(
conf=conf[apkvitrine.DEFAULT],
builders=builders,
cached=time.time() if CACHE_ENABLED else None,
)
print(response)
save_cache(path, response)
def page_branches(conf, path, _query):
branches = list(conf.sections())
show_builders = apkvitrine.BUILDERS in branches
for i, branch in enumerate(branches):
if not (SRCDIR / f"{branch}.sqlite").is_file():
......@@ -136,6 +208,7 @@ def page_branches(conf, path, _query):
response = ENV.get_template("branches.tmpl").render(
conf=conf[apkvitrine.DEFAULT],
branches=branches,
show_builders=show_builders,
)
print(response)
save_cache(path, response)
......@@ -217,6 +290,7 @@ _BORING_TOGGLES = (
"subpkgs",
"availability",
"sort",
"purge",
)
def page_search(conf, path, query):
......@@ -276,15 +350,11 @@ def page_search(conf, path, query):
save_cache(path, response)
def page_home(conf, _path, _query):
conf = conf[apkvitrine.DEFAULT]
location = ENV.globals["base"] + "/" + conf["default_version"]
response(
http.HTTPStatus.TEMPORARY_REDIRECT,
headers={"Location": location},
)
redirect(conf[apkvitrine.DEFAULT]["default_version"])
ROUTES = {
"-/versions": page_branches,
"-/builders": page_builders,
"*/-/search": page_search,
"*/*/*": lambda _conf, _path, _query: notfound(),
"*/*": page_package,
......@@ -309,14 +379,18 @@ if __name__ == "__main__":
sys.exit(0)
cache = cache_name(path)
if cache.exists() and not query:
ok()
with cache.open() as cached:
shutil.copyfileobj(cached, sys.stdout)
sys.exit(0)
if cache.exists():
if not query:
ok()
with cache.open() as cached:
shutil.copyfileobj(cached, sys.stdout)
sys.exit(0)
elif query.get("purge") == "1":
cache.unlink()
redirect(path)
sys.exit(0)
conf = apkvitrine.config()
for route, handler in ROUTES.items():
if route == ".":
if route == str(path):
......
......@@ -2,6 +2,7 @@
# Copyright (c) 2020 Max Rees
# See LICENSE for more information.
import collections # namedtuple
import datetime # datetime, timezone
def _insert(table, columns):
values = ", ".join("?" for i in columns)
......@@ -320,3 +321,53 @@ def build_search(query):
sql += f" ORDER BY {sort}"
return sql
def gl_strptime(s):
# 2020-12-22T23:53:51.993Z
s, micro = s.split(".", maxsplit=1)
micro = int(micro.replace("Z", "", 1)) * 1000
s = s.replace("T", " ", 1)
s = datetime.datetime.strptime(s, "%Y-%m-%d %H:%M:%S")
return s.replace(
microsecond=micro,
tzinfo=datetime.timezone.utc,
).timestamp()
class Builder:
__slots__ = (
"id",
"name",
"tags",
"online",
"active",
"seen",
"running_job",
"success_job",
"fail_job",
)
def __init__(self, data):
self.id = data["id"]
self.name = data["description"]
self.tags = data.get("tag_list")
self.online = data["online"]
self.active = data["active"]
self.seen = gl_strptime(data.get("contacted_at"))
self.running_job = self.success_job = self.fail_job = None
class Job:
__slots__ = (
"id",
"pipeline",
"status",
"revision",
"title",
"finished",
)
def __init__(self, data):
self.id = data["id"]
self.status = data["status"]
self.revision = data["commit"]["id"]
self.title = data["commit"]["title"]
self.finished = gl_strptime(data["finished_at"])
......@@ -12,4 +12,8 @@
<li><a href="{{ branch }}">{{ branch }}</a></li>
# endfor
</ul>
# if show_builders
<p><a href="-/builders">View builder status.</a></p>
# endif
# endblock
## vi: ft=jinja:
## SPDX-License-Identifier: NCSA
## Copyright (c) 2020 Max Rees
## See LICENSE for more information.
# extends "base.tmpl"
# set title = [conf["distro"], "builder status"]|join(" ")
# block breadcrumb
<nav class="breadcrumb is-medium">
<ul>
<li><a href="-/versions">packages</a></li>
<li class="is-active"><a href="-/builders">builders</a></li>
</ul>
</nav>
# endblock
# macro job_info(job)
# if job
<a href="https://code.foxkit.us/sroracle/packages/-/jobs/{{ job.id }}">
{{ "#" ~ job.id }}
<br>
{{ job.title }}
<br>
{{ job.finished|datetime }}
</a>
# endif
# endmacro
# block content
<div class="table-container">
<table class="builders table is-bordered is-striped is-hoverable">
<tr>
<th>Name</th>
<th>Status</th>
<th>Running</th>
<th>Last success</th>
<th>Last failure</th>
</tr>
# for builder in builders|sort(attribute="name")
<tr>
<td>
{{ builder.name }}
# for tag in builder.tags|sort
<br>
<span class="tag is-info">{{ tag }}</span>
# endfor
</td>
<td>
# if builder.online
<span class="tag is-success">online</span>
# else
<span class="tag is-danger">offline</span>
# endif
# if not builder.active
<span class="tag is-warning">paused</span>
# endif
<br>
{{ builder.seen|datetime }}
</td>
<td>
{{ job_info(builder.running_job) }}
<td>
{{ job_info(builder.success_job) }}
<td>
{{ job_info(builder.fail_job) }}
</td>
</tr>
# endfor
</table>
</div>
# if cached is not none
<p>
Last updated {{ cached|datetime }}.
<a href="-/builders?purge=1">Click here to update.</a>
</p>
# endif
# endblock
......@@ -53,6 +53,15 @@ pagination = 25
; Required: default OS version
default_version = current
; Optional: enable builders page
;[@builders]
; Optional: override Gitlab API endpoint to query builder status
; ("runners" API endpoint)
;gl_api_url = https://code.foxkit.us/api/v4/projects/76
; Required for builders page: Gitlab API token (PRIVATE-TOKEN) that can
; view the above project's runners list
;gl_api_token = YOUR-TOKEN-HERE
[current]
; =========================================================
; *******************
......@@ -74,8 +83,6 @@ repology_link = https://repology.org/tools/project-by?repo=adelie_current&name_t
repology_badge = https://repology.org/tools/project-by?repo=adelie_current&name_type=binname&target_page=badge_version_for_repo&name=
[1.0-beta4]
; =========================================================
; Actually a tag. just demontrating
gl_branch = 1.0-BETA4
gl_url = https://code.foxkit.us/adelie/packages/-/tree/1.0-BETA4/{startdir}
......
......@@ -30,3 +30,8 @@ nav.breadcrumb .is-active a { color: white }
#search-controls .glob { margin-bottom: 0.5rem }
#search-controls .glob > .field { margin-bottom: 0 }
#search-controls .glob .control { padding: 0 0.25rem 0 0.25rem }
.builders th, .builders td {
text-align: center !important;
vertical-align: middle !important;
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment