From 998c6007cd4601eecf57dc77c6c11393aecc720b Mon Sep 17 00:00:00 2001 From: Jimmy Zelinskie Date: Thu, 26 Mar 2015 16:20:53 -0400 Subject: [PATCH] trigger: initial custom git trigger --- endpoints/api/trigger.py | 14 +++- endpoints/trigger.py | 105 +++++++++++++++++++++++++- endpoints/webhooks.py | 7 +- static/js/services/trigger-service.js | 25 ++++++ 4 files changed, 143 insertions(+), 8 deletions(-) diff --git a/endpoints/api/trigger.py b/endpoints/api/trigger.py index 1d1209c17..e231c3e7e 100644 --- a/endpoints/api/trigger.py +++ b/endpoints/api/trigger.py @@ -212,15 +212,12 @@ class BuildTriggerActivate(RepositoryParamResource): token = model.create_delegate_token(namespace, repository, token_name, 'write') - # Generate an SSH keypair - new_config_dict['public_key'], trigger.private_key = generate_ssh_keypair() - try: path = url_for('webhooks.build_trigger_webhook', trigger_uuid=trigger.uuid) authed_url = _prepare_webhook_url(app.config['PREFERRED_URL_SCHEME'], '$token', token.code, app.config['SERVER_HOSTNAME'], path) - final_config = handler.activate(trigger.uuid, authed_url, + final_config, trigger.private_key = handler.activate(trigger.uuid, authed_url, trigger.auth_token, new_config_dict) except TriggerActivationException as exc: token.delete_instance() @@ -512,3 +509,12 @@ class BuildTriggerSources(RepositoryParamResource): } else: raise Unauthorized() + +@resource('/v1/repository//trigger/redirect') +@path_param('repository', 'The full path of the repository. e.g. namespace/name') +@internal_only +class CustomBuildTriggerRedirect(RepositoryParamResource): + """ Custom verb to properly handle redirecting users using their own custom git hook. """ + @nickname('customTriggerRedirect') + def get(self, namespace, repository): + pass diff --git a/endpoints/trigger.py b/endpoints/trigger.py index f621cacb4..e12c76639 100644 --- a/endpoints/trigger.py +++ b/endpoints/trigger.py @@ -11,6 +11,7 @@ from tempfile import SpooledTemporaryFile from app import app, userfiles as user_files, github_trigger from util.tarfileappender import TarfileAppender +from util.ssh import generate_ssh_keypair client = app.config['HTTPCLIENT'] @@ -26,6 +27,8 @@ CHUNK_SIZE = 512 * 1024 def should_skip_commit(message): return '[skip build]' in message or '[build skip]' in message +class InvalidPayloadException(Exception): + pass class BuildArchiveException(Exception): pass @@ -185,6 +188,7 @@ class GithubBuildTrigger(BuildTrigger): # Add a deploy key to the GitHub repository. try: + config['public_key'], private_key = generate_ssh_keypair() deploy_key = gh_repo.create_key('Quay.io Builder', config['public_key']) config['deploy_key_id'] = deploy_key.id except GithubException: @@ -206,7 +210,7 @@ class GithubBuildTrigger(BuildTrigger): msg = 'Unable to create webhook on repository: %s' % new_build_source raise TriggerActivationException(msg) - return config + return config, private_key def deactivate(self, auth_token, config): gh_client = self._get_client(auth_token) @@ -541,3 +545,102 @@ class GithubBuildTrigger(BuildTrigger): return branches return None + +class CustomBuildTrigger(BuildTrigger): + @classmethod + def service_name(cls): + return 'custom' + + def is_active(self, config): + return 'public_key' in config + + @staticmethod + def _metadata_from_payload(payload): + try: + metadata = { + 'commit_sha': payload['commit'], + 'ref': payload['ref'], + 'default_branch': payload.get('default_branch', 'master'), + } + except KeyError: + raise InvalidPayloadException() + + commit_info = payload['commit_info'] + if commit_info is not None: + try: + metadata['commit_info'] = { + 'url': commit_info['url'], + 'message': commit_info['message'], + 'date': commit_info['date'], + } + except KeyError: + raise InvalidPayloadException() + + author = commit_info['author'] + if author is not None: + try: + metadata['commit_info']['author'] = { + 'username': author['username'], + 'avatar_url': author['avatar_url'], + 'url': author['url'], + } + except KeyError: + raise InvalidPayloadException() + + committer = commit_info['committer'] + if committer is not None: + try: + metadata['commit_info']['committer'] = { + 'username': committer['username'], + 'avatar_url': committer['avatar_url'], + 'url': committer['url'], + } + except KeyError: + raise InvalidPayloadException() + + return metadata + + def handle_trigger_request(self, request, trigger): + payload = request.get_json() + if not payload: + raise SkipRequestException() + + logger.debug('Payload %s', payload) + metadata = self._metadata_from_payload(payload) + + # The build source is the canonical git URL used to clone. + config = get_trigger_config(trigger) + metadata['git_url'] = config['build_source'] + + branch = metadata['ref'].split('/')[-1] + tags = {branch} + + build_name = metadata['commit_sha'][:6] + dockerfile_id = None + + return dockerfile_id, tags, build_name, trigger.config['subdir'], metadata + + def activate(self, trigger_uuid, standard_webhook_url, auth_token, config): + config['public_key'], private_key = generate_ssh_keypair() + return config, private_key + + def deactivate(self, auth_token, config): + config.pop('public_key', None) + return config + + def manual_start(self, trigger, run_parameters=None): + for parameter in ['branch_name', 'commit_sha',]: + if parameter not in run_parameters: + raise TriggerStartException + + dockerfile_id = None + branch = run_parameters.get('branch_name', None) + tags = {branch} if branch is not None else {} + build_name = 'HEAD' + metadata = { + 'commit_sha': run_parameters['commit_sha'], + 'default_branch': branch, + 'ref': 'refs/heads/%s' % branch, + } + + return dockerfile_id, tags, build_name, trigger.config['subdir'], metadata diff --git a/endpoints/webhooks.py b/endpoints/webhooks.py index 6c40609ed..0ab4f0bc5 100644 --- a/endpoints/webhooks.py +++ b/endpoints/webhooks.py @@ -9,7 +9,7 @@ from auth.permissions import ModifyRepositoryPermission from util.invoice import renderInvoiceToHtml from util.useremails import send_invoice_email, send_subscription_change, send_payment_failed from util.http import abort -from endpoints.trigger import BuildTrigger, ValidationRequestException, SkipRequestException +from endpoints.trigger import BuildTrigger, ValidationRequestException, SkipRequestException, InvalidPayloadException from endpoints.common import start_build @@ -88,14 +88,15 @@ def build_trigger_webhook(trigger_uuid, **kwargs): try: specs = handler.handle_trigger_request(request, trigger) dockerfile_id, tags, name, subdir, metadata = specs - except ValidationRequestException: # This was just a validation request, we don't need to build anything return make_response('Okay') - except SkipRequestException: # The build was requested to be skipped return make_response('Okay') + except InvalidPayloadException: + # The payload was malformed + abort(400) pull_robot_name = model.get_pull_robot_name(trigger) repo = model.get_repository(namespace, repository) diff --git a/static/js/services/trigger-service.js b/static/js/services/trigger-service.js index 169b8766b..f89c4f0e7 100644 --- a/static/js/services/trigger-service.js +++ b/static/js/services/trigger-service.js @@ -49,6 +49,31 @@ angular.module('quay').factory('TriggerService', ['UtilService', '$sanitize', 'K return 'GitHub Repository Push'; } + }, + + 'custom': { + 'description': function(config) { + var source = UtilService.textToSafeHtml(config['build_source']); + var desc = ' Push to Custom Git Repository ' + source; + desc += '
Dockerfile folder: //' + UtilService.textToSafeHtml(config['subdir']); + return desc; + }, + 'run_parameters': [ + { + 'title': 'Branch', + 'type': 'string', + 'name': 'branch_name' + }, + { + 'title': 'Commit SHA1', + 'type': 'string', + 'name': 'commit_sha' + } + ], + 'get_redirect_url': function() {}, + 'is_enabled': function() { return true; }, + 'icon': 'fa-git', + 'title': function() { return 'Custom Git Repository Push'; } } }