diff --git a/endpoints/api/__init__.py b/endpoints/api/__init__.py index e38a6b47e..225ca31ac 100644 --- a/endpoints/api/__init__.py +++ b/endpoints/api/__init__.py @@ -179,3 +179,4 @@ import endpoints.api.user import endpoints.api.search import endpoints.api.build import endpoints.api.webhook +import endpoints.api.trigger diff --git a/endpoints/api/legacy.py b/endpoints/api/legacy.py index c2c75b7f1..73b32c9a1 100644 --- a/endpoints/api/legacy.py +++ b/endpoints/api/legacy.py @@ -1360,6 +1360,7 @@ def delete_webhook(namespace, repository, public_id): abort(403) # Permission denied +# Ported @api_bp.route('/repository//trigger/', methods=['GET']) @api_login_required @@ -1382,6 +1383,7 @@ def _prepare_webhook_url(scheme, username, password, hostname, path): return urlparse.urlunparse((scheme, auth_hostname, path, '', '', '')) +# Ported @api_bp.route('/repository//trigger//subdir', methods=['POST']) @api_login_required @@ -1415,6 +1417,7 @@ def list_build_trigger_subdirs(namespace, repository, trigger_uuid): abort(403) # Permission denied +# Ported @api_bp.route('/repository//trigger//activate', methods=['POST']) @api_login_required @@ -1474,6 +1477,7 @@ def activate_build_trigger(namespace, repository, trigger_uuid): abort(403) # Permission denied +# Ported @api_bp.route('/repository//trigger//start', methods=['POST']) @api_login_required @@ -1512,6 +1516,7 @@ def manually_start_build_trigger(namespace, repository, trigger_uuid): abort(403) # Permission denied +# Ported @api_bp.route('/repository//trigger//builds', methods=['GET']) @api_login_required @@ -1529,6 +1534,7 @@ def list_trigger_recent_builds(namespace, repository, trigger_uuid): abort(403) # Permission denied +# Ported @api_bp.route('/repository//trigger//sources', methods=['GET']) @api_login_required @@ -1553,6 +1559,7 @@ def list_trigger_build_sources(namespace, repository, trigger_uuid): +# Ported @api_bp.route('/repository//trigger/', methods=['GET']) @api_login_required @parse_repository_name @@ -1567,6 +1574,7 @@ def list_build_triggers(namespace, repository): abort(403) # Permission denied +# Ported @api_bp.route('/repository//trigger/', methods=['DELETE']) @api_login_required diff --git a/endpoints/api/trigger.py b/endpoints/api/trigger.py new file mode 100644 index 000000000..640f27e9f --- /dev/null +++ b/endpoints/api/trigger.py @@ -0,0 +1,283 @@ +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('/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('/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) \ No newline at end of file diff --git a/endpoints/api/webhook.py b/endpoints/api/webhook.py index b59919b2e..c8751097d 100644 --- a/endpoints/api/webhook.py +++ b/endpoints/api/webhook.py @@ -2,7 +2,7 @@ from flask import request, url_for from flask.ext.restful import abort from endpoints.api import (RepositoryParamResource, nickname, resource, require_repo_admin, - log_action) + log_action, validate_json_request) from data import model @@ -16,9 +16,18 @@ def webhook_view(webhook): @resource('/v1/repository//webhook/') class WebhookList(RepositoryParamResource): """ Resource for dealing with listing and creating webhooks. """ + schemas = { + 'WebhookCreateRequest': { + 'id': 'WebhookCreateRequest', + 'type': 'object', + 'description': 'Arbitrary json.', + 'required': True, + }, + } @require_repo_admin @nickname('createWebhook') + @validate_json_request('WebhookCreateRequest') def post(self, namespace, repository): """ Create a new webhook for the specified repository. """ repo = model.get_repository(namespace, repository)