flask-gogs-maker/gogsmaker.py

209 lines
5.9 KiB
Python
Raw Permalink Normal View History

2018-03-03 00:47:28 +01:00
""" GogsMaker
A webhook-handler for Gogs running `make` when needed. """
2018-03-03 01:45:54 +01:00
import os
2018-03-03 02:13:35 +01:00
import sys
2018-03-03 01:45:54 +01:00
import subprocess
2018-03-03 02:31:54 +01:00
import hmac
2018-03-03 14:24:03 +01:00
import logging
2018-03-03 02:31:54 +01:00
from hashlib import sha256
2018-03-03 02:13:35 +01:00
from threading import Thread
from functools import wraps
2018-03-03 14:37:07 +01:00
import coloredlogs
2018-03-03 01:09:24 +01:00
from flask import Flask, request
2018-03-03 14:37:07 +01:00
import settings
2018-03-03 01:09:24 +01:00
2018-03-03 14:24:03 +01:00
LOGGER_NAME = __name__
2018-03-03 01:09:24 +01:00
app = Flask(__name__)
2018-03-03 01:45:54 +01:00
class UnmonitoredRepository(Exception):
pass
class GitError(Exception):
def __init__(self, what):
super().__init__()
2018-03-03 01:45:54 +01:00
self.what = what
def __str__(self):
return self.what
def get_hook(url):
2021-09-01 00:00:42 +02:00
""" Get the hook matching an URL, or raise UnmonitoredRepository """
2018-03-03 01:45:54 +01:00
for hook in settings.HOOKS:
2021-09-01 00:00:42 +02:00
if hook["url"] == url:
2018-03-03 01:45:54 +01:00
return hook
raise UnmonitoredRepository
def repo_path(hook):
2021-09-01 00:00:42 +02:00
""" Get the path at which the hook's repo is cloned """
return os.path.join(settings.CLONE_ROOT, hook["name"])
2018-03-03 01:45:54 +01:00
2018-03-03 14:24:03 +01:00
def subprocess_run(command, **kwargs):
2021-09-01 00:00:42 +02:00
""" Run subprocess with default arguments """
2018-03-03 14:24:03 +01:00
args = {
2021-09-01 00:00:42 +02:00
"check": True,
"stdout": subprocess.DEVNULL,
"stderr": subprocess.PIPE,
2018-03-03 14:24:03 +01:00
}
args.update(kwargs)
return subprocess.run(command, **args)
2018-03-03 02:13:35 +01:00
class MakeWorker(Thread):
2021-09-01 00:00:42 +02:00
""" A make job """
2018-03-03 02:13:35 +01:00
def __init__(self, hook):
super().__init__()
self.hook = hook
2021-09-01 00:00:42 +02:00
self.name = "makeworker-{}".format(hook["name"])
2018-03-03 02:13:35 +01:00
self.path = repo_path(hook)
def run(self):
2021-09-01 00:00:42 +02:00
""" Run the make job """
2018-03-03 02:13:35 +01:00
try:
2021-09-01 00:00:42 +02:00
subprocess_run(["make", "-C", self.path, "--"] + self.hook["targets"])
2018-03-03 14:24:03 +01:00
except subprocess.CalledProcessError as error:
logging.error(
2021-09-01 00:00:42 +02:00
("Hook %s: make failed with status %s. " "Error output:\n%s\n"),
self.hook["name"],
2018-03-03 14:24:03 +01:00
error.returncode,
2021-09-01 00:00:42 +02:00
error.stderr.decode("utf-8"),
)
2018-03-03 02:13:35 +01:00
2018-03-03 01:45:54 +01:00
def update_repo(hook, clone_url):
2021-09-01 00:00:42 +02:00
""" Update (or clone) the given repository. May raise GitError. """
2018-03-03 01:45:54 +01:00
path = repo_path(hook)
2021-09-01 00:00:42 +02:00
if os.path.isdir(os.path.join(path, ".git")): # Repo is already cloned
2018-03-03 01:45:54 +01:00
try:
2021-09-01 00:00:42 +02:00
subprocess_run(["git", "-C", path, "reset", "--hard"]) # Just in case.
subprocess_run(["git", "-C", path, "pull"])
2018-03-03 14:24:03 +01:00
except subprocess.CalledProcessError as error:
logging.error(
2021-09-01 00:00:42 +02:00
("Hook %s: git failed with status %s. " "Error output:\n%s\n"),
hook["name"],
2018-03-03 14:24:03 +01:00
error.returncode,
2021-09-01 00:00:42 +02:00
error.stderr.decode("utf-8"),
)
raise GitError("Cannot pull {}".format(hook["name"]))
2018-03-03 01:45:54 +01:00
2018-03-03 14:24:03 +01:00
else: # Repo is to be cloned
try:
2021-09-01 00:00:42 +02:00
subprocess_run(["mkdir", "-p", path])
subprocess_run(["git", "clone", clone_url, path], check=True)
2018-03-03 14:24:03 +01:00
except subprocess.CalledProcessError as error:
logging.error(
2021-09-01 00:00:42 +02:00
("Hook %s: git failed cloning with status %s. " "Error output:\n%s"),
hook["name"],
2018-03-03 14:24:03 +01:00
error.returncode,
2021-09-01 00:00:42 +02:00
error.stderr.decode("utf-8"),
)
raise GitError("Cannot clone {}".format(clone_url))
2018-03-03 01:45:54 +01:00
2018-03-03 02:31:54 +01:00
def check_signature(received_sig, hook, payload):
2021-09-01 00:00:42 +02:00
""" Check Gogs signature """
digest = hmac.new(
hook["secret"].encode("utf-8"), msg=payload, digestmod=sha256
).hexdigest()
2018-03-03 02:31:54 +01:00
return hmac.compare_digest(digest, received_sig)
2018-03-03 01:45:54 +01:00
def gogs_payload(required):
def wrapper(fct):
@wraps(fct)
def wrapped(*args, **kwargs):
payload = request.json
if payload is None:
2021-09-01 00:00:42 +02:00
return "Expected json\n", 415
2021-09-01 00:00:42 +02:00
for field in required + ["repository/html_url"]:
path = field.split("/")
explore = payload
for section in path:
if section not in explore:
return (
2021-09-01 00:00:42 +02:00
"Invalid json: missing {}\n".format("/".join(path)),
400,
)
explore = explore[section]
try:
2021-09-01 00:00:42 +02:00
hook = get_hook(payload["repository"]["html_url"])
except UnmonitoredRepository:
2021-09-01 00:00:42 +02:00
return "Unmonitored repository\n", 403
2018-03-03 02:31:54 +01:00
if not settings.DEBUG:
2021-09-01 00:00:42 +02:00
received_sig = request.headers["X-Gogs-Signature"]
2018-03-03 02:31:54 +01:00
payload_raw = request.data
if not check_signature(received_sig, hook, payload_raw):
2021-09-01 00:00:42 +02:00
return "Invaild signature\n", 403
2018-03-03 02:31:54 +01:00
return fct(payload, hook, *args, **kwargs)
2021-09-01 00:00:42 +02:00
return wrapped
2021-09-01 00:00:42 +02:00
2018-03-03 01:45:54 +01:00
return wrapper
2021-09-01 00:00:42 +02:00
@app.route("/", methods=["POST"])
@gogs_payload(["repository/clone_url"])
2018-03-03 01:45:54 +01:00
def view_root(payload, hook):
2021-09-01 00:00:42 +02:00
clone_url = payload["repository"]["clone_url"]
if "clone_url" in hook:
clone_url = hook["clone_url"]
2018-03-03 01:45:54 +01:00
try:
update_repo(hook, clone_url)
except GitError as error:
2021-09-01 00:00:42 +02:00
return "Git error: {}\n".format(error), 500
2018-03-03 01:45:54 +01:00
2018-03-03 02:13:35 +01:00
worker = MakeWorker(hook)
worker.start()
2018-03-03 01:45:54 +01:00
2021-09-01 00:00:42 +02:00
return "OK\n", 200
2018-03-03 02:13:35 +01:00
2018-03-03 02:48:09 +01:00
@app.before_first_request # FIXME this should be run on startup...
2018-03-03 14:24:03 +01:00
def startup_actions():
setup_logger()
check_settings()
def setup_logger():
2021-09-01 00:00:42 +02:00
""" Setup the default logger """
2018-03-03 14:24:03 +01:00
coloredlogs.install(
fmt="%(asctime)s [%(levelname)s] %(message)s",
)
2018-03-03 02:48:09 +01:00
def check_settings():
2021-09-01 00:00:42 +02:00
""" Check the supplied settings """
2018-03-03 02:48:09 +01:00
if settings.DEBUG:
2021-09-01 00:00:42 +02:00
logging.warning(
"GogsMaker is running in DEBUG MODE, this is "
"unsuitable for production environments!"
)
2018-03-03 02:48:09 +01:00
2021-09-01 00:00:42 +02:00
required_keys = ["name", "url", "targets", "secret"]
2018-03-03 02:48:09 +01:00
for hook_id, hook in enumerate(settings.HOOKS):
for key in required_keys:
if key not in hook:
2021-09-01 00:00:42 +02:00
if key == "name":
descr = "#{}".format(hook_id)
2018-03-03 02:48:09 +01:00
else:
2021-09-01 00:00:42 +02:00
descr = "{} (#{})".format(hook["name"], hook_id)
2018-03-03 02:48:09 +01:00
2021-09-01 00:00:42 +02:00
logging.critical(
("Configuration error: hook %s lacks " "attribute %s."), descr, key
)
2018-03-03 02:48:09 +01:00
sys.exit(1)