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:
parent
e9a8184fe0
commit
a8311bf6a6
85
tests/test_webapp.py
Normal file
85
tests/test_webapp.py
Normal 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)
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user