import logging import json from flask import request, redirect from app import app, userfiles as user_files, build_logs, log_archive from endpoints.api import (RepositoryParamResource, parse_args, query_param, nickname, resource, require_repo_read, require_repo_write, validate_json_request, ApiResource, internal_only, format_date, api, Unauthorized, NotFound) from endpoints.common import start_build from endpoints.trigger import BuildTrigger from data import model from auth.auth_context import get_authenticated_user from auth.permissions import ModifyRepositoryPermission, AdministerOrganizationPermission from data.buildlogs import BuildStatusRetrievalError from util.names import parse_robot_username logger = logging.getLogger(__name__) def get_trigger_config(trigger): try: return json.loads(trigger.config) except: return {} def get_job_config(build_obj): try: return json.loads(build_obj.job_config) except: return None def user_view(user): return { 'name': user.username, 'kind': 'user', 'is_robot': user.robot, } def trigger_view(trigger): if trigger and trigger.uuid: config_dict = get_trigger_config(trigger) build_trigger = BuildTrigger.get_trigger_for_service(trigger.service.name) return { 'service': trigger.service.name, 'config': config_dict, 'id': trigger.uuid, 'connected_user': trigger.connected_user.username, 'is_active': build_trigger.is_active(config_dict), 'pull_robot': user_view(trigger.pull_robot) if trigger.pull_robot else None } return None def build_status_view(build_obj, can_write=False): phase = build_obj.phase try: status = build_logs.get_status(build_obj.uuid) except BuildStatusRetrievalError: status = {} phase = 'cannot_load' logger.debug('Can write: %s job_config: %s', can_write, build_obj.job_config) resp = { 'id': build_obj.uuid, 'phase': phase, 'started': format_date(build_obj.started), 'display_name': build_obj.display_name, 'status': status or {}, 'job_config': get_job_config(build_obj) if can_write else None, 'is_writer': can_write, 'trigger': trigger_view(build_obj.trigger), 'resource_key': build_obj.resource_key, 'pull_robot': user_view(build_obj.pull_robot) if build_obj.pull_robot else None, } if can_write: resp['archive_url'] = user_files.get_file_url(build_obj.resource_key, requires_cors=True) return resp @resource('/v1/repository//build/') class RepositoryBuildList(RepositoryParamResource): """ Resource related to creating and listing repository builds. """ schemas = { 'RepositoryBuildRequest': { 'id': 'RepositoryBuildRequest', 'type': 'object', 'description': 'Description of a new repository build.', 'required': [ 'file_id', ], 'properties': { 'file_id': { 'type': 'string', 'description': 'The file id that was generated when the build spec was uploaded', }, 'subdirectory': { 'type': 'string', 'description': 'Subdirectory in which the Dockerfile can be found', }, 'pull_robot': { 'type': 'string', 'description': 'Username of a Quay robot account to use as pull credentials', }, 'docker_tags': { 'type': 'array', 'description': 'The tags to which the built images will be pushed', 'items': { 'type': 'string' }, 'minItems': 1, 'uniqueItems': True } }, }, } @require_repo_read @parse_args @query_param('limit', 'The maximum number of builds to return', type=int, default=5) @nickname('getRepoBuilds') def get(self, args, namespace, repository): """ Get the list of repository builds. """ limit = args['limit'] builds = list(model.list_repository_builds(namespace, repository, limit)) can_write = ModifyRepositoryPermission(namespace, repository).can() return { 'builds': [build_status_view(build, can_write) for build in builds] } @require_repo_write @nickname('requestRepoBuild') @validate_json_request('RepositoryBuildRequest') def post(self, namespace, repository): """ Request that a repository be built and pushed from the specified input. """ logger.debug('User requested repository initialization.') request_json = request.get_json() dockerfile_id = request_json['file_id'] subdir = request_json['subdirectory'] if 'subdirectory' in request_json else '' tags = request_json.get('docker_tags', ['latest']) pull_robot_name = request_json.get('pull_robot', None) # Verify the security behind the pull robot. if pull_robot_name: result = parse_robot_username(pull_robot_name) if result: pull_robot = model.lookup_robot(pull_robot_name) if not pull_robot: raise NotFound() # Make sure the user has administer permissions for the robot's namespace. (robot_namespace, shortname) = result if not AdministerOrganizationPermission(robot_namespace).can(): raise Unauthorized() else: raise Unauthorized() # Check if the dockerfile resource has already been used. If so, then it # can only be reused if the user has access to the repository for which it # was used. associated_repository = model.get_repository_for_resource(dockerfile_id) if associated_repository: if not ModifyRepositoryPermission(associated_repository.namespace_user.username, associated_repository.name): raise Unauthorized() # Start the build. repo = model.get_repository(namespace, repository) display_name = user_files.get_file_checksum(dockerfile_id) build_request = start_build(repo, dockerfile_id, tags, display_name, subdir, True, pull_robot_name=pull_robot_name) resp = build_status_view(build_request, True) repo_string = '%s/%s' % (namespace, repository) headers = { 'Location': api.url_for(RepositoryBuildStatus, repository=repo_string, build_uuid=build_request.uuid), } return resp, 201, headers @resource('/v1/repository//build//status') class RepositoryBuildStatus(RepositoryParamResource): """ Resource for dealing with repository build status. """ @require_repo_read @nickname('getRepoBuildStatus') def get(self, namespace, repository, build_uuid): """ Return the status for the builds specified by the build uuids. """ build = model.get_repository_build(build_uuid) if (not build or build.repository.name != repository or build.repository.namespace_user.username != namespace): raise NotFound() can_write = ModifyRepositoryPermission(namespace, repository).can() return build_status_view(build, can_write) @resource('/v1/repository//build//logs') class RepositoryBuildLogs(RepositoryParamResource): """ Resource for loading repository build logs. """ @require_repo_write @nickname('getRepoBuildLogs') def get(self, namespace, repository, build_uuid): """ Return the build logs for the build specified by the build uuid. """ response_obj = {} build = model.get_repository_build(build_uuid) if (not build or build.repository.name != repository or build.repository.namespace_user.username != namespace): raise NotFound() # If the logs have been archived, just redirect to the completed archive if build.logs_archived: return redirect(log_archive.get_file_url(build.uuid)) start = int(request.args.get('start', 0)) try: count, logs = build_logs.get_log_entries(build.uuid, start) except BuildStatusRetrievalError: count, logs = (0, []) response_obj.update({ 'start': start, 'total': count, 'logs': [log for log in logs], }) return response_obj @resource('/v1/filedrop/') @internal_only class FileDropResource(ApiResource): """ Custom verb for setting up a client side file transfer. """ schemas = { 'FileDropRequest': { 'id': 'FileDropRequest', 'type': 'object', 'description': 'Description of the file that the user wishes to upload.', 'required': [ 'mimeType', ], 'properties': { 'mimeType': { 'type': 'string', 'description': 'Type of the file which is about to be uploaded', }, }, }, } @nickname('getFiledropUrl') @validate_json_request('FileDropRequest') def post(self): """ Request a URL to which a file may be uploaded. """ mime_type = request.get_json()['mimeType'] (url, file_id) = user_files.prepare_for_drop(mime_type, requires_cors=True) return { 'url': url, 'file_id': str(file_id), }