From 37311766b488a6325abd3a8f486072e0d0e6485c Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Fri, 19 Jul 2019 20:14:06 +0300 Subject: Add: controller /status, change how access control is done For reasons that I don't understand, the old way didn't work, as it resulted in all routes in Bottle being the same route. I think it's a bug in Bottle, but I didn't get to the root cause. Now it works by installing a Bottle plugin to do the access check. In some ways, it's a cleaner way, anyway. --- api.py | 130 +++++++++++++++++++++++++++++++++++--------------------------- gitlab.md | 21 +++++++++- 2 files changed, 93 insertions(+), 58 deletions(-) diff --git a/api.py b/api.py index 434b0cf..8ea2819 100755 --- a/api.py +++ b/api.py @@ -114,6 +114,34 @@ class AccessChecker: return token_text +class AccessCheckerPlugin: + + def __init__(self, checker): + self._checker = checker + + def apply(self, callback, route): + def access_checker_wrapper(*args, **kwargs): + r = bottle.request + try: + logging.debug('AccessCheckerPlugin: checking if access is allowed') + scopes = route['config']['scopes'] + if not self._checker.access_is_allowed(r.headers, scopes): + logging.error('Request denied %s %s', r.method, r.path) + return bottle.HTTPError(400) + + logging.debug( + 'AccessCheckerPlugin: access is allowed, ' + 'calling callback') + return callback(*args, **kwargs) + except BaseException as e: + logging.error( + 'Could not handle request: %s %s: %s', + r.method, r.path, str(e)) + raise bottle.HTTPError(500) + + return access_checker_wrapper + + class API: '''Base class for simple HTTP APIs @@ -125,73 +153,28 @@ class API: def __init__(self): self._checker = None + self.app = None def setup(self, app, token_pubkey): - self._checker = AccessChecker(token_pubkey) + checker = AccessChecker(token_pubkey) + self._plugin = AccessCheckerPlugin(checker) self._add_routes(app, self.get_routes()) def get_routes(self): raise NotImplementedError() def _add_routes(self, app, routes): - for route in routes: - func = route.pop('func') - scopes = route.pop('scopes') - assert isinstance(scopes, list) - callback = lambda **kwargs: self.check(func, scopes, kwargs) - route = dict(route) - route['callback'] = callback + for r in routes: + assert isinstance(r['scopes'], list) + route = { + 'method': r['method'], + 'path': r['path'], + 'callback': r['func'], + 'apply': self._plugin, + 'scopes': r['scopes'], + } app.route(**route) - def check(self, func, required_scopes, kwargs): - '''Call a callback function, if it's OK to do so''' - try: - r = bottle.request - logging.debug('New request, checking access: %s %s', r.method, r.path) - - if self._checker.access_is_allowed(r.headers, required_scopes): - logging.info('Access is allowed: %s %s', r.method, r.path) - ret = func(**kwargs) - logging.info('Result: %r', ret) - return ret - - logging.error('Request denied %s %s', r.method, r.path) - return bottle.HTTPError(400) - except Exception as e: - logging.warning('Caught exception: %s', str(e)) - return bottle.HTTPError(500) - - -class Controller(API): - - '''A dummy controller API''' - - def get_routes(self): - return [ - { - 'method': 'GET', - 'path': '/status', - 'func': self._status, - 'scopes': ['status'], - }, - { - 'method': 'GET', - 'path': '/hello/', - 'func': self._hello, - 'scopes': ['hello'], - }, - ] - - def _status(self): - return { - 'queue': [], - 'running': [], - 'finished': [], - } - - def _hello(self, name=None): - return 'hello {}\n'.format(name) - class VCSWorker(API): @@ -385,6 +368,39 @@ class Deployer(API): return runcmd('.', argv, self.MAX_UPLOAD_TIME) +class Controller(API): + + '''A dummy controller API''' + + def __init__(self): + super().__init__() + self.builds = [] + + def get_routes(self): + return [ + { + 'method': 'GET', + 'path': '/status', + 'func': self._status, + 'scopes': ['status'], + }, + { + 'method': 'POST', + 'path': '/trigger', + 'func': self._trigger, + 'scopes': ['trigger'], + }, + ] + + def _status(self): + return { + 'builds': list(self.builds), + } + + def _trigger(self): + return 'trigger?' + + def runcmd(cwd, argv, timeout): logging.info('Running command: %r', argv) try: diff --git a/gitlab.md b/gitlab.md index 9dc2fa8..d3f7c6a 100644 --- a/gitlab.md +++ b/gitlab.md @@ -70,10 +70,29 @@ use them, and don't have to learn stuff to get started. * Simple HTTP API * Endpoint: POST /cd, body specifies which repo and ref to build and - deploy; queues the build + deploy; queues the build, the queued build will be visible via the + /status endpoint + + { + "repo": "...", + "ref": "..." + "artifact": "..." + } + * Endpoint: GET /status, which lists what jobs (posts to /cd) are queued, or running, or finished + { + "builds": [ + "id1": {"repo":"...", "ref":"...", "artifact":"..."}, + "id2": {"repo":"...", "ref":"...", "artifact":"..."}, + "id3": {"repo":"...", "ref":"...", "artifact":"..."}, + ] + "queued": ["id1", ...] + "building": ["id2", ...] + "finished": ["id3", ...] + } + ## VCS worker * Simple HTTP API -- cgit v1.2.1