summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSean Dague <sean@dague.net>2014-09-30 06:28:26 -0400
committerSean Dague <sean@dague.net>2014-10-01 06:02:28 -0400
commita8311bf6a6b751bf038a85b7d0236ee530bbf0ce (patch)
treee0df96e163f0883fa917e358eb92332393b3f26a
parente9a8184fe0e15d877e8fc43bdd59c717615bc249 (diff)
downloadzuul-a8311bf6a6b751bf038a85b7d0236ee530bbf0ce.tar.gz
add support for getting status of individual changes
This expands the rest API for zuul for selecting a portion of the zuul data. The supported urls are: - /status: return a complex data structure that represents the entire queue / pipeline structure of the system - /status.json (backwards compatibility): same as /status - /status/change/X,Y: return status just for gerrit change X,Y In the individual status case the changes are returned as a simple array, and not in the pipeline structure. Tests are added to demonstrate this working, as well as ensure 404 is correctly returned when invalid urls are provided. Co-Authored-By: Joshua Hesketh <joshua.hesketh@rackspace.com> Change-Id: Ib8d80530cc99c222226f73046c17ab0bbf6e080b
-rw-r--r--tests/test_webapp.py85
-rw-r--r--zuul/webapp.py70
2 files changed, 152 insertions, 3 deletions
diff --git a/tests/test_webapp.py b/tests/test_webapp.py
new file mode 100644
index 000000000..b127c517e
--- /dev/null
+++ b/tests/test_webapp.py
@@ -0,0 +1,85 @@
+#!/usr/bin/env python
+
+# Copyright 2014 Hewlett-Packard Development Company, L.P.
+# Copyright 2014 Rackspace Australia
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import json
+import urllib2
+
+from tests.base import ZuulTestCase
+
+
+class TestWebapp(ZuulTestCase):
+
+ def _cleanup(self):
+ self.worker.hold_jobs_in_build = False
+ self.worker.release()
+ self.waitUntilSettled()
+
+ def setUp(self):
+ super(TestWebapp, self).setUp()
+ self.addCleanup(self._cleanup)
+ self.worker.hold_jobs_in_build = True
+ A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
+ A.addApproval('CRVW', 2)
+ self.fake_gerrit.addEvent(A.addApproval('APRV', 1))
+ B = self.fake_gerrit.addFakeChange('org/project1', 'master', 'B')
+ B.addApproval('CRVW', 2)
+ self.fake_gerrit.addEvent(B.addApproval('APRV', 1))
+ self.waitUntilSettled()
+ self.port = self.webapp.server.socket.getsockname()[1]
+
+ def test_webapp_status(self):
+ "Test that we can filter to only certain changes in the webapp."
+
+ req = urllib2.Request(
+ "http://localhost:%s/status" % self.port)
+ f = urllib2.urlopen(req)
+ data = json.loads(f.read())
+
+ self.assertIn('pipelines', data)
+
+ def test_webapp_status_compat(self):
+ # testing compat with status.json
+ req = urllib2.Request(
+ "http://localhost:%s/status.json" % self.port)
+ f = urllib2.urlopen(req)
+ data = json.loads(f.read())
+
+ self.assertIn('pipelines', data)
+
+ def test_webapp_bad_url(self):
+ # do we 404 correctly
+ req = urllib2.Request(
+ "http://localhost:%s/status/foo" % self.port)
+ self.assertRaises(urllib2.HTTPError, urllib2.urlopen, req)
+
+ def test_webapp_find_change(self):
+ # can we filter by change id
+ req = urllib2.Request(
+ "http://localhost:%s/status/change/1,1" % self.port)
+ f = urllib2.urlopen(req)
+ data = json.loads(f.read())
+
+ self.assertEqual(1, len(data), data)
+ self.assertEqual("org/project", data[0]['project'])
+
+ req = urllib2.Request(
+ "http://localhost:%s/status/change/2,1" % self.port)
+ f = urllib2.urlopen(req)
+ data = json.loads(f.read())
+
+ self.assertEqual(1, len(data), data)
+ self.assertEqual("org/project1", data[0]['project'], data)
diff --git a/zuul/webapp.py b/zuul/webapp.py
index 4d6115f96..e289398ac 100644
--- a/zuul/webapp.py
+++ b/zuul/webapp.py
@@ -13,13 +13,32 @@
# License for the specific language governing permissions and limitations
# under the License.
+import copy
+import json
import logging
+import re
import threading
import time
from paste import httpserver
import webob
from webob import dec
+"""Zuul main web app.
+
+Zuul supports HTTP requests directly against it for determining the
+change status. These responses are provided as json data structures.
+
+The supported urls are:
+
+ - /status: return a complex data structure that represents the entire
+ queue / pipeline structure of the system
+ - /status.json (backwards compatibility): same as /status
+ - /status/change/X,Y: return status just for gerrit change X,Y
+
+When returning status for a single gerrit change you will get an
+array of changes, they will not include the queue structure.
+"""
+
class WebApp(threading.Thread):
log = logging.getLogger("zuul.WebApp")
@@ -41,9 +60,44 @@ class WebApp(threading.Thread):
def stop(self):
self.server.server_close()
+ def _changes_by_func(self, func):
+ """Filter changes by a user provided function.
+
+ In order to support arbitrary collection of subsets of changes
+ we provide a low level filtering mechanism that takes a
+ function which applies to changes. The output of this function
+ is a flattened list of those collected changes.
+ """
+ status = []
+ jsonstruct = json.loads(self.cache)
+ for pipeline in jsonstruct['pipelines']:
+ for change_queue in pipeline['change_queues']:
+ for head in change_queue['heads']:
+ for change in head:
+ if func(change):
+ status.append(copy.deepcopy(change))
+ return json.dumps(status)
+
+ def _status_for_change(self, rev):
+ """Return the statuses for a particular change id X,Y."""
+ def func(change):
+ return change['id'] == rev
+ return self._changes_by_func(func)
+
+ def _normalize_path(self, path):
+ # support legacy status.json as well as new /status
+ if path == '/status.json' or path == '/status':
+ return "status"
+ m = re.match('/status/change/(\d+,\d+)$', path)
+ if m:
+ return m.group(1)
+ return None
+
def app(self, request):
- if request.path != '/status.json':
+ path = self._normalize_path(request.path)
+ if path is None:
raise webob.exc.HTTPNotFound()
+
if (not self.cache or
(time.time() - self.cache_time) > self.cache_expiry):
try:
@@ -54,8 +108,18 @@ class WebApp(threading.Thread):
except:
self.log.exception("Exception formatting status:")
raise
- response = webob.Response(body=self.cache,
- content_type='application/json')
+
+ if path == 'status':
+ response = webob.Response(body=self.cache,
+ content_type='application/json')
+ else:
+ status = self._status_for_change(path)
+ if status:
+ response = webob.Response(body=status,
+ content_type='application/json')
+ else:
+ raise webob.exc.HTTPNotFound()
+
response.headers['Access-Control-Allow-Origin'] = '*'
response.last_modified = self.cache_time
return response