import json import logging from flask import request, url_for from flask.ext.restful import abort from urllib import quote from urlparse import urlunparse from app import app from endpoints.api import (RepositoryParamResource, nickname, resource, require_repo_admin, log_action, request_error, query_param, parse_args, validate_json_request) from endpoints.api.build import build_status_view from endpoints.common import start_build from endpoints.trigger import (BuildTrigger, TriggerDeactivationException, TriggerActivationException, EmptyRepositoryException) from data import model from auth.permissions import UserPermission logger = logging.getLogger(__name__) def trigger_view(trigger): if trigger and trigger.uuid: config_dict = json.loads(trigger.config) 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) } return None def _prepare_webhook_url(scheme, username, password, hostname, path): auth_hostname = '%s:%s@%s' % (quote(username), quote(password), hostname) return urlunparse((scheme, auth_hostname, path, '', '', '')) @resource('/v1/repository//trigger/') class BuildTriggerList(RepositoryParamResource): """ Resource for listing repository build triggers. """ @require_repo_admin @nickname('listBuildTriggers') def get(self, namespace, repository): """ List the triggers for the specified repository. """ triggers = model.list_build_triggers(namespace, repository) return { 'triggers': [trigger_view(trigger) for trigger in triggers] } @resource('/v1/repository//trigger/') class BuildTrigger(RepositoryParamResource): """ Resource for managing specific build triggers. """ @require_repo_admin @nickname('getBuildTrigger') def get(self, namespace, repository, trigger_uuid): """ Get information for the specified build trigger. """ try: trigger = model.get_build_trigger(namespace, repository, trigger_uuid) except model.InvalidBuildTriggerException: abort(404) return trigger_view(trigger) @require_repo_admin @nickname('deleteBuildTrigger') def delete(self, namespace, repository, trigger_uuid): """ Delete the specified build trigger. """ try: trigger = model.get_build_trigger(namespace, repository, trigger_uuid) except model.InvalidBuildTriggerException: abort(404) return handler = BuildTrigger.get_trigger_for_service(trigger.service.name) config_dict = json.loads(trigger.config) if handler.is_active(config_dict): try: handler.deactivate(trigger.auth_token, config_dict) except TriggerDeactivationException as ex: # We are just going to eat this error logger.warning('Trigger deactivation problem: %s', ex) log_action('delete_repo_trigger', namespace, {'repo': repository, 'trigger_id': trigger_uuid, 'service': trigger.service.name, 'config': config_dict}, repo=model.get_repository(namespace, repository)) trigger.delete_instance(recursive=True) return 'No Content', 204 @resource('/v1/repository//trigger//subdir') class BuildTriggerSubdirs(RepositoryParamResource): """ Custom verb for fetching the subdirs which are buildable for a trigger. """ schemas = { 'BuildTriggerSubdirRequest': { 'id': 'BuildTriggerSubdirRequest', 'type': 'object', 'description': 'Arbitrary json.', 'required': True, }, } @require_repo_admin @nickname('listBuildTriggerSubdirs') @validate_json_request('BuildTriggerSubdirRequest') def post(self, namespace, repository, trigger_uuid): """ List the subdirectories available for the specified build trigger and source. """ try: trigger = model.get_build_trigger(namespace, repository, trigger_uuid) except model.InvalidBuildTriggerException: abort(404) return handler = BuildTrigger.get_trigger_for_service(trigger.service.name) user_permission = UserPermission(trigger.connected_user.username) if user_permission.can(): new_config_dict = request.get_json() try: subdirs = handler.list_build_subdirs(trigger.auth_token, new_config_dict) return { 'subdir': subdirs, 'status': 'success' } except EmptyRepositoryException as exc: return { 'status': 'error', 'message': exc.msg } else: abort(403) @resource('/v1/repository//trigger//activate') class BuildTriggerActivate(RepositoryParamResource): """ Custom verb for activating a build trigger once all required information has been collected. """ schemas = { 'BuildTriggerActivateRequest': { 'id': 'BuildTriggerActivateRequest', 'type': 'object', 'description': 'Arbitrary json.', 'required': True, }, } @require_repo_admin @nickname('activateBuildTrigger') @validate_json_request('BuildTriggerActivateRequest') def post(self, namespace, repository, trigger_uuid): """ Activate the specified build trigger. """ try: trigger = model.get_build_trigger(namespace, repository, trigger_uuid) except model.InvalidBuildTriggerException: abort(404) handler = BuildTrigger.get_trigger_for_service(trigger.service.name) existing_config_dict = json.loads(trigger.config) if handler.is_active(existing_config_dict): abort(400) user_permission = UserPermission(trigger.connected_user.username) if user_permission.can(): new_config_dict = request.get_json() token_name = 'Build Trigger: %s' % trigger.service.name token = model.create_delegate_token(namespace, repository, token_name, 'write') try: repository_path = '%s/%s' % (trigger.repository.namespace, trigger.repository.name) path = url_for('webhooks.build_trigger_webhook', repository=repository_path, trigger_uuid=trigger.uuid) authed_url = _prepare_webhook_url(app.config['URL_SCHEME'], '$token', token.code, app.config['URL_HOST'], path) final_config = handler.activate(trigger.uuid, authed_url, trigger.auth_token, new_config_dict) except TriggerActivationException as exc: token.delete_instance() return request_error(message=exc.message) # Save the updated config. trigger.config = json.dumps(final_config) trigger.write_token = token trigger.save() # Log the trigger setup. repo = model.get_repository(namespace, repository) log_action('setup_repo_trigger', namespace, {'repo': repository, 'namespace': namespace, 'trigger_id': trigger.uuid, 'service': trigger.service.name, 'config': final_config}, repo=repo) return trigger_view(trigger) else: abort(403) @resource('/v1/repository//trigger//start') class ActivateBuildTrigger(RepositoryParamResource): """ Custom verb to manually activate a build trigger. """ @require_repo_admin @nickname('manuallyStartBuildTrigger') def post(self, namespace, repository, trigger_uuid): """ Manually start a build from the specified trigger. """ try: trigger = model.get_build_trigger(namespace, repository, trigger_uuid) except model.InvalidBuildTriggerException: abort(404) handler = BuildTrigger.get_trigger_for_service(trigger.service.name) existing_config_dict = json.loads(trigger.config) if not handler.is_active(existing_config_dict): abort(400) specs = handler.manual_start(trigger.auth_token, json.loads(trigger.config)) dockerfile_id, tags, name, subdir = specs repo = model.get_repository(namespace, repository) build_request = start_build(repo, dockerfile_id, tags, name, subdir, True) resp = build_status_view(build_request, True) repo_string = '%s/%s' % (namespace, repository) headers = { 'Location': url_for('api_bp.get_repo_build_status', repository=repo_string, build_uuid=build_request.uuid), } return resp, 201, headers @resource('/v1/repository//trigger//builds') class TriggerBuildList(RepositoryParamResource): """ Resource to represent builds that were activated from the specified trigger. """ @parse_args @query_param('limit', 'The maximum number of builds to return', type=int, default=5) @require_repo_admin @nickname('listTriggerRecentBuilds') def get(self, args, namespace, repository, trigger_uuid): """ List the builds started by the specified trigger. """ limit = args['limit'] builds = list(model.list_trigger_builds(namespace, repository, trigger_uuid, limit)) return { 'builds': [build_status_view(build, True) for build in builds] } @resource('/v1/repository//trigger//sources') class BuildTriggerSources(RepositoryParamResource): """ Custom verb to fetch the list of build sources for the trigger config. """ @require_repo_admin @nickname('listTriggerBuildSources') def get(self, namespace, repository, trigger_uuid): """ List the build sources for the trigger configuration thus far. """ try: trigger = model.get_build_trigger(namespace, repository, trigger_uuid) except model.InvalidBuildTriggerException: abort(404) user_permission = UserPermission(trigger.connected_user.username) if user_permission.can(): trigger_handler = BuildTrigger.get_trigger_for_service(trigger.service.name) return { 'sources': trigger_handler.list_build_sources(trigger.auth_token) } else: abort(403)