Merge "Add script to generate openapi spec"
This commit is contained in:
commit
72849b2daa
|
@ -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
|
@ -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 = {
|
||||
|
|
Loading…
Reference in New Issue