summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLars Wirzenius <lwirzenius@wikimedia.org>2019-07-19 20:14:06 +0300
committerLars Wirzenius <lwirzenius@wikimedia.org>2019-07-19 20:14:06 +0300
commit37311766b488a6325abd3a8f486072e0d0e6485c (patch)
tree61efc715183317160018b144b5d41d496711174b
parent9b52055d3a2fbac4b2287cc51b2bda1f8ac0e4b9 (diff)
downloadwmf-ci-arch-37311766b488a6325abd3a8f486072e0d0e6485c.tar.gz
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.
-rwxr-xr-xapi.py130
-rw-r--r--gitlab.md21
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/<name>',
- '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