diff --git a/tools/openapi_generate.py b/tools/openapi_generate.py new file mode 100644 index 0000000000..120e9130a3 --- /dev/null +++ b/tools/openapi_generate.py @@ -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() diff --git a/web/public/openapi.yaml b/web/public/openapi.yaml index cb2e10b37e..5562f223a9 100644 --- a/web/public/openapi.yaml +++ b/web/public/openapi.yaml @@ -1,18 +1,135 @@ info: + description: Incomplete (work in progress) list of the endpoints. title: Zuul REST API version: v1 - description: Incomplete (work in progress) list of the endpoints. openapi: 3.0.0 -tags: -- name: tenant paths: - /api/tenant/{tenant}/badge: + /api: get: - operationId: get-badge + operationId: index + parameters: [] + responses: + '200': + description: Response not yet documented + summary: '' + tags: + - tenant + /api/authorizations: + get: + operationId: root_authorizations + parameters: [] + responses: + '200': + description: Response not yet documented + summary: '' + tags: + - tenant + /api/components: + get: + operationId: components + parameters: [] + responses: + '200': + description: Response not yet documented + summary: '' + tags: + - tenant + /api/connections: + get: + operationId: connections + parameters: [] + responses: + '200': + description: Response not yet documented + summary: '' + tags: + - tenant + /api/info: + get: + operationId: info + parameters: [] + responses: + '200': + description: Response not yet documented + summary: '' + tags: + - tenant + /api/tenant/{tenant_name}/authorizations: + get: + operationId: tenant_authorizations + parameters: + - in: path + name: tenant_name + required: true + schema: + type: string + responses: + '200': + description: Response not yet documented + summary: '' + tags: + - tenant + /api/tenant/{tenant_name}/autohold: + get: + operationId: autohold_list + parameters: + - in: path + name: tenant_name + required: true + schema: + type: string + responses: + '200': + description: Response not yet documented + summary: '' + tags: + - tenant + /api/tenant/{tenant_name}/autohold/{request_id}: + delete: + operationId: autohold_delete + parameters: + - in: path + name: tenant_name + required: true + schema: + type: string + - in: path + name: request_id + required: true + schema: + type: string + responses: + '200': + description: Response not yet documented + summary: '' + tags: + - tenant + get: + operationId: autohold_get + parameters: + - in: path + name: tenant_name + required: true + schema: + type: string + - in: path + name: request_id + required: true + schema: + type: string + responses: + '200': + description: Response not yet documented + summary: '' + tags: + - tenant + /api/tenant/{tenant_name}/badge: + get: + operationId: badge parameters: - description: The tenant name in: path - name: tenant + name: tenant_name required: true schema: type: string @@ -32,239 +149,554 @@ paths: schema: type: string responses: - '200': + 200: content: image/svg+xml: schema: - description: SVG image type: object description: Badge describing the result of the latest buildset found. - '404': + 404: description: No buildset found summary: Get a badge describing the result of the latest buildset found. tags: - tenant - /api/tenant/{tenant}/builds: + /api/tenant/{tenant_name}/build-times: get: - operationId: list-builds + operationId: build_times parameters: - - description: The tenant name - in: path - name: tenant + - in: path + name: tenant_name required: true schema: type: string - - description: A project name - in: query + - in: query name: project schema: type: string - - description: A pipeline name - in: query + - in: query name: pipeline schema: type: string - - description: A job name - in: query + - in: query + name: branch + schema: + type: string + - in: query + name: ref + schema: + type: string + - in: query name: job_name schema: type: string - - description: A branch name - in: query - name: branch + - in: query + name: final schema: type: string - - description: A change number - in: query - name: change + - in: query + name: start_time schema: type: string - - description: A patchset number - in: query - name: patchset + - in: query + name: end_time schema: type: string - - description: A ref - in: query - name: ref - schema: - type: string - - description: A new revision hash - in: query - name: newrev - schema: - type: string - - description: A build uuid - in: query - name: uuid - schema: - type: string - - description: A build voting status - in: query - name: voting - schema: - type: string - - description: A build result - in: query - name: result - schema: - type: string - - description: The limit count (default 50) - in: query + - in: query name: limit schema: type: string - - description: Skip number of results - in: query + - in: query name: skip schema: type: string + - in: query + name: exclude_result + schema: + type: string responses: '200': - content: - application/json: - schema: - description: The list of builds - items: - $ref: '#/components/schemas/build' - type: array - description: Returns the list of builds - '404': - description: Tenant not found - summary: List the executed builds + description: Response not yet documented + summary: '' tags: - tenant - /api/tenant/{tenant}/buildsets: + /api/tenant/{tenant_name}/build/{uuid}: get: - operationId: list-buildsets + operationId: build parameters: - - description: The tenant name - in: path - name: tenant + - in: path + name: tenant_name required: true schema: type: string - - description: A project name - in: query + - in: path + name: uuid + required: true + schema: + type: string + responses: + '200': + description: Response not yet documented + summary: '' + tags: + - tenant + /api/tenant/{tenant_name}/builds: + get: + operationId: builds + parameters: + - in: path + name: tenant_name + required: true + schema: + type: string + - in: query name: project schema: type: string - - description: A pipeline name - in: query + - in: query name: pipeline schema: type: string - - description: A branch name - in: query - name: branch - schema: - type: string - - description: A change number - in: query + - in: query name: change schema: type: string - - description: A patchset number - in: query + - in: query + name: branch + schema: + type: string + - in: query name: patchset schema: type: string - - description: A ref - in: query + - in: query name: ref schema: type: string - - description: A new revision hash - in: query + - in: query name: newrev schema: type: string - - description: A buildset uuid - in: query + - in: query name: uuid schema: type: string - - description: A buildset result - in: query + - in: query + name: job_name + schema: + type: string + - in: query + name: voting + schema: + type: string + - in: query + name: nodeset + schema: + type: string + - in: query name: result schema: type: string - - description: The limit count (default 50) - in: query + - in: query + name: final + schema: + type: string + - in: query + name: held + schema: + type: string + - in: query + name: complete + schema: + type: string + - in: query name: limit schema: type: string - - description: Skip number of results - in: query + - in: query + name: skip + schema: + type: string + - in: query + name: idx_min + schema: + type: string + - in: query + name: idx_max + schema: + type: string + - in: query + name: exclude_result + schema: + type: string + responses: + 200: + content: + application/json: + schema: + items: + description: The build + properties: + _id: + type: string + artifacts: + items: + properties: + metadata: + type: object + name: + type: string + url: + type: string + type: object + type: array + duration: + type: string + end_time: + type: string + error_detail: + type: string + final: + type: string + held: + type: string + job_name: + type: string + log_url: + type: string + nodeset: + type: string + provides: + items: + properties: + name: + type: string + type: object + type: array + ref: + description: The ref + properties: + branch: + type: string + change: + type: string + newrev: + type: string + oldrev: + type: string + patchset: + type: string + project: + type: string + ref: + type: string + ref_url: + type: string + type: object + result: + type: string + start_time: + type: string + uuid: + type: string + voting: + type: string + type: object + type: array + description: Returns the list of builds + 404: + description: Tenant not found + summary: List the executed builds + tags: + - tenant + /api/tenant/{tenant_name}/buildset/{uuid}: + get: + operationId: buildset + parameters: + - in: path + name: tenant_name + required: true + schema: + type: string + - in: path + name: uuid + required: true + schema: + type: string + responses: + '200': + description: Response not yet documented + summary: '' + tags: + - tenant + /api/tenant/{tenant_name}/buildsets: + get: + operationId: buildsets + parameters: + - in: path + name: tenant_name + required: true + schema: + type: string + - in: query + name: project + schema: + type: string + - in: query + name: pipeline + schema: + type: string + - in: query + name: change + schema: + type: string + - in: query + name: branch + schema: + type: string + - in: query + name: patchset + schema: + type: string + - in: query + name: ref + schema: + type: string + - in: query + name: newrev + schema: + type: string + - in: query + name: uuid + schema: + type: string + - in: query + name: result + schema: + type: string + - in: query + name: complete + schema: + type: string + - in: query + name: limit + schema: + type: string + - in: query + name: skip + schema: + type: string + - in: query + name: idx_min + schema: + type: string + - in: query + name: idx_max + schema: + type: string + responses: + 200: + content: + application/json: + schema: + items: + description: The buildset + properties: + _id: + type: string + event_id: + type: string + event_timestamp: + type: string + first_build_start_time: + type: string + last_build_end_time: + type: string + message: + type: string + pipeline: + type: string + refs: + items: + description: The ref + properties: + branch: + type: string + change: + type: string + newrev: + type: string + oldrev: + type: string + patchset: + type: string + project: + type: string + ref: + type: string + ref_url: + type: string + type: object + type: array + result: + type: string + uuid: + type: string + type: object + type: array + description: Returns the list of buildsets + 404: + description: Tenant not found + summary: '' + tags: + - tenant + /api/tenant/{tenant_name}/config-errors: + get: + operationId: config_errors + parameters: + - in: path + name: tenant_name + required: true + schema: + type: string + - in: query + name: project + schema: + type: string + - in: query + name: branch + schema: + type: string + - in: query + name: severity + schema: + type: string + - in: query + name: name + schema: + type: string + - in: query + name: limit + schema: + type: string + - in: query name: skip schema: type: string responses: '200': - content: - application/json: - schema: - description: The list of buildsets - items: - $ref: '#/components/schemas/buildset' - type: array - description: Returns the list of builds - '404': - description: Tenant not found - summary: List the executed builds + description: Response not yet documented + summary: '' tags: - tenant - /api/tenant/{tenant}/jobs: + /api/tenant/{tenant_name}/console-stream: get: - operationId: list-jobs + operationId: console_stream_get parameters: - - description: The tenant name - in: path - name: tenant + - in: path + name: tenant_name required: true schema: type: string responses: '200': + description: Response not yet documented + summary: '' + tags: + - tenant + /api/tenant/{tenant_name}/info: + get: + operationId: tenant_info + parameters: + - in: path + name: tenant_name + required: true + schema: + type: string + responses: + '200': + description: Response not yet documented + summary: '' + tags: + - tenant + /api/tenant/{tenant_name}/job/{job_name}: + get: + operationId: job + parameters: + - in: path + name: tenant_name + required: true + schema: + type: string + - in: path + name: job_name + required: true + schema: + type: string + responses: + '200': + description: Response not yet documented + summary: '' + tags: + - tenant + /api/tenant/{tenant_name}/jobs: + get: + operationId: jobs + parameters: + - in: path + name: tenant_name + required: true + schema: + type: string + responses: + 200: content: application/json: schema: - description: The list of jobs items: - description: A job properties: description: - description: The job short description type: string name: - description: The job name type: string + tags: + items: + type: string + type: array + variants: + items: + properties: + branches: + items: + type: string + type: array + parent: + type: string + type: object + type: array type: object type: array description: Returns the list of jobs - '404': + 404: description: Tenant not found - summary: List available jobs + summary: '' tags: - tenant - /api/tenant/{tenant}/key/{project}.pub: + /api/tenant/{tenant_name}/key/{project_name}.pub: get: - operationId: get-project-secrets-key + operationId: key parameters: - - description: The tenant name - in: path - name: tenant + - in: path + name: tenant_name required: true schema: type: string - - description: The project name - in: path - name: project + - in: path + name: project_name required: true schema: type: string responses: - '200': + 200: content: text/plain: example: '-----BEGIN PUBLIC KEY----- @@ -275,114 +707,354 @@ paths: ' schema: - description: The project secrets public key in PKCS8 format type: string description: Returns the project public key that is used to encrypt secrets - '404': + 404: description: Tenant or Project not found - summary: Get a project public key that is used to encrypt secrets + summary: '' tags: - tenant - /api/tenant/{tenant}/project-ssh-key/{project}.pub: + /api/tenant/{tenant_name}/labels: get: - operationId: get-project-ssh-key + operationId: labels parameters: - - description: The tenant name - in: path - name: tenant - required: true - schema: - type: string - - description: The project name - in: path - name: project + - in: path + name: tenant_name required: true schema: type: string responses: '200': + description: Response not yet documented + summary: '' + tags: + - tenant + /api/tenant/{tenant_name}/nodes: + get: + operationId: nodes + parameters: + - in: path + name: tenant_name + required: true + schema: + type: string + responses: + '200': + description: Response not yet documented + summary: '' + tags: + - tenant + /api/tenant/{tenant_name}/pipeline/{pipeline_name}/project/{project_name}/branch/{branch_name}/freeze-job/{job_name}: + get: + operationId: project_freeze_job + parameters: + - in: path + name: tenant_name + required: true + schema: + type: string + - in: path + name: pipeline_name + required: true + schema: + type: string + - in: path + name: project_name + required: true + schema: + type: string + - in: path + name: branch_name + required: true + schema: + type: string + - in: path + name: job_name + required: true + schema: + type: string + responses: + '200': + description: Response not yet documented + summary: '' + tags: + - tenant + /api/tenant/{tenant_name}/pipeline/{pipeline_name}/project/{project_name}/branch/{branch_name}/freeze-jobs: + get: + operationId: project_freeze_jobs + parameters: + - in: path + name: tenant_name + required: true + schema: + type: string + - in: path + name: pipeline_name + required: true + schema: + type: string + - in: path + name: project_name + required: true + schema: + type: string + - in: path + name: branch_name + required: true + schema: + type: string + responses: + '200': + description: Response not yet documented + summary: '' + tags: + - tenant + /api/tenant/{tenant_name}/pipelines: + get: + operationId: pipelines + parameters: + - in: path + name: tenant_name + required: true + schema: + type: string + responses: + '200': + description: Response not yet documented + summary: '' + tags: + - tenant + /api/tenant/{tenant_name}/project-ssh-key/{project_name}.pub: + get: + operationId: project_ssh_key + parameters: + - in: path + name: tenant_name + required: true + schema: + type: string + - in: path + name: project_name + required: true + schema: + type: string + responses: + 200: content: text/plain: - example: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACA - - ' + example: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACA schema: - description: The project ssh public key in SSH2 format type: string description: Returns the project public key that executor adds to SSH agent - '404': + 404: description: Tenant or Project not found - summary: Get a project public key that is used for SSH in post-merge pipelines + summary: '' tags: - tenant - /api/tenant/{tenant}/semaphores: + /api/tenant/{tenant_name}/project/{project_name}: get: - operationId: list-semaphores + operationId: project parameters: - - description: The tenant name - in: path - name: tenant + - in: path + name: tenant_name + required: true + schema: + type: string + - in: path + name: project_name required: true schema: type: string responses: '200': - content: - application/json: - schema: - description: The list of semaphores - items: - $ref: '#/components/schemas/semaphore' - type: array + description: Response not yet documented + summary: '' + tags: + - tenant + /api/tenant/{tenant_name}/project/{project_name}/autohold: + get: + operationId: autohold_project_get + parameters: + - in: path + name: tenant_name + required: true + schema: + type: string + - in: path + name: project_name + required: true + schema: + type: string + responses: + '200': + description: Response not yet documented + summary: '' + tags: + - tenant + post: + operationId: autohold_project_post + parameters: + - in: path + name: tenant_name + required: true + schema: + type: string + - in: path + name: project_name + required: true + schema: + type: string + responses: + '200': + description: Response not yet documented + summary: '' + tags: + - tenant + /api/tenant/{tenant_name}/project/{project_name}/dequeue: + get: + operationId: dequeue + parameters: + - in: path + name: tenant_name + required: true + schema: + type: string + - in: path + name: project_name + required: true + schema: + type: string + responses: + '200': + description: Response not yet documented + summary: '' + tags: + - tenant + /api/tenant/{tenant_name}/project/{project_name}/enqueue: + get: + operationId: enqueue + parameters: + - in: path + name: tenant_name + required: true + schema: + type: string + - in: path + name: project_name + required: true + schema: + type: string + responses: + '200': + description: Response not yet documented + summary: '' + tags: + - tenant + /api/tenant/{tenant_name}/projects: + get: + operationId: projects + parameters: + - in: path + name: tenant_name + required: true + schema: + type: string + responses: + '200': + description: Response not yet documented + summary: '' + tags: + - tenant + /api/tenant/{tenant_name}/promote: + get: + operationId: promote + parameters: + - in: path + name: tenant_name + required: true + schema: + type: string + responses: + '200': + description: Response not yet documented + summary: '' + tags: + - tenant + /api/tenant/{tenant_name}/semaphores: + get: + operationId: semaphores + parameters: + - in: path + name: tenant_name + required: true + schema: + type: string + responses: + 200: description: Returns the list of semaphores - '404': + 404: description: Tenant not found - summary: List available semaphores + summary: '' tags: - tenant - /api/tenant/{tenant}/status: + /api/tenant/{tenant_name}/status: get: - operationId: get-status + operationId: status parameters: - - description: The tenant name - in: path - name: tenant + - in: path + name: tenant_name required: true schema: type: string responses: '200': - content: - application/json: - schema: - description: The tenant status - properties: - last_reconfigured: - description: The Epoch timestamp of the last reconfiguration - type: integer - pipelines: - description: The list of pipelines - items: - $ref: '#/components/schemas/pipelineStatus' - type: array - trigger_event_queue: - description: The number of running events - type: integer - zuul_version: - description: The Zuul version - type: string - type: object - description: Returns the list of tenants - '404': - description: Tenant not found - summary: Get tenant status + description: Response not yet documented + summary: 'Return the tenant status. Note: the output format is not currently + documented and subject to change without notice.' + tags: + - tenant + /api/tenant/{tenant_name}/status/change/{change}: + get: + operationId: status_change + parameters: + - in: path + name: tenant_name + required: true + schema: + type: string + - in: path + name: change + required: true + schema: + type: string + responses: + '200': + description: Response not yet documented + summary: 'Return the status for a single change. Note: the output format is + not currently documented and subject to change without notice.' + tags: + - tenant + /api/tenant/{tenant_name}/tenant-status: + get: + operationId: tenant_status + parameters: + - in: path + name: tenant_name + required: true + schema: + type: string + responses: + '200': + description: Response not yet documented + summary: '' tags: - tenant /api/tenants: get: - operationId: list-tenants + operationId: tenants + parameters: [] responses: - '200': + 200: content: application/json: schema: @@ -400,289 +1072,10 @@ paths: type: object type: array description: Returns the list of tenants - summary: List the tenants + 404: + description: Tenant not found + summary: '' tags: - tenant -components: - schemas: - build: - description: A build - properties: - branch: - description: The build change branch - type: string - change: - description: The build change number - type: integer - duration: - description: The build duration in second - type: integer - end_time: - description: The build end time ISO 8601 format - type: string - job_name: - description: The build job name - type: string - log_url: - description: The build logs url - type: string - newrev: - description: The build newrev - type: string - patchset: - description: The build patchset number - type: integer - pipeline: - description: The build pipeline - type: string - project: - description: The build project name - type: string - ref: - description: The build change reference - type: string - ref_url: - description: The build change reference url - type: string - result: - description: The build result - type: string - start_time: - description: The build start time ISO 8601 format - type: string - uuid: - description: The build uuid - type: string - voting: - description: The build voting status - type: boolean - type: object - buildset: - description: A buildset - properties: - project: - description: The buildset project name - type: string - event_id: - description: The event id that triggered the buildset - type: string - uuid: - description: The buildset uuid - type: string - branch: - description: The buildset change branch - type: string - ref_url: - description: The buildset change reference url - type: string - newrev: - description: The buildset newrev - type: string - result: - description: The buildset result - type: string - change: - description: The buildset change number - type: integer - patchset: - description: The buildset patchset number - type: integer - ref: - description: The buildset change reference - type: string - pipeline: - description: The buildset pipeline - type: string - message: - description: The message that got reported by the buildset - type: object - changeQueue: - description: A change queue - properties: - heads: - description: The list of queue events - items: - items: - $ref: '#/components/schemas/eventQueue' - type: array - type: array - name: - description: The queue name - type: string - window: - description: The queue window - type: integer - type: object - eventQueue: - description: A queue event - properties: - active: - description: Is the event active - type: boolean - enqueue_time: - description: The epoch creation time - type: integer - failing_reasons: - description: The list of failing reasons - items: - type: string - type: array - id: - description: The event id - type: string - item_ahead: - description: The event ahead - type: string - items_behind: - description: The list of events behind - items: - type: string - type: array - jobs: - description: The event list of jobs - items: - $ref: '#/components/schemas/statusJob' - type: array - live: - description: Is the event live - type: boolean - owner: - description: The event owner - type: string - project: - description: The event project - type: string - remaining_time: - description: The estimated remaining time - type: integer - url: - description: The event url - type: string - zuul_ref: - description: The zuul ref - type: string - type: object - pipelineStatus: - description: A pipeline - properties: - change_queues: - description: The pipeline list of change queues - items: - $ref: '#/components/schemas/changeQueue' - type: array - description: - description: The pipeline description - type: string - name: - description: The pipeline name - type: string - type: object - semaphore: - description: A semaphore - properties: - name: - description: The semaphore name - type: string - global: - description: Whether the semaphore is global - type: boolean - max: - description: The maximum number of holders - type: integer - holders: - $ref: '#/components/schemas/semaphoreHolders' - type: object - semaphoreHolders: - description: Information about the holders of a semaphore - properties: - count: - description: The number of jobs currently holding this semaphore - type: integer - this_tenant: - description: Holders within this tenant - items: - $ref: '#/components/schemas/semaphoreHolder' - type: array - other_tenants: - description: The number of jobs in other tenants currently holding this semaphore - type: integer - type: object - semaphoreHolder: - description: Information about a holder of a semaphore - properties: - buildset_uuid: - description: The UUID of the job's buildset - type: string - job_name: - description: The name of the job - type: string - type: object - statusJob: - description: A job status - properties: - canceled: - description: Is the job canceled - type: boolean - elapsed_time: - description: The elapsed time - type: integer - launch_time: - description: The epoch launch time - type: integer - name: - description: The job name - type: string - number: - description: The job number - type: integer - pipeline: - description: The job pipeline - type: string - remaining_time: - description: The estimated remaining time - type: integer - result: - description: The job result - type: string - retry: - description: The retry count - type: integer - start_time: - description: The epoch start time - type: integer - url: - description: The job stream url - type: string - uuid: - description: The job uuid - type: string - voting: - description: Is the job voting - type: boolean - worker: - $ref: '#/components/schemas/statusWorker' - type: object - statusWorker: - description: A job worker node - properties: - fqdn: - description: The worker fully qualified domain name - type: string - hostname: - description: The worker hostname - type: string - ips: - description: The list of worker ip addresses - items: - type: string - type: array - name: - description: The worker name - type: string - program: - description: The worker program - type: string - version: - description: The worker version - type: string - type: object +tags: +- name: tenant diff --git a/zuul/web/__init__.py b/zuul/web/__init__.py index 01424e5d2d..a29f891f27 100755 --- a/zuul/web/__init__.py +++ b/zuul/web/__init__.py @@ -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 = {