Merge "Add script to generate openapi spec"

This commit is contained in:
Zuul 2024-03-18 19:12:29 +00:00 committed by Gerrit Code Review
commit 72849b2daa
3 changed files with 1564 additions and 713 deletions

167
tools/openapi_generate.py Normal file
View File

@ -0,0 +1,167 @@
# Copyright 2024 Acme Gating, LLC
#
# 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 inspect
import re
import yaml
from zuul.web import ZuulWeb, ZuulWebAPI
PARAM_RE = re.compile(':param (.*?) (.*?): (.*)')
spec = {
'info': {
'title': 'Zuul REST API',
'version': 'v1',
'description': 'Incomplete (work in progress) list of the endpoints.',
},
'openapi': '3.0.0',
'tags': [
{'name': 'tenant'},
],
'paths': {},
}
def parse_docstring(doc):
# This separates the overall method summary (to be used as the
# endpoint summary in the openapi spec) from parameter types and
# descriptions.
summary = []
params = {}
pname = None
ptype = None
pbuf = []
for line in doc.split('\n'):
line = line.strip()
if not line:
continue
m = PARAM_RE.match(line)
if m:
if pname:
params[pname] = {
'type': ptype,
'desc': ' '.join(pbuf),
}
ptype, pname, pdesc = m.groups()
if ptype == 'str':
ptype = 'string'
pbuf = [pdesc.strip()]
continue
if pname:
pbuf.append(line.strip())
else:
summary.append(line.strip())
if pname:
params[pname] = {
'type': ptype,
'desc': ' '.join(pbuf),
}
return ' '.join(summary), params
def generate_spec():
api = ZuulWebAPI
route_map = ZuulWeb.generateRouteMap(api, True)
# Iterate over each route
for r in route_map.mapper.matchlist:
# Some of our routes have globs in the variable names; remove
# those so that "{project:.*}" becomes "{project}" to match
# the function arguments.
routepath = r.routepath.replace(':.*', '')
# This is our output; initialize it to an empty dict, or if
# we're handling another instance of a previous route (for
# example, different GET and POST methods), start with that.
routespec = spec['paths'].setdefault(routepath, {})
# action is the ZuulWebAPI method name
action = r.defaults['action']
# handler is the ZuulWebAPI method itself
handler = getattr(api, action)
# Parse the docstring if available
doc = handler.__doc__
if doc:
summary, doc_params = parse_docstring(doc)
else:
summary = ''
doc_params = {}
if r.conditions and r.conditions.get('method'):
# If this route specifies methods, use that
methods = r.conditions['method']
else:
# Otherwise assume this method only handles GET
methods = ['GET']
for method in methods:
if method == 'OPTIONS':
continue
# The @openapi decorators set this attribute; initialize
# the output dictionary for this route-method to that
# value or the empty dict.
default_methodspec = {
'responses': {
'200': {
'description': 'Response not yet documented',
}
}
}
methodspec = getattr(handler, '__openapi__', default_methodspec)
routespec[method.lower()] = methodspec
methodspec['summary'] = summary
methodspec['operationId'] = action
methodspec['tags'] = ['tenant']
methodspec['parameters'] = []
# All inputs should be in the method signature, so iterate
# over that.
for handler_param in inspect.signature(
handler).parameters.values():
paramspec = {}
# See if this function argument appears in the route
# path, if so, it's a "path" parameter, otherwise it's
# a "query" param.
in_path = '{' + handler_param.name + '}' in routepath
required = False
if handler_param.default is handler_param.empty:
# No default value; it's either in the path or we
# don't care about it.
if not in_path:
continue
required = True
paramspec = {
'name': handler_param.name,
'in': 'path' if in_path else 'query',
}
# Merge in information from the docstring if available.
doc_param = doc_params.get(handler_param.name)
if doc_param:
paramspec['description'] = doc_param['desc']
paramspec['schema'] = {'type': doc_param['type']}
else:
paramspec['schema'] = {'type': 'string'}
if required:
paramspec['required'] = required
methodspec['parameters'].append(paramspec)
return spec
def main():
with open('web/public/openapi.yaml', 'w') as f:
f.write(yaml.safe_dump(generate_spec(),
default_flow_style=False))
if __name__ == '__main__':
main()

