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
This commit is contained in:
Sean Dague 2014-09-30 06:28:26 -04:00
parent e9a8184fe0
commit a8311bf6a6
2 changed files with 152 additions and 3 deletions

85
tests/test_webapp.py Normal file
View File

@ -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)

View File

@ -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