From 86e93a2c0f6612ca7a17ac1c435ff91aba95efcd Mon Sep 17 00:00:00 2001 From: jakedt Date: Fri, 21 Feb 2014 17:09:56 -0500 Subject: [PATCH] Write triggers are successfully installing on GitHub, noice! --- config.py | 8 ++++++++ data/database.py | 27 ++++++++++++++------------- data/model.py | 10 ++++++++-- endpoints/api.py | 23 +++++++++++++++++++---- endpoints/trigger.py | 28 +++++++++++++++++++--------- endpoints/webhooks.py | 12 ++++++++---- test/data/test.db | Bin 166912 -> 167936 bytes 7 files changed, 76 insertions(+), 32 deletions(-) diff --git a/config.py b/config.py index ce8d7dc13..22870bbe9 100644 --- a/config.py +++ b/config.py @@ -206,6 +206,8 @@ class TestConfig(FlaskConfig, FakeStorage, EphemeralDB, FakeUserfiles, LOGGING_CONFIG = logs_init_builder(logging.WARN) POPULATE_DB_TEST_DATA = True TESTING = True + URL_SCHEME = 'http' + URL_HOST = 'localhost:5000' class DebugConfig(FlaskConfig, MailConfig, LocalStorage, SQLiteDB, @@ -215,6 +217,8 @@ class DebugConfig(FlaskConfig, MailConfig, LocalStorage, SQLiteDB, LOGGING_CONFIG = logs_init_builder(formatter=logging.Formatter()) SEND_FILE_MAX_AGE_DEFAULT = 0 POPULATE_DB_TEST_DATA = True + URL_SCHEME = 'http' + URL_HOST = 'ci.devtable.com:5000' class LocalHostedConfig(FlaskConfig, MailConfig, S3Storage, RDSMySQL, @@ -224,6 +228,8 @@ class LocalHostedConfig(FlaskConfig, MailConfig, S3Storage, RDSMySQL, UserEventConfig, LargePoolHttpClient): LOGGING_CONFIG = logs_init_builder() SEND_FILE_MAX_AGE_DEFAULT = 0 + URL_SCHEME = 'http' + URL_HOST = 'ci.devtable.com:5000' class ProductionConfig(FlaskProdConfig, MailConfig, S3Storage, RDSMySQL, @@ -234,3 +240,5 @@ class ProductionConfig(FlaskProdConfig, MailConfig, S3Storage, RDSMySQL, LOGGING_CONFIG = logs_init_builder() SEND_FILE_MAX_AGE_DEFAULT = 0 + URL_SCHEME = 'https' + URL_HOST = 'quay.io' diff --git a/data/database.py b/data/database.py index 5316bafba..beaa033dc 100644 --- a/data/database.py +++ b/data/database.py @@ -110,19 +110,6 @@ class Repository(BaseModel): ) -class BuildTriggerService(BaseModel): - name = CharField(index=True) - - -class RepositoryBuildTrigger(BaseModel): - uuid = CharField(default=uuid_generator) - service = ForeignKeyField(BuildTriggerService, index=True) - repository = ForeignKeyField(Repository, index=True) - connected_user = ForeignKeyField(User) - auth_token = CharField() - config = TextField(default='{}') - - class Role(BaseModel): name = CharField(index=True) @@ -176,6 +163,20 @@ class AccessToken(BaseModel): temporary = BooleanField(default=True) +class BuildTriggerService(BaseModel): + name = CharField(index=True) + + +class RepositoryBuildTrigger(BaseModel): + uuid = CharField(default=uuid_generator) + service = ForeignKeyField(BuildTriggerService, index=True) + repository = ForeignKeyField(Repository, index=True) + connected_user = ForeignKeyField(User) + auth_token = CharField() + config = TextField(default='{}') + write_token = ForeignKeyField(AccessToken, null=True) + + class EmailConfirmation(BaseModel): code = CharField(default=random_string_generator(), unique=True, index=True) user = ForeignKeyField(User) diff --git a/data/model.py b/data/model.py index 962ccf5e9..767e7ca78 100644 --- a/data/model.py +++ b/data/model.py @@ -1320,8 +1320,9 @@ def create_access_token(repository, role): return new_token -def create_delegate_token(namespace_name, repository_name, friendly_name): - read_only = Role.get(name='read') +def create_delegate_token(namespace_name, repository_name, friendly_name, + role='read'): + read_only = Role.get(name=role) repo = Repository.get(Repository.name == repository_name, Repository.namespace == namespace_name) new_token = AccessToken.create(repository=repo, role=read_only, @@ -1509,6 +1510,11 @@ def list_build_triggers(namespace_name, repository_name): def delete_build_trigger(namespace_name, repository_name, trigger_uuid): trigger = get_build_trigger(namespace_name, repository_name, trigger_uuid) + + # Delete the access token created for this trigger, and the trigger itself + if trigger.write_token and trigger.write_token.code: + trigger.write_token.delete_instance() + trigger.delete_instance() diff --git a/endpoints/api.py b/endpoints/api.py index ca7e09c14..877a14384 100644 --- a/endpoints/api.py +++ b/endpoints/api.py @@ -9,6 +9,7 @@ from flask.ext.login import current_user, logout_user from flask.ext.principal import identity_changed, AnonymousIdentity from functools import wraps from collections import defaultdict +from urllib import quote from data import model from data.queue import dockerfile_build_queue @@ -1363,6 +1364,11 @@ def get_build_trigger(namespace, repository, trigger_uuid): abort(403) # Permission denied +def _prepare_webhook_url(scheme, username, password, hostname, path): + auth_hostname = '%s:%s@%s' % (quote(username), quote(password), hostname) + return urlparse.urlunparse((scheme, auth_hostname, path, '', '', '')) + + @api.route('/repository//trigger//activate', methods=['POST']) @api_login_required @@ -1386,20 +1392,29 @@ def activate_build_trigger(namespace, repository, trigger_uuid): 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 = '%s/%s' % (trigger.repository.namespace, trigger.repository.name) - webhook_url = url_for('webhooks.build_trigger_webhook', - repository=repository, trigger_uuid=trigger.uuid, - _external=True) - handler.activate(trigger.uuid, webhook_url, trigger.auth_token, + path = url_for('webhooks.build_trigger_webhook', repository=repository, + trigger_uuid=trigger.uuid) + authed_url = _prepare_webhook_url(app.config['URL_SCHEME'], '$token', + token.code, app.config['URL_HOST'], + path) + + handler.activate(trigger.uuid, authed_url, trigger.auth_token, new_config_dict) except TriggerActivationException as e: + token.delete_instance() abort(400, message = e.msg) return # Save the updated config. trigger.config = json.dumps(new_config_dict) + trigger.write_token = token trigger.save() return jsonify(trigger_view(trigger)) diff --git a/endpoints/trigger.py b/endpoints/trigger.py index 0b6c4d07e..cd9066766 100644 --- a/endpoints/trigger.py +++ b/endpoints/trigger.py @@ -1,7 +1,7 @@ import logging import io -from github import Github +from github import Github, UnknownObjectException, GithubException from tempfile import SpooledTemporaryFile from app import app @@ -27,6 +27,9 @@ class InvalidServiceException(Exception): class TriggerActivationException(Exception): pass +class ValidationRequestException(Exception): + pass + class BuildTrigger(object): def __init__(self): @@ -97,17 +100,20 @@ class GithubBuildTrigger(BuildTrigger): try: to_add_webhook = gh_client.get_repo(new_build_source) + except UnknownObjectException: + msg = 'Unable to find GitHub repository for source: %s' + raise TriggerActivationException(msg % new_build_source) - webhook_config = { - 'url': standard_webhook_url, - 'content_type': 'json', - } + webhook_config = { + 'url': standard_webhook_url, + 'content_type': 'json', + } + try: to_add_webhook.create_hook('web', webhook_config) - - - except Exception: - pass + except GithubException: + msg = 'Unable to create webhook on repository: %s' + raise TriggerActivationException(msg % new_build_source) def list_build_sources(self, auth_token): gh_client = self._get_client(auth_token) @@ -142,6 +148,10 @@ class GithubBuildTrigger(BuildTrigger): def handle_trigger_request(self, request, auth_token, config): payload = request.get_json() + + if 'zen' in payload: + raise ValidationRequestException() + logger.debug('Payload %s', payload) ref = payload['ref'] commit_id = payload['head_commit']['id'][0:7] diff --git a/endpoints/webhooks.py b/endpoints/webhooks.py index b7d0fc212..0e424b052 100644 --- a/endpoints/webhooks.py +++ b/endpoints/webhooks.py @@ -11,7 +11,7 @@ from util.invoice import renderInvoiceToHtml from util.email import send_invoice_email from util.names import parse_repository_name from util.http import abort -from endpoints.trigger import BuildTrigger +from endpoints.trigger import BuildTrigger, ValidationRequestException logger = logging.getLogger(__name__) @@ -61,9 +61,13 @@ def build_trigger_webhook(namespace, repository, trigger_uuid): handler = BuildTrigger.get_trigger_for_service(trigger.service.name) logger.debug('Passing webhook request to handler %s', handler) - df_id, tag, name = handler.handle_trigger_request(request, - trigger.auth_token, - trigger.config) + try: + df_id, tag, name = handler.handle_trigger_request(request, + trigger.auth_token, + trigger.config) + except ValidationRequestException: + # This was just a validation request, don't need to build anything + return make_response('Okay') host = urlparse.urlparse(request.url).netloc full_tag = '%s/%s/%s:%s' % (host, trigger.repository.namespace, diff --git a/test/data/test.db b/test/data/test.db index b0f27568c5a5aefbb3385f1c90350d3a1441d79b..e733ca294d1d5f8f2ae18cac7e3b72521cc6085f 100644 GIT binary patch delta 5477 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 f+i0_?hBuC2#~rj;<9)LKnA*