File diff suppressed because it is too large Load Diff

View File

@ -34,6 +34,7 @@ import threading
import uuid
import prometheus_client
import urllib.parse
import types
import zuul.executor.common
from zuul import exceptions
@ -108,6 +109,241 @@ def get_request_logger(logger=None):
return get_annotated_logger(logger, None, request=zuul_request_id)
def _datetimeToString(my_datetime):
if my_datetime:
return my_datetime.strftime('%Y-%m-%dT%H:%M:%S')
return None
class Prop:
def __init__(self, description, value):
"""A property of an OpenAPI schema.
:param str description: The description of this property
:param type value: The type of the property; either a native
Python scalar type, Prop instance, or list or dictionary of
the preceding.
"""
self.description = description
self.value = value
@staticmethod
def _toOpenAPI(value):
ret = {}
if isinstance(value, Prop):
ret['description'] = value.description
value = value.value
if isinstance(value, (list, tuple)):
ret['type'] = 'array'
ret['items'] = Prop._toOpenAPI(value[0])
elif isinstance(value, (types.MappingProxyType, dict)):
ret['type'] = 'object'
ret['properties'] = {
k: Prop._toOpenAPI(v) for (k, v) in value.items()
}
elif value is str:
ret['type'] = 'string'
elif value is int:
ret['type'] = 'integer'
elif value is float:
ret['type'] = 'number'
elif value is bool:
ret['type'] = 'boolean'
elif value is dict:
ret['type'] = 'object'
elif value is object:
ret['type'] = 'object'
return ret
def toOpenAPI(self):
"Convert this Prop to an OpenAPI schema."
return Prop._toOpenAPI(self.value)
class RefConverter:
# A class to encapsulate the conversion of database Ref objects to
# API output.
@staticmethod
def toDict(ref):
return {
'project': ref.project,
'branch': ref.branch,
'change': ref.change,
'patchset': ref.patchset,
'ref': ref.ref,
'oldrev': ref.oldrev,
'newrev': ref.newrev,
'ref_url': ref.ref_url,
}
@staticmethod
def schema():
return Prop('The ref', {
'project': str,
'branch': str,
'change': str,
'patchset': str,
'ref': str,
'oldrev': str,
'newrev': str,
'ref_url': str,
})
class BuildConverter:
# A class to encapsulate the conversion of database Build objects to
# API output.
def toDict(build, buildset=None, skip_refs=False):
start_time = _datetimeToString(build.start_time)
end_time = _datetimeToString(build.end_time)
if build.start_time and build.end_time:
duration = (build.end_time -
build.start_time).total_seconds()
else:
duration = None
ret = {
'_id': build.id,
'uuid': build.uuid,
'job_name': build.job_name,
'result': build.result,
'held': build.held,
'start_time': start_time,
'end_time': end_time,
'duration': duration,
'voting': build.voting,
'log_url': build.log_url,
'nodeset': build.nodeset,
'error_detail': build.error_detail,
'final': build.final,
'artifacts': [],
'provides': [],
'ref': RefConverter.toDict(build.ref),
}
if buildset:
# We enter this branch if we're returning top-level build
# objects (ie, not builds under a buildset).
event_timestamp = _datetimeToString(buildset.event_timestamp)
ret.update({
'pipeline': buildset.pipeline,
'event_id': buildset.event_id,
'event_timestamp': event_timestamp,
'buildset': {
'uuid': buildset.uuid,
},
})
if not skip_refs:
ret['buildset']['refs'] = [
RefConverter.toDict(ref)
for ref in buildset.refs
]
for artifact in build.artifacts:
art = {
'name': artifact.name,
'url': artifact.url,
}
if artifact.meta:
art['metadata'] = json.loads(artifact.meta)
ret['artifacts'].append(art)
for provides in build.provides:
ret['provides'].append({
'name': provides.name,
})
return ret
@staticmethod
def schema(buildset=False, skip_refs=False):
ret = {
'_id': str,
'uuid': str,
'job_name': str,
'result': str,
'held': str,
'start_time': str,
'end_time': str,
'duration': str,
'voting': str,
'log_url': str,
'nodeset': str,
'error_detail': str,
'final': str,
'artifacts': [{
'name': str,
'url': str,
'metadata': dict,
}],
'provides': [{
'name': str,
}],
'ref': RefConverter.schema(),
}
if buildset:
# We enter this branch if we're returning top-level build
# objects (ie, not builds under a buildset).
ret.update({
'pipeline': str,
'event_id': str,
'event_timestamp': str,
'buildset': {
'uuid': str,
},
})
if not skip_refs:
ret['buildset']['refs'] = [RefConverter.schema()]
ret = Prop('The build', ret)
return ret
class BuildsetConverter:
# A class to encapsulate the conversion of database Buildset
# objects to API output.
def toDict(buildset, builds=[]):
event_timestamp = _datetimeToString(buildset.event_timestamp)
start = _datetimeToString(buildset.first_build_start_time)
end = _datetimeToString(buildset.last_build_end_time)
ret = {
'_id': buildset.id,
'uuid': buildset.uuid,
'result': buildset.result,
'message': buildset.message,
'pipeline': buildset.pipeline,
'event_id': buildset.event_id,
'event_timestamp': event_timestamp,
'first_build_start_time': start,
'last_build_end_time': end,
'refs': [
RefConverter.toDict(ref)
for ref in buildset.refs
],
}
if builds:
ret['builds'] = []
for build in builds:
ret['builds'].append(BuildConverter.toDict(build))
return ret
def schema(builds=False):
ret = {
'_id': str,
'uuid': str,
'result': str,
'message': str,
'pipeline': str,
'event_id': str,
'event_timestamp': str,
'first_build_start_time': str,
'last_build_end_time': str,
'refs': [
RefConverter.schema()
],
}
if builds:
ret['builds'] = [BuildConverter.schema()]
return Prop('The buildset', ret)
class APIError(cherrypy.HTTPError):
def __init__(self, code, json_doc=None, headers=None):
self._headers = headers or {}
@ -179,6 +415,42 @@ cherrypy.tools.handle_options = cherrypy.Tool('on_start_resource',
priority=50)
def openapi_response(
code,
description=None,
content_type=None,
example=None,
schema=None,
):
"""Describe an OpenAPI response
:param int code: The HTTP response code
:param str description: A description for the response
:param str content_type: The HTTP content type
:param str example: An example of the response output
:param Prop schema: A Prop describing the returned schema
"""
response_spec = {}
if description:
response_spec['description'] = description
if content_type:
content_spec = {}
response_spec['content'] = {content_type: content_spec}
if example:
content_spec['example'] = example
if schema:
content_spec['schema'] = schema.toOpenAPI()
def decorator(func):
if not hasattr(func, '__openapi__'):
func.__openapi__ = {}
func.__openapi__.setdefault('responses', {})
func.__openapi__['responses'][code] = response_spec
return func
return decorator
class AuthInfo:
def __init__(self, uid, admin):
self.uid = uid
@ -1069,6 +1341,17 @@ class ZuulWebAPI(object):
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
@cherrypy.tools.handle_options()
@cherrypy.tools.check_root_auth()
@openapi_response(
code=200,
content_type='application/json',
description='Returns the list of tenants',
schema=Prop('The list of tenants', [{
'name': Prop('Tenant name', str),
'projects': Prop('Tenant project count', int),
'queue': Prop('Active changes count', int),
}]),
)
@openapi_response(404, description='Tenant not found')
def tenants(self, auth):
cache_time = self.tenants_cache_time
if time.time() - cache_time > self.cache_expiry:
@ -1204,6 +1487,12 @@ class ZuulWebAPI(object):
@cherrypy.tools.handle_options()
@cherrypy.tools.check_tenant_auth()
def status(self, tenant_name, tenant, auth):
"""Return the tenant status.
Note: the output format is not currently documented and
subject to change without notice.
"""
return self._getStatus(tenant)[1]
@cherrypy.expose
@ -1212,6 +1501,12 @@ class ZuulWebAPI(object):
@cherrypy.tools.handle_options()
@cherrypy.tools.check_tenant_auth()
def status_change(self, tenant_name, tenant, auth, change):
"""Return the status for a single change.
Note: the output format is not currently documented and
subject to change without notice.
"""
payload = self._getStatus(tenant)[0]
result_filter = RefFilter(change)
return result_filter.filterPayload(payload)
@ -1223,6 +1518,21 @@ class ZuulWebAPI(object):
)
@cherrypy.tools.handle_options()
@cherrypy.tools.check_tenant_auth()
@openapi_response(
code=200,
content_type='application/json',
description='Returns the list of jobs',
schema=Prop('The list of jobs', [{
'name': str,
'description': str,
'tags': [str],
'variants': [{
'parent': str,
'branches': [str],
}],
}]),
)
@openapi_response(404, description='Tenant not found')
def jobs(self, tenant_name, tenant, auth):
result = []
for job_name in sorted(tenant.layout.jobs):
@ -1435,6 +1745,18 @@ class ZuulWebAPI(object):
@cherrypy.tools.save_params()
@cherrypy.tools.handle_options()
@cherrypy.tools.check_tenant_auth()
@openapi_response(
code=200,
content_type='text/plain',
description=('Returns the project public key that is used '
'to encrypt secrets'),
example=('-----BEGIN PUBLIC KEY-----\n'
'MIICI...\n'
'-----END PUBLIC KEY-----\n'),
schema=Prop('The project secrets public key '
'in PKCS8 format', str)
)
@openapi_response(404, 'Tenant or Project not found')
def key(self, tenant_name, tenant, auth, project_name):
project = self._getProjectOrRaise(tenant, project_name)
@ -1447,6 +1769,16 @@ class ZuulWebAPI(object):
@cherrypy.tools.save_params()
@cherrypy.tools.handle_options()
@cherrypy.tools.check_tenant_auth()
@openapi_response(
code=200,
content_type='text/plain',
description=('Returns the project public key that executor '
'adds to SSH agent'),
example='ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACA',
schema=Prop('The project secrets public key '
'in SSH2 format', str),
)
@openapi_response(404, 'Tenant or Project not found')
def project_ssh_key(self, tenant_name, tenant, auth, project_name):
project = self._getProjectOrRaise(tenant, project_name)
@ -1455,82 +1787,6 @@ class ZuulWebAPI(object):
resp.headers['Content-Type'] = 'text/plain'
return key
def _datetimeToString(self, my_datetime):
if my_datetime:
return my_datetime.strftime('%Y-%m-%dT%H:%M:%S')
return None
def refToDict(self, ref):
return {
'project': ref.project,
'branch': ref.branch,
'change': ref.change,
'patchset': ref.patchset,
'ref': ref.ref,
'oldrev': ref.oldrev,
'newrev': ref.newrev,
'ref_url': ref.ref_url,
}
def buildToDict(self, build, buildset=None, skip_refs=False):
start_time = self._datetimeToString(build.start_time)
end_time = self._datetimeToString(build.end_time)
if build.start_time and build.end_time:
duration = (build.end_time -
build.start_time).total_seconds()
else:
duration = None
ret = {
'_id': build.id,
'uuid': build.uuid,
'job_name': build.job_name,
'result': build.result,
'held': build.held,
'start_time': start_time,
'end_time': end_time,
'duration': duration,
'voting': build.voting,
'log_url': build.log_url,
'nodeset': build.nodeset,
'error_detail': build.error_detail,
'final': build.final,
'artifacts': [],
'provides': [],
'ref': self.refToDict(build.ref),
}
if buildset:
# We enter this branch if we're returning top-level build
# objects (ie, not builds under a buildset).
event_timestamp = self._datetimeToString(buildset.event_timestamp)
ret.update({
'pipeline': buildset.pipeline,
'event_id': buildset.event_id,
'event_timestamp': event_timestamp,
'buildset': {
'uuid': buildset.uuid,
},
})
if not skip_refs:
ret['buildset']['refs'] = [
self.refToDict(ref)
for ref in buildset.refs
]
for artifact in build.artifacts:
art = {
'name': artifact.name,
'url': artifact.url,
}
if artifact.meta:
art['metadata'] = json.loads(artifact.meta)
ret['artifacts'].append(art)
for provides in build.provides:
ret['provides'].append({
'name': provides.name,
})
return ret
def _get_connection(self):
return self.zuulweb.connections.connections['database']
@ -1539,12 +1795,23 @@ class ZuulWebAPI(object):
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
@cherrypy.tools.handle_options()
@cherrypy.tools.check_tenant_auth()
@openapi_response(
code=200,
content_type='application/json',
description='Returns the list of builds',
schema=Prop('The list of builds', [BuildConverter.schema()]),
)
@openapi_response(404, 'Tenant not found')
def builds(self, tenant_name, tenant, auth, project=None,
pipeline=None, change=None, branch=None, patchset=None,
ref=None, newrev=None, uuid=None, job_name=None,
voting=None, nodeset=None, result=None, final=None,
held=None, complete=None, limit=50, skip=0,
idx_min=None, idx_max=None, exclude_result=None):
"""
List the executed builds
"""
connection = self._get_connection()
if tenant_name not in self.zuulweb.abide.tenants.keys():
@ -1574,7 +1841,7 @@ class ZuulWebAPI(object):
idx_max=_idx_max, exclude_result=exclude_result,
query_timeout=self.query_timeout)
return [self.buildToDict(b, b.buildset, skip_refs=True)
return [BuildConverter.toDict(b, b.buildset, skip_refs=True)
for b in builds]
@cherrypy.expose
@ -1588,12 +1855,12 @@ class ZuulWebAPI(object):
data = connection.getBuild(tenant_name, uuid)
if not data:
raise cherrypy.HTTPError(404, "Build not found")
data = self.buildToDict(data, data.buildset)
data = BuildConverter.toDict(data, data.buildset)
return data
def buildTimeToDict(self, build):
start_time = self._datetimeToString(build.start_time)
end_time = self._datetimeToString(build.end_time)
start_time = _datetimeToString(build.start_time)
end_time = _datetimeToString(build.end_time)
if build.start_time and build.end_time:
duration = (build.end_time -
build.start_time).total_seconds()
@ -1652,37 +1919,31 @@ class ZuulWebAPI(object):
return [self.buildTimeToDict(b) for b in build_times]
def buildsetToDict(self, buildset, builds=[]):
event_timestamp = self._datetimeToString(buildset.event_timestamp)
start = self._datetimeToString(buildset.first_build_start_time)
end = self._datetimeToString(buildset.last_build_end_time)
ret = {
'_id': buildset.id,
'uuid': buildset.uuid,
'result': buildset.result,
'message': buildset.message,
'pipeline': buildset.pipeline,
'event_id': buildset.event_id,
'event_timestamp': event_timestamp,
'first_build_start_time': start,
'last_build_end_time': end,
'refs': [
self.refToDict(ref)
for ref in buildset.refs
],
}
if builds:
ret['builds'] = []
for build in builds:
ret['builds'].append(self.buildToDict(build))
return ret
@cherrypy.expose
@cherrypy.tools.save_params()
@cherrypy.tools.handle_options()
@cherrypy.tools.check_tenant_auth()
@openapi_response(
code=200,
content_type='image/svg+xml',
description=('Badge describing the result of '
'the latest buildset found.'),
schema=Prop('SVG image', object),
)
@openapi_response(404, 'No buildset found')
def badge(self, tenant_name, tenant, auth, project=None,
pipeline=None, branch=None):
"""
Get a badge describing the result of the latest buildset found.
:param str tenant_name: The tenant name
:param Tenant tenant: The tenant object
:param AuthInfo auth: The auth object
:param str project: A project name
:param str pipeline: A pipeline name
:param str branch: A branch name
"""
connection = self._get_connection()
buildsets = connection.getBuildsets(
@ -1709,6 +1970,13 @@ class ZuulWebAPI(object):
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
@cherrypy.tools.handle_options()
@cherrypy.tools.check_tenant_auth()
@openapi_response(
code=200,
content_type='application/json',
description='Returns the list of buildsets',
schema=Prop('The list of buildsets', [BuildsetConverter.schema()]),
)
@openapi_response(404, 'Tenant not found')
def buildsets(self, tenant_name, tenant, auth, project=None,
pipeline=None, change=None, branch=None,
patchset=None, ref=None, newrev=None, uuid=None,
@ -1732,7 +2000,7 @@ class ZuulWebAPI(object):
limit=limit, offset=skip, idx_min=_idx_min, idx_max=_idx_max,
query_timeout=self.query_timeout)
return [self.buildsetToDict(b) for b in buildsets]
return [BuildsetConverter.toDict(b) for b in buildsets]
@cherrypy.expose
@cherrypy.tools.save_params()
@ -1745,7 +2013,7 @@ class ZuulWebAPI(object):
data = connection.getBuildset(tenant_name, uuid)
if not data:
raise cherrypy.HTTPError(404, "Buildset not found")
data = self.buildsetToDict(data, data.builds)
data = BuildsetConverter.toDict(data, data.builds)
return data
@cherrypy.expose
@ -1755,6 +2023,24 @@ class ZuulWebAPI(object):
)
@cherrypy.tools.handle_options()
@cherrypy.tools.check_tenant_auth()
@openapi_response(
code=200,
description='Returns the list of semaphores',
schema=Prop('The list of semaphores', [{
'name': str,
'global': bool,
'max': int,
'holders': {
'count': int,
'other_tenants': int,
'this_tenant': [{
'buildset_uuid': str,
'job_name': str,
}],
},
}]),
)
@openapi_response(404, 'Tenant not found')
def semaphores(self, tenant_name, tenant, auth):
result = []
names = set(tenant.layout.semaphores.keys())
@ -2022,6 +2308,132 @@ class ZuulWeb(object):
log = logging.getLogger("zuul.web")
tracer = trace.get_tracer("zuul")
@staticmethod
def generateRouteMap(api, include_auth):
route_map = cherrypy.dispatch.RoutesDispatcher()
route_map.connect('api', '/api',
controller=api, action='index')
route_map.connect('api', '/api/info',
controller=api, action='info')
route_map.connect('api', '/api/connections',
controller=api, action='connections')
route_map.connect('api', '/api/components',
controller=api, action='components')
route_map.connect('api', '/api/tenants',
controller=api, action='tenants')
route_map.connect('api', '/api/tenant/{tenant_name}/info',
controller=api, action='tenant_info')
route_map.connect('api', '/api/tenant/{tenant_name}/status',
controller=api, action='status')
route_map.connect('api', '/api/tenant/{tenant_name}/status/change'
'/{change}',
controller=api, action='status_change')
route_map.connect('api', '/api/tenant/{tenant_name}/semaphores',
controller=api, action='semaphores')
route_map.connect('api', '/api/tenant/{tenant_name}/jobs',
controller=api, action='jobs')
route_map.connect('api', '/api/tenant/{tenant_name}/job/{job_name}',
controller=api, action='job')
# if no auth configured, deactivate admin routes
if include_auth:
# route order is important, put project actions before the more
# generic tenant/{tenant_name}/project/{project} route
route_map.connect('api',
'/api/tenant/{tenant_name}/authorizations',
controller=api,
action='tenant_authorizations')
route_map.connect('api',
'/api/authorizations',
controller=api,
action='root_authorizations')
route_map.connect('api', '/api/tenant/{tenant_name}/promote',
controller=api, action='promote')
route_map.connect(
'api',
'/api/tenant/{tenant_name}/project/{project_name:.*}/autohold',
controller=api,
conditions=dict(method=['GET', 'OPTIONS']),
action='autohold_project_get')
route_map.connect(
'api',
'/api/tenant/{tenant_name}/project/{project_name:.*}/autohold',
controller=api,
conditions=dict(method=['POST']),
action='autohold_project_post')
route_map.connect(
'api',
'/api/tenant/{tenant_name}/project/{project_name:.*}/enqueue',
controller=api, action='enqueue')
route_map.connect(
'api',
'/api/tenant/{tenant_name}/project/{project_name:.*}/dequeue',
controller=api, action='dequeue')
route_map.connect('api',
'/api/tenant/{tenant_name}/autohold/{request_id}',
controller=api,
conditions=dict(method=['GET', 'OPTIONS']),
action='autohold_get')
route_map.connect('api',
'/api/tenant/{tenant_name}/autohold/{request_id}',
controller=api,
conditions=dict(method=['DELETE']),
action='autohold_delete')
route_map.connect('api', '/api/tenant/{tenant_name}/autohold',
controller=api, action='autohold_list')
route_map.connect('api', '/api/tenant/{tenant_name}/projects',
controller=api, action='projects')
route_map.connect('api', '/api/tenant/{tenant_name}/project/'
'{project_name:.*}',
controller=api, action='project')
route_map.connect(
'api',
'/api/tenant/{tenant_name}/pipeline/{pipeline_name}'
'/project/{project_name:.*}/branch/{branch_name:.*}/freeze-jobs',
controller=api, action='project_freeze_jobs'
)
route_map.connect(
'api',
'/api/tenant/{tenant_name}/pipeline/{pipeline_name}'
'/project/{project_name:.*}/branch/{branch_name:.*}'
'/freeze-job/{job_name}',
controller=api, action='project_freeze_job'
)
route_map.connect('api', '/api/tenant/{tenant_name}/pipelines',
controller=api, action='pipelines')
route_map.connect('api', '/api/tenant/{tenant_name}/labels',
controller=api, action='labels')
route_map.connect('api', '/api/tenant/{tenant_name}/nodes',
controller=api, action='nodes')
route_map.connect('api', '/api/tenant/{tenant_name}/key/'
'{project_name:.*}.pub',
controller=api, action='key')
route_map.connect('api', '/api/tenant/{tenant_name}/'
'project-ssh-key/{project_name:.*}.pub',
controller=api, action='project_ssh_key')
route_map.connect('api', '/api/tenant/{tenant_name}/console-stream',
controller=api, action='console_stream_get',
conditions=dict(method=['GET']))
route_map.connect('api', '/api/tenant/{tenant_name}/console-stream',
controller=api, action='console_stream_options',
conditions=dict(method=['OPTIONS']))
route_map.connect('api', '/api/tenant/{tenant_name}/builds',
controller=api, action='builds')
route_map.connect('api', '/api/tenant/{tenant_name}/badge',
controller=api, action='badge')
route_map.connect('api', '/api/tenant/{tenant_name}/build/{uuid}',
controller=api, action='build')
route_map.connect('api', '/api/tenant/{tenant_name}/buildsets',
controller=api, action='buildsets')
route_map.connect('api', '/api/tenant/{tenant_name}/buildset/{uuid}',
controller=api, action='buildset')
route_map.connect('api', '/api/tenant/{tenant_name}/build-times',
controller=api, action='build_times')
route_map.connect('api', '/api/tenant/{tenant_name}/config-errors',
controller=api, action='config_errors')
route_map.connect('api', '/api/tenant/{tenant_name}/tenant-status',
controller=api, action='tenant_status')
return route_map
def __init__(self,
config,
connections,
@ -2130,130 +2542,15 @@ class ZuulWeb(object):
self.finger_tls_verify_hostnames = get_default(
self.config, 'fingergw', 'tls_verify_hostnames', default=True)
route_map = cherrypy.dispatch.RoutesDispatcher()
api = ZuulWebAPI(self)
self.api = api
route_map.connect('api', '/api',
controller=api, action='index')
route_map.connect('api', '/api/info',
controller=api, action='info')
route_map.connect('api', '/api/connections',
controller=api, action='connections')
route_map.connect('api', '/api/components',
controller=api, action='components')
route_map.connect('api', '/api/tenants',
controller=api, action='tenants')
route_map.connect('api', '/api/tenant/{tenant_name}/info',
controller=api, action='tenant_info')
route_map.connect('api', '/api/tenant/{tenant_name}/status',
controller=api, action='status')
route_map.connect('api', '/api/tenant/{tenant_name}/status/change'
'/{change}',
controller=api, action='status_change')
route_map.connect('api', '/api/tenant/{tenant_name}/semaphores',
controller=api, action='semaphores')
route_map.connect('api', '/api/tenant/{tenant_name}/jobs',
controller=api, action='jobs')
route_map.connect('api', '/api/tenant/{tenant_name}/job/{job_name}',
controller=api, action='job')
# if no auth configured, deactivate admin routes
if self.authenticators.authenticators:
# route order is important, put project actions before the more
# generic tenant/{tenant_name}/project/{project} route
route_map.connect('api',
'/api/tenant/{tenant_name}/authorizations',
controller=api,
action='tenant_authorizations')
route_map.connect('api',
'/api/authorizations',
controller=api,
action='root_authorizations')
route_map.connect('api', '/api/tenant/{tenant_name}/promote',
controller=api, action='promote')
route_map.connect(
'api',
'/api/tenant/{tenant_name}/project/{project_name:.*}/autohold',
controller=api,
conditions=dict(method=['GET', 'OPTIONS']),
action='autohold_project_get')
route_map.connect(
'api',
'/api/tenant/{tenant_name}/project/{project_name:.*}/autohold',
controller=api,
conditions=dict(method=['POST']),
action='autohold_project_post')
route_map.connect(
'api',
'/api/tenant/{tenant_name}/project/{project_name:.*}/enqueue',
controller=api, action='enqueue')
route_map.connect(
'api',
'/api/tenant/{tenant_name}/project/{project_name:.*}/dequeue',
controller=api, action='dequeue')
route_map.connect('api',
'/api/tenant/{tenant_name}/autohold/{request_id}',
controller=api,
conditions=dict(method=['GET', 'OPTIONS']),
action='autohold_get')
route_map.connect('api',
'/api/tenant/{tenant_name}/autohold/{request_id}',
controller=api,
conditions=dict(method=['DELETE']),
action='autohold_delete')
route_map.connect('api', '/api/tenant/{tenant_name}/autohold',
controller=api, action='autohold_list')
route_map.connect('api', '/api/tenant/{tenant_name}/projects',
controller=api, action='projects')
route_map.connect('api', '/api/tenant/{tenant_name}/project/'
'{project_name:.*}',
controller=api, action='project')
route_map = self.generateRouteMap(
api, bool(self.authenticators.authenticators))
# Add fallthrough routes at the end for the static html/js files
route_map.connect(
'api',
'/api/tenant/{tenant_name}/pipeline/{pipeline_name}'
'/project/{project_name:.*}/branch/{branch_name:.*}/freeze-jobs',
controller=api, action='project_freeze_jobs'
)
route_map.connect(
'api',
'/api/tenant/{tenant_name}/pipeline/{pipeline_name}'
'/project/{project_name:.*}/branch/{branch_name:.*}'
'/freeze-job/{job_name}',
controller=api, action='project_freeze_job'
)
route_map.connect('api', '/api/tenant/{tenant_name}/pipelines',
controller=api, action='pipelines')
route_map.connect('api', '/api/tenant/{tenant_name}/labels',
controller=api, action='labels')
route_map.connect('api', '/api/tenant/{tenant_name}/nodes',
controller=api, action='nodes')
route_map.connect('api', '/api/tenant/{tenant_name}/key/'
'{project_name:.*}.pub',
controller=api, action='key')
route_map.connect('api', '/api/tenant/{tenant_name}/'
'project-ssh-key/{project_name:.*}.pub',
controller=api, action='project_ssh_key')
route_map.connect('api', '/api/tenant/{tenant_name}/console-stream',
controller=api, action='console_stream_get',
conditions=dict(method=['GET']))
route_map.connect('api', '/api/tenant/{tenant_name}/console-stream',
controller=api, action='console_stream_options',
conditions=dict(method=['OPTIONS']))
route_map.connect('api', '/api/tenant/{tenant_name}/builds',
controller=api, action='builds')
route_map.connect('api', '/api/tenant/{tenant_name}/badge',
controller=api, action='badge')
route_map.connect('api', '/api/tenant/{tenant_name}/build/{uuid}',
controller=api, action='build')
route_map.connect('api', '/api/tenant/{tenant_name}/buildsets',
controller=api, action='buildsets')
route_map.connect('api', '/api/tenant/{tenant_name}/buildset/{uuid}',
controller=api, action='buildset')
route_map.connect('api', '/api/tenant/{tenant_name}/build-times',
controller=api, action='build_times')
route_map.connect('api', '/api/tenant/{tenant_name}/config-errors',
controller=api, action='config_errors')
route_map.connect('api', '/api/tenant/{tenant_name}/tenant-status',
controller=api, action='tenant_status')
'root_static', '/{path:.*}',
controller=StaticHandler(self.static_path),
action='default')
for connection in connections.connections.values():
controller = connection.getWebController(self)
@ -2262,12 +2559,6 @@ class ZuulWeb(object):
controller,
'/api/connection/%s' % connection.connection_name)
# Add fallthrough routes at the end for the static html/js files
route_map.connect(
'root_static', '/{path:.*}',
controller=StaticHandler(self.static_path),
action='default')
cherrypy.tools.stats = StatsTool(self.statsd, self.metrics)
conf = {