From 6b1fcefc26378f1a0b074d24099f92d8677e1ae7 Mon Sep 17 00:00:00 2001 From: yackob03 Date: Tue, 11 Feb 2014 13:53:44 -0500 Subject: [PATCH 001/176] Check in progress on github connection, this will not work. --- endpoints/webhooks.py | 28 ++++++++++++++++++++++++++-- requirements-nover.txt | 3 ++- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/endpoints/webhooks.py b/endpoints/webhooks.py index 5a6c0ad3d..9d6b9f319 100644 --- a/endpoints/webhooks.py +++ b/endpoints/webhooks.py @@ -1,13 +1,17 @@ import logging import stripe +import urlparse +import json from flask import request, make_response, Blueprint from data import model -from app import app +from auth.auth import process_auth +from auth.permissions import ModifyRepositoryPermission from util.invoice import renderInvoiceToHtml from util.email import send_invoice_email - +from util.names import parse_repository_name +from util.http import abort logger = logging.getLogger(__name__) @@ -36,3 +40,23 @@ def stripe_webhook(): send_invoice_email(user.email, invoice_html) return make_response('Okay') + +@webhooks.route('/github/push/repository/', methods=['POST']) +@process_auth +@parse_repository_name +def github_push_webhook(namespace, repository): + # data = request.get_json() + permission = ModifyRepositoryPermission(namespace, repository) + if permission.can(): + payload = json.loads(request.form['payload']) + ref = payload['ref'] + + repo = model.get_repository(namespace, repository) + token = model.create_access_token(repo, 'write') + + host = urlparse.urlparse(request.url).netloc + tag = '%s/%s/%s:latest' % (host, repo.namespace, repo.name) + + model.create_repository_build(repo, token, build_spec, tag) + + abort(403) \ No newline at end of file diff --git a/requirements-nover.txt b/requirements-nover.txt index 5b1ef8841..969d1e563 100644 --- a/requirements-nover.txt +++ b/requirements-nover.txt @@ -22,4 +22,5 @@ logstash_formatter redis hiredis git+https://github.com/dotcloud/docker-py.git -loremipsum \ No newline at end of file +loremipsum +pygithub \ No newline at end of file From b5d49193647477276d582642e3cab4c085ef2474 Mon Sep 17 00:00:00 2001 From: jakedt Date: Tue, 18 Feb 2014 15:50:15 -0500 Subject: [PATCH 002/176] Split out callbacks into their own blueprint. Add build trigger DB information and connect it with some APIs. Stub out the UI to allow for generation of triggers. Split out the triggers into a plugin-ish architecture for easily adding new triggers. --- application.py | 2 + data/database.py | 16 ++++- data/model.py | 84 +++++++++++++++++----- endpoints/api.py | 90 +++++++++++++++--------- endpoints/callbacks.py | 119 ++++++++++++++++++++++++++++++++ endpoints/common.py | 43 +++++++++++- endpoints/trigger.py | 111 +++++++++++++++++++++++++++++ endpoints/web.py | 116 +++---------------------------- endpoints/webhooks.py | 37 +++++++--- initdb.py | 4 ++ static/js/controllers.js | 28 +++++++- static/partials/repo-admin.html | 20 ++++++ test/data/test.db | Bin 142336 -> 151552 bytes 13 files changed, 500 insertions(+), 170 deletions(-) create mode 100644 endpoints/callbacks.py create mode 100644 endpoints/trigger.py diff --git a/application.py b/application.py index 2d6660866..797dc848b 100644 --- a/application.py +++ b/application.py @@ -16,11 +16,13 @@ from endpoints.tags import tags from endpoints.registry import registry from endpoints.webhooks import webhooks from endpoints.realtime import realtime +from endpoints.callbacks import callback logger = logging.getLogger(__name__) application.register_blueprint(web) +application.register_blueprint(callback, url_prefix='/oauth2') application.register_blueprint(index, url_prefix='/v1') application.register_blueprint(tags, url_prefix='/v1') application.register_blueprint(registry, url_prefix='/v1') diff --git a/data/database.py b/data/database.py index 86a797bed..c72fa86a5 100644 --- a/data/database.py +++ b/data/database.py @@ -110,6 +110,19 @@ 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) @@ -248,4 +261,5 @@ all_models = [User, Repository, Image, AccessToken, Role, RepositoryPermission, Visibility, RepositoryTag, EmailConfirmation, FederatedLogin, LoginService, QueueItem, RepositoryBuild, Team, TeamMember, TeamRole, Webhook, - LogEntryKind, LogEntry, PermissionPrototype] + LogEntryKind, LogEntry, PermissionPrototype, BuildTriggerService, + RepositoryBuildTrigger] diff --git a/data/model.py b/data/model.py index d137ab2e5..98d1e28b2 100644 --- a/data/model.py +++ b/data/model.py @@ -55,6 +55,10 @@ class InvalidWebhookException(DataModelException): pass +class InvalidBuildTriggerException(DataModelException): + pass + + def create_user(username, password, email): if not validate_email(email): raise InvalidEmailAddressException('Invalid email address: %s' % email) @@ -1352,27 +1356,75 @@ def delete_webhook(namespace_name, repository_name, public_id): webhook = get_webhook(namespace_name, repository_name, public_id) webhook.delete_instance() return webhook - -def list_logs(user_or_organization_name, start_time, end_time, performer = None, repository = None): - joined = LogEntry.select().join(User) - if repository: - joined = joined.where(LogEntry.repository == repository) - if performer: - joined = joined.where(LogEntry.performer == performer) - return joined.where( - User.username == user_or_organization_name, - LogEntry.datetime >= start_time, - LogEntry.datetime < end_time).order_by(LogEntry.datetime.desc()) +def list_logs(user_or_organization_name, start_time, end_time, performer=None, + repository=None): + joined = LogEntry.select().join(User) + if repository: + joined = joined.where(LogEntry.repository == repository) -def log_action(kind_name, user_or_organization_name, performer=None, repository=None, - access_token=None, ip=None, metadata={}, timestamp=None): + if performer: + joined = joined.where(LogEntry.performer == performer) + + return joined.where( + User.username == user_or_organization_name, + LogEntry.datetime >= start_time, + LogEntry.datetime < end_time).order_by(LogEntry.datetime.desc()) + + +def log_action(kind_name, user_or_organization_name, performer=None, + repository=None, access_token=None, ip=None, metadata={}, + timestamp=None): if not timestamp: timestamp = datetime.today() kind = LogEntryKind.get(LogEntryKind.name == kind_name) account = User.get(User.username == user_or_organization_name) - entry = LogEntry.create(kind=kind, account=account, performer=performer, - repository=repository, access_token=access_token, ip=ip, - metadata_json=json.dumps(metadata), datetime=timestamp) + LogEntry.create(kind=kind, account=account, performer=performer, + repository=repository, access_token=access_token, ip=ip, + metadata_json=json.dumps(metadata), datetime=timestamp) + + +def create_build_trigger(namespace_name, repository_name, service_name, + auth_token, user): + service = BuildTriggerService.get(name=service_name) + repo = Repository.get(namespace=namespace_name, name=repository_name) + + trigger = RepositoryBuildTrigger.create(repository=repo, service=service, + auth_token=auth_token, + connected_user=user) + return trigger + + +def get_build_trigger(namespace_name, repository_name, trigger_uuid): + try: + return (RepositoryBuildTrigger + .select(RepositoryBuildTrigger, BuildTriggerService, Repository) + .join(BuildTriggerService) + .switch(RepositoryBuildTrigger) + .join(Repository) + .switch(RepositoryBuildTrigger) + .join(User) + .where(RepositoryBuildTrigger.uuid == trigger_uuid, + Repository.namespace == namespace_name, + Repository.name == repository_name) + .get()) + except RepositoryBuildTrigger.DoesNotExist: + msg = 'No build trigger with uuid: %s' % trigger_uuid + raise InvalidBuildTriggerException(msg) + + +def list_build_triggers(namespace_name, repository_name): + return (RepositoryBuildTrigger + .select(RepositoryBuildTrigger, BuildTriggerService, Repository) + .join(BuildTriggerService) + .switch(RepositoryBuildTrigger) + .join(Repository) + .where(Repository.namespace == namespace_name, + Repository.name == repository_name)) + + +def delete_build_trigger(namespace_name, repository_name, trigger_uuid): + trigger = get_build_trigger(namespace_name, repository_name, trigger_uuid) + trigger.delete_instance() diff --git a/endpoints/api.py b/endpoints/api.py index 472064dc2..59e2cc76b 100644 --- a/endpoints/api.py +++ b/endpoints/api.py @@ -25,7 +25,7 @@ from auth.permissions import (ReadRepositoryPermission, AdministerOrganizationPermission, OrganizationMemberPermission, ViewTeamPermission) -from endpoints.common import common_login +from endpoints.common import common_login, get_route_data from util.cache import cache_control from datetime import datetime, timedelta @@ -34,7 +34,6 @@ user_files = app.config['USERFILES'] build_logs = app.config['BUILDLOGS'] logger = logging.getLogger(__name__) -route_data = None api = Blueprint('api', __name__) @@ -62,37 +61,6 @@ def request_error(exception=None, **kwargs): return make_response(jsonify(data), 400) -def get_route_data(): - global route_data - if route_data: - return route_data - - routes = [] - for rule in app.url_map.iter_rules(): - if rule.endpoint.startswith('api.'): - endpoint_method = app.view_functions[rule.endpoint] - is_internal = '__internal_call' in dir(endpoint_method) - is_org_api = '__user_call' in dir(endpoint_method) - methods = list(rule.methods.difference(['HEAD', 'OPTIONS'])) - - route = { - 'name': rule.endpoint[4:], - 'methods': methods, - 'path': rule.rule, - 'parameters': list(rule.arguments) - } - - if is_org_api: - route['user_method'] = endpoint_method.__user_call - - routes.append(route) - - route_data = { - 'endpoints': routes - } - return route_data - - def log_action(kind, user_or_orgname, metadata={}, repo=None): performer = current_user.db_user() model.log_action(kind, user_or_orgname, performer=performer, @@ -1335,6 +1303,62 @@ def delete_webhook(namespace, repository, public_id): abort(403) # Permission denied +def trigger_view(trigger): + return { + 'service': trigger.service.name, + 'config': json.loads(trigger.config), + 'id': trigger.uuid, + 'connected_user': trigger.connected_user.username, + } + + +@api.route('/repository//trigger/', + methods=['GET']) +@api_login_required +@parse_repository_name +def get_build_trigger(namespace, repository, trigger_uuid): + permission = AdministerRepositoryPermission(namespace, repository) + if permission.can(): + try: + trigger = model.get_build_trigger(namespace, repository, trigger_uuid) + except model.InvalidBuildTriggerException: + abort(404) + + return jsonify(trigger_view(trigger)) + + abort(403) # Permission denied + + +@api.route('/repository//trigger/', methods=['GET']) +@api_login_required +@parse_repository_name +def list_build_triggers(namespace, repository): + permission = AdministerRepositoryPermission(namespace, repository) + if permission.can(): + triggers = model.list_build_triggers(namespace, repository) + return jsonify({ + 'triggers': [trigger_view(trigger) for trigger in triggers] + }) + + abort(403) # Permission denied + + +@api.route('/repository//trigger/', + methods=['DELETE']) +@api_login_required +@parse_repository_name +def delete_build_trigger(namespace, repository, trigger_uuid): + permission = AdministerRepositoryPermission(namespace, repository) + if permission.can(): + model.delete_build_trigger(namespace, repository, trigger_uuid) + log_action('delete_repo_trigger', namespace, + {'repo': repository, 'trigger_id': trigger_uuid}, + repo=model.get_repository(namespace, repository)) + return make_response('No Content', 204) + + abort(403) # Permission denied + + @api.route('/filedrop/', methods=['POST']) @api_login_required @internal_api_call diff --git a/endpoints/callbacks.py b/endpoints/callbacks.py new file mode 100644 index 000000000..26b35f456 --- /dev/null +++ b/endpoints/callbacks.py @@ -0,0 +1,119 @@ +import requests +import logging + +from flask import request, redirect, url_for, Blueprint +from flask.ext.login import login_required, current_user + +from endpoints.common import render_page_template, common_login +from app import app, mixpanel +from data import model +from util.names import parse_repository_name + + +logger = logging.getLogger(__name__) + +callback = Blueprint('callback', __name__) + + +def exchange_github_code_for_token(code): + code = request.args.get('code') + payload = { + 'client_id': app.config['GITHUB_CLIENT_ID'], + 'client_secret': app.config['GITHUB_CLIENT_SECRET'], + 'code': code, + } + headers = { + 'Accept': 'application/json' + } + + get_access_token = requests.post(app.config['GITHUB_TOKEN_URL'], + params=payload, headers=headers) + + token = get_access_token.json()['access_token'] + return token + + +def get_github_user(token): + token_param = { + 'access_token': token, + } + get_user = requests.get(app.config['GITHUB_USER_URL'], params=token_param) + + return get_user.json() + + +@callback.route('/github/callback', methods=['GET']) +def github_oauth_callback(): + error = request.args.get('error', None) + if error: + return render_page_template('githuberror.html', error_message=error) + + token = exchange_github_code_for_token(request.args.get('code')) + user_data = get_github_user(token) + + username = user_data['login'] + github_id = user_data['id'] + + v3_media_type = { + 'Accept': 'application/vnd.github.v3' + } + + token_param = { + 'access_token': token, + } + get_email = requests.get(app.config['GITHUB_USER_EMAILS'], + params=token_param, headers=v3_media_type) + + # We will accept any email, but we prefer the primary + found_email = None + for user_email in get_email.json(): + found_email = user_email['email'] + if user_email['primary']: + break + + to_login = model.verify_federated_login('github', github_id) + if not to_login: + # try to create the user + try: + to_login = model.create_federated_user(username, found_email, 'github', + github_id) + + # Success, tell mixpanel + mixpanel.track(to_login.username, 'register', {'service': 'github'}) + + state = request.args.get('state', None) + if state: + logger.debug('Aliasing with state: %s' % state) + mixpanel.alias(to_login.username, state) + + except model.DataModelException, ex: + return render_page_template('githuberror.html', error_message=ex.message) + + if common_login(to_login): + return redirect(url_for('web.index')) + + return render_page_template('githuberror.html') + + +@callback.route('/github/callback/attach', methods=['GET']) +@login_required +def github_oauth_attach(): + token = exchange_github_code_for_token(request.args.get('code')) + user_data = get_github_user(token) + github_id = user_data['id'] + user_obj = current_user.db_user() + model.attach_federated_login(user_obj, 'github', github_id) + return redirect(url_for('web.user')) + + +@callback.route('/github/callback/trigger/', methods=['GET']) +@login_required +@parse_repository_name +def attach_github_build_trigger(namespace, repository): + token = exchange_github_code_for_token(request.args.get('code')) + model.create_build_trigger(namespace, repository, 'github', token, + current_user.db_user()) + admin_path = '%s/%s/%s' % (namespace, repository, 'admin') + full_url = url_for('web.repository', path=admin_path) + '?tab=trigger' + logger.debug('Redirecting to full url: %s' % full_url) + return redirect(full_url) diff --git a/endpoints/common.py b/endpoints/common.py index ec4727edb..688cb7f33 100644 --- a/endpoints/common.py +++ b/endpoints/common.py @@ -2,7 +2,7 @@ import logging import os import base64 -from flask import request, abort, session, make_response +from flask import session, make_response, render_template from flask.ext.login import login_user, UserMixin from flask.ext.principal import identity_changed @@ -14,6 +14,39 @@ from auth.permissions import QuayDeferredPermissionUser logger = logging.getLogger(__name__) +route_data = None + +def get_route_data(): + global route_data + if route_data: + return route_data + + routes = [] + for rule in app.url_map.iter_rules(): + if rule.endpoint.startswith('api.'): + endpoint_method = app.view_functions[rule.endpoint] + is_internal = '__internal_call' in dir(endpoint_method) + is_org_api = '__user_call' in dir(endpoint_method) + methods = list(rule.methods.difference(['HEAD', 'OPTIONS'])) + + route = { + 'name': rule.endpoint[4:], + 'methods': methods, + 'path': rule.rule, + 'parameters': list(rule.arguments) + } + + if is_org_api: + route['user_method'] = endpoint_method.__user_call + + routes.append(route) + + route_data = { + 'endpoints': routes + } + return route_data + + @login_manager.user_loader def load_user(username): logger.debug('Loading user: %s' % username) @@ -68,3 +101,11 @@ def generate_csrf_token(): return session['_csrf_token'] app.jinja_env.globals['csrf_token'] = generate_csrf_token + + +def render_page_template(name, **kwargs): + + resp = make_response(render_template(name, route_data=get_route_data(), + **kwargs)) + resp.headers['X-FRAME-OPTIONS'] = 'DENY' + return resp \ No newline at end of file diff --git a/endpoints/trigger.py b/endpoints/trigger.py new file mode 100644 index 000000000..37d8fbf33 --- /dev/null +++ b/endpoints/trigger.py @@ -0,0 +1,111 @@ +import json +import requests +import logging + +from github import Github + +from app import app + + +user_files = app.config['USERFILES'] + + +logger = logging.getLogger(__name__) + + +ZIPBALL = 'application/zip' + + +class BuildArchiveException(Exception): + pass + + +class InvalidServiceException(Exception): + pass + + +class BuildTrigger(object): + def __init__(self): + pass + + def list_repositories(self, auth_token): + """ + Take the auth information for the specific trigger type and load the + list of repositories. + """ + raise NotImplementedError + + def incoming_webhook(self, request, auth_token, config): + """ + Transform the incoming request data into a set of actions. + """ + raise NotImplementedError + + @classmethod + def service_name(cls): + """ + Particular service implemented by subclasses. + """ + raise NotImplementedError + + @classmethod + def get_trigger_for_service(cls, service): + for subc in cls.__subclasses__(): + if subc.service_name() == service: + return subc() + + raise InvalidServiceException('Unable to find service: %s' % service) + + +class GithubBuildTrigger(BuildTrigger): + @staticmethod + def _get_client(auth_token): + return Github(auth_token, client_id=app.config['GITHUB_CLIENT_ID'], + client_secret=app.config['GITHUB_CLIENT_SECRET']) + + @classmethod + def service_name(cls): + return 'github' + + def list_repositories(self, auth_token): + gh_client = self._get_client(auth_token) + usr = gh_client.get_user() + + repo_list = [repo.full_name for repo in usr.get_repos()] + for org in usr.get_orgs(): + repo_list.extend((repo.full_name for repo in org.get_repos())) + + return repo_list + + def incoming_webhook(self, request, auth_token, config): + payload = request.get_json() + logger.debug('Payload %s', payload) + ref = payload['ref'] + commit_id = payload['head_commit']['id'][0:7] + + gh_client = self._get_client(auth_token) + + repo_full_name = '%s/%s' % (payload['repository']['owner']['name'], + payload['repository']['name']) + repo = gh_client.get_repo(repo_full_name) + + logger.debug('Github repo: %s', repo) + + # Prepare the download and upload URLs + branch_name = ref.split('/')[-1] + archive_link = repo.get_archive_link('zipball', branch_name) + download_archive = requests.get(archive_link, stream=True) + + upload_url, dockerfile_id = user_files.prepare_for_drop(ZIPBALL) + up_headers = {'Content-Type': ZIPBALL} + upload_archive = requests.put(upload_url, headers=up_headers, + data=download_archive.raw) + + if upload_archive.status_code / 100 != 2: + logger.debug('Failed to upload archive to s3') + raise BuildArchiveException('Unable to copy archie to s3 for ref: %s' % + ref) + + logger.debug('Successfully prepared job') + + return dockerfile_id, branch_name, commit_id \ No newline at end of file diff --git a/endpoints/web.py b/endpoints/web.py index cc56eeac6..3aa3617e1 100644 --- a/endpoints/web.py +++ b/endpoints/web.py @@ -2,19 +2,18 @@ import logging import requests import stripe -from flask import (abort, redirect, request, url_for, render_template, - make_response, Response, Blueprint) -from flask.ext.login import login_required, current_user +from flask import (abort, redirect, request, url_for, make_response, Response, + Blueprint) +from flask.ext.login import current_user from urlparse import urlparse from data import model -from app import app, mixpanel +from app import app from auth.permissions import AdministerOrganizationPermission from util.invoice import renderInvoiceToPdf from util.seo import render_snapshot from util.cache import no_cache -from endpoints.api import get_route_data -from endpoints.common import common_login +from endpoints.common import common_login, render_page_template logger = logging.getLogger(__name__) @@ -22,16 +21,7 @@ logger = logging.getLogger(__name__) web = Blueprint('web', __name__) -def render_page_template(name, **kwargs): - - resp = make_response(render_template(name, route_data=get_route_data(), - **kwargs)) - resp.headers['X-FRAME-OPTIONS'] = 'DENY' - return resp - - @web.route('/', methods=['GET'], defaults={'path': ''}) -@web.route('/repository/', methods=['GET']) @web.route('/organization/', methods=['GET']) @no_cache def index(path): @@ -106,9 +96,10 @@ def new(): return index('') -@web.route('/repository/') +@web.route('/repository/', defaults={'path': ''}) +@web.route('/repository/', methods=['GET']) @no_cache -def repository(): +def repository(path): return index('') @@ -179,97 +170,6 @@ def receipt(): abort(404) -def exchange_github_code_for_token(code): - code = request.args.get('code') - payload = { - 'client_id': app.config['GITHUB_CLIENT_ID'], - 'client_secret': app.config['GITHUB_CLIENT_SECRET'], - 'code': code, - } - headers = { - 'Accept': 'application/json' - } - - get_access_token = requests.post(app.config['GITHUB_TOKEN_URL'], - params=payload, headers=headers) - - token = get_access_token.json()['access_token'] - return token - - -def get_github_user(token): - token_param = { - 'access_token': token, - } - get_user = requests.get(app.config['GITHUB_USER_URL'], params=token_param) - - return get_user.json() - - -@web.route('/oauth2/github/callback', methods=['GET']) -def github_oauth_callback(): - error = request.args.get('error', None) - if error: - return render_page_template('githuberror.html', error_message=error) - - token = exchange_github_code_for_token(request.args.get('code')) - user_data = get_github_user(token) - - username = user_data['login'] - github_id = user_data['id'] - - v3_media_type = { - 'Accept': 'application/vnd.github.v3' - } - - token_param = { - 'access_token': token, - } - get_email = requests.get(app.config['GITHUB_USER_EMAILS'], - params=token_param, headers=v3_media_type) - - # We will accept any email, but we prefer the primary - found_email = None - for user_email in get_email.json(): - found_email = user_email['email'] - if user_email['primary']: - break - - to_login = model.verify_federated_login('github', github_id) - if not to_login: - # try to create the user - try: - to_login = model.create_federated_user(username, found_email, 'github', - github_id) - - # Success, tell mixpanel - mixpanel.track(to_login.username, 'register', {'service': 'github'}) - - state = request.args.get('state', None) - if state: - logger.debug('Aliasing with state: %s' % state) - mixpanel.alias(to_login.username, state) - - except model.DataModelException, ex: - return render_page_template('githuberror.html', error_message=ex.message) - - if common_login(to_login): - return redirect(url_for('web.index')) - - return render_page_template('githuberror.html') - - -@web.route('/oauth2/github/callback/attach', methods=['GET']) -@login_required -def github_oauth_attach(): - token = exchange_github_code_for_token(request.args.get('code')) - user_data = get_github_user(token) - github_id = user_data['id'] - user_obj = current_user.db_user() - model.attach_federated_login(user_obj, 'github', github_id) - return redirect(url_for('web.user')) - - @web.route('/confirm', methods=['GET']) def confirm_email(): code = request.values['code'] diff --git a/endpoints/webhooks.py b/endpoints/webhooks.py index 9d6b9f319..a35708e03 100644 --- a/endpoints/webhooks.py +++ b/endpoints/webhooks.py @@ -1,7 +1,6 @@ import logging import stripe import urlparse -import json from flask import request, make_response, Blueprint @@ -12,11 +11,14 @@ 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 + logger = logging.getLogger(__name__) webhooks = Blueprint('webhooks', __name__) + @webhooks.route('/stripe', methods=['POST']) def stripe_webhook(): request_data = request.get_json() @@ -41,22 +43,37 @@ def stripe_webhook(): return make_response('Okay') -@webhooks.route('/github/push/repository/', methods=['POST']) + +@webhooks.route('/push//trigger/', + methods=['POST']) @process_auth @parse_repository_name -def github_push_webhook(namespace, repository): - # data = request.get_json() +def github_push_webhook(namespace, repository, trigger_uuid): + logger.debug('Webhook received for %s/%s with uuid %s', namespace, + repository, trigger_uuid) permission = ModifyRepositoryPermission(namespace, repository) if permission.can(): - payload = json.loads(request.form['payload']) - ref = payload['ref'] + try: + trigger = model.get_build_trigger(namespace, repository, trigger_uuid) + except model.InvalidBuildTriggerException: + abort(404) - repo = model.get_repository(namespace, repository) - token = model.create_access_token(repo, 'write') + handler = BuildTrigger.get_trigger_for_service(trigger.service.name) + + logger.debug('Passing webhook request to handler %s', handler) + df_id, tag, name = handler.incoming_webhook(request, trigger.auth_token, + trigger.config) host = urlparse.urlparse(request.url).netloc - tag = '%s/%s/%s:latest' % (host, repo.namespace, repo.name) + full_tag = '%s/%s/%s:%s' % (host, trigger.repository.namespace, + trigger.repository.name, tag) - model.create_repository_build(repo, token, build_spec, tag) + token = model.create_access_token(trigger.repository, 'write') + logger.debug('Creating build %s with full_tag %s and dockerfile_id %s', + name, full_tag, df_id) + model.create_repository_build(trigger.repository, token, df_id, full_tag, + name) + + return make_response('Okay') abort(403) \ No newline at end of file diff --git a/initdb.py b/initdb.py index cb29d5246..0bc651d04 100644 --- a/initdb.py +++ b/initdb.py @@ -164,6 +164,8 @@ def initialize_database(): Visibility.create(name='private') LoginService.create(name='github') LoginService.create(name='quayrobot') + + BuildTriggerService.create(name='github') LogEntryKind.create(name='account_change_plan') LogEntryKind.create(name='account_change_cc') @@ -200,6 +202,8 @@ def initialize_database(): LogEntryKind.create(name='modify_prototype_permission') LogEntryKind.create(name='delete_prototype_permission') + LogEntryKind.create(name='delete_repo_trigger') + def wipe_database(): logger.debug('Wiping all data from the DB.') diff --git a/static/js/controllers.js b/static/js/controllers.js index d3ab397c1..237d65da0 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -1014,7 +1014,7 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope fetchRepository(); } -function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope) { +function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams, $rootScope) { var namespace = $routeParams.namespace; var name = $routeParams.name; @@ -1024,6 +1024,9 @@ function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope $scope.permissionCache = {}; + $scope.githubRedirectUri = KeyService.githubRedirectUri; + $scope.githubClientId = KeyService.githubClientId; + $scope.buildEntityForPermission = function(name, permission, kind) { var key = name + ':' + kind; if ($scope.permissionCache[key]) { @@ -1244,6 +1247,29 @@ function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope }); }; + $scope.loadTriggers = function() { + var params = { + 'repository': namespace + '/' + name + }; + + $scope.newWebhook = {}; + $scope.triggersResource = ApiService.listBuildTriggersAsResource(params).get(function(resp) { + $scope.triggers = resp.triggers; + return $scope.triggers; + }); + }; + + $scope.deletetrigger = function(trigger) { + var params = { + 'repository': namespace + '/' + name, + 'trigger_uuid': trigger.id + }; + + ApiService.deleteBuildTrigger(null, params).then(function(resp) { + $scope.triggers.splice($scope.triggers.indexOf(trigger), 1); + }); + }; + var fetchTokens = function() { var params = { 'repository': namespace + '/' + name diff --git a/static/partials/repo-admin.html b/static/partials/repo-admin.html index 3bfbae297..d320433b2 100644 --- a/static/partials/repo-admin.html +++ b/static/partials/repo-admin.html @@ -18,6 +18,7 @@ + +
+
+
Build Triggers + +
+ +
+ + +
+ Quay will do something. +
+
+
+
+
diff --git a/test/data/test.db b/test/data/test.db index 775f2c82df3f4c7678ad4e387d553c7218578905..c768c312082f3ddfceba0a4f0f446bf0369f4aac 100644 GIT binary patch delta 6605 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 I6%e5R4?yRUc>n+a From f60f9eb62a936931193f551c8bd7d5f46da21b2f Mon Sep 17 00:00:00 2001 From: jakedt Date: Tue, 18 Feb 2014 18:09:14 -0500 Subject: [PATCH 003/176] Properly connect the github push webhook with the build worker. Still need to resolve the archive format. --- config.py | 24 ++++++++++++++++++++---- data/userfiles.py | 9 +++++---- endpoints/api.py | 7 ++++--- endpoints/callbacks.py | 15 +++++++++------ endpoints/trigger.py | 28 +++++++++++++++------------- endpoints/web.py | 1 - endpoints/webhooks.py | 5 +++-- test/teststorage.py | 2 +- 8 files changed, 57 insertions(+), 34 deletions(-) diff --git a/config.py b/config.py index c81667d4d..bfb36426f 100644 --- a/config.py +++ b/config.py @@ -1,5 +1,6 @@ import logging import logstash_formatter +import requests from peewee import MySQLDatabase, SqliteDatabase from storage.s3 import S3Storage @@ -18,6 +19,7 @@ class FlaskConfig(object): SECRET_KEY = '1cb18882-6d12-440d-a4cc-b7430fb5f884' JSONIFY_PRETTYPRINT_REGULAR = False + class FlaskProdConfig(FlaskConfig): SESSION_COOKIE_SECURE = True @@ -163,9 +165,22 @@ def logs_init_builder(level=logging.DEBUG, return init_logs +def build_requests_session(): + sess = requests.Session() + adapter = requests.adapters.HTTPAdapter(pool_connections=100, + pool_maxsize=100) + sess.mount('http://', adapter) + sess.mount('https://', adapter) + return sess + + +class LargePoolHttpClient(object): + HTTPCLIENT = build_requests_session() + + class TestConfig(FlaskConfig, FakeStorage, EphemeralDB, FakeUserfiles, FakeAnalytics, StripeTestConfig, RedisBuildLogs, - UserEventConfig): + UserEventConfig, LargePoolHttpClient): LOGGING_CONFIG = logs_init_builder(logging.WARN) POPULATE_DB_TEST_DATA = True TESTING = True @@ -174,7 +189,7 @@ class TestConfig(FlaskConfig, FakeStorage, EphemeralDB, FakeUserfiles, class DebugConfig(FlaskConfig, MailConfig, LocalStorage, SQLiteDB, StripeTestConfig, MixpanelTestConfig, GitHubTestConfig, DigitalOceanConfig, BuildNodeConfig, S3Userfiles, - UserEventConfig, TestBuildLogs): + UserEventConfig, TestBuildLogs, LargePoolHttpClient): LOGGING_CONFIG = logs_init_builder(formatter=logging.Formatter()) SEND_FILE_MAX_AGE_DEFAULT = 0 POPULATE_DB_TEST_DATA = True @@ -184,7 +199,7 @@ class LocalHostedConfig(FlaskConfig, MailConfig, S3Storage, RDSMySQL, StripeLiveConfig, MixpanelTestConfig, GitHubProdConfig, DigitalOceanConfig, BuildNodeConfig, S3Userfiles, RedisBuildLogs, - UserEventConfig): + UserEventConfig, LargePoolHttpClient): LOGGING_CONFIG = logs_init_builder() SEND_FILE_MAX_AGE_DEFAULT = 0 @@ -192,7 +207,8 @@ class LocalHostedConfig(FlaskConfig, MailConfig, S3Storage, RDSMySQL, class ProductionConfig(FlaskProdConfig, MailConfig, S3Storage, RDSMySQL, StripeLiveConfig, MixpanelProdConfig, GitHubProdConfig, DigitalOceanConfig, BuildNodeConfig, - S3Userfiles, RedisBuildLogs, UserEventConfig): + S3Userfiles, RedisBuildLogs, UserEventConfig, + LargePoolHttpClient): LOGGING_CONFIG = logs_init_builder() SEND_FILE_MAX_AGE_DEFAULT = 0 diff --git a/data/userfiles.py b/data/userfiles.py index c2a8bc63c..cc314a47f 100644 --- a/data/userfiles.py +++ b/data/userfiles.py @@ -40,14 +40,15 @@ class UserRequestFiles(object): encrypt_key=True) return (url, file_id) - def store_file(self, flask_file): + def store_file(self, file_like_obj, content_type): self._initialize_s3() file_id = str(uuid4()) full_key = os.path.join(self._prefix, file_id) k = Key(self._bucket, full_key) - logger.debug('Setting s3 content type to: %s' % flask_file.content_type) - k.set_metadata('Content-Type', flask_file.content_type) - bytes_written = k.set_contents_from_file(flask_file, encrypt_key=True) + logger.debug('Setting s3 content type to: %s' % content_type) + k.set_metadata('Content-Type', content_type) + bytes_written = k.set_contents_from_file(file_like_obj, encrypt_key=True, + rewind=True) if bytes_written == 0: raise S3FileWriteException('Unable to write file to S3') diff --git a/endpoints/api.py b/endpoints/api.py index 389c74e0c..4b307337d 100644 --- a/endpoints/api.py +++ b/endpoints/api.py @@ -1,10 +1,10 @@ import logging import stripe -import requests import urlparse import json -from flask import request, make_response, jsonify, abort, url_for, Blueprint, session +from flask import (request, make_response, jsonify, abort, url_for, Blueprint, + session) from flask.ext.login import current_user, logout_user from flask.ext.principal import identity_changed, AnonymousIdentity from functools import wraps @@ -14,7 +14,8 @@ from data import model from data.queue import dockerfile_build_queue from data.plans import PLANS, get_plan from app import app -from util.email import send_confirmation_email, send_recovery_email, send_change_email +from util.email import (send_confirmation_email, send_recovery_email, + send_change_email) from util.names import parse_repository_name, format_robot_username from util.gravatar import compute_hash diff --git a/endpoints/callbacks.py b/endpoints/callbacks.py index 26b35f456..e78bda55e 100644 --- a/endpoints/callbacks.py +++ b/endpoints/callbacks.py @@ -1,4 +1,3 @@ -import requests import logging from flask import request, redirect, url_for, Blueprint @@ -12,9 +11,13 @@ from util.names import parse_repository_name logger = logging.getLogger(__name__) +client = app.config['HTTPCLIENT'] + + callback = Blueprint('callback', __name__) + def exchange_github_code_for_token(code): code = request.args.get('code') payload = { @@ -26,8 +29,8 @@ def exchange_github_code_for_token(code): 'Accept': 'application/json' } - get_access_token = requests.post(app.config['GITHUB_TOKEN_URL'], - params=payload, headers=headers) + get_access_token = client.post(app.config['GITHUB_TOKEN_URL'], + params=payload, headers=headers) token = get_access_token.json()['access_token'] return token @@ -37,7 +40,7 @@ def get_github_user(token): token_param = { 'access_token': token, } - get_user = requests.get(app.config['GITHUB_USER_URL'], params=token_param) + get_user = client.get(app.config['GITHUB_USER_URL'], params=token_param) return get_user.json() @@ -61,8 +64,8 @@ def github_oauth_callback(): token_param = { 'access_token': token, } - get_email = requests.get(app.config['GITHUB_USER_EMAILS'], - params=token_param, headers=v3_media_type) + get_email = client.get(app.config['GITHUB_USER_EMAILS'], params=token_param, + headers=v3_media_type) # We will accept any email, but we prefer the primary found_email = None diff --git a/endpoints/trigger.py b/endpoints/trigger.py index 37d8fbf33..8242e7ee6 100644 --- a/endpoints/trigger.py +++ b/endpoints/trigger.py @@ -1,19 +1,21 @@ -import json -import requests import logging +import io from github import Github +from tempfile import SpooledTemporaryFile from app import app user_files = app.config['USERFILES'] +client = app.config['HTTPCLIENT'] logger = logging.getLogger(__name__) ZIPBALL = 'application/zip' +CHUNK_SIZE = 512 * 1024 class BuildArchiveException(Exception): @@ -35,7 +37,7 @@ class BuildTrigger(object): """ raise NotImplementedError - def incoming_webhook(self, request, auth_token, config): + def handle_trigger_request(self, request, auth_token, config): """ Transform the incoming request data into a set of actions. """ @@ -57,6 +59,10 @@ class BuildTrigger(object): raise InvalidServiceException('Unable to find service: %s' % service) +def raise_unsupported(): + raise io.UnsupportedOperation + + class GithubBuildTrigger(BuildTrigger): @staticmethod def _get_client(auth_token): @@ -77,7 +83,7 @@ class GithubBuildTrigger(BuildTrigger): return repo_list - def incoming_webhook(self, request, auth_token, config): + def handle_trigger_request(self, request, auth_token, config): payload = request.get_json() logger.debug('Payload %s', payload) ref = payload['ref'] @@ -94,17 +100,13 @@ class GithubBuildTrigger(BuildTrigger): # Prepare the download and upload URLs branch_name = ref.split('/')[-1] archive_link = repo.get_archive_link('zipball', branch_name) - download_archive = requests.get(archive_link, stream=True) + download_archive = client.get(archive_link, stream=True) - upload_url, dockerfile_id = user_files.prepare_for_drop(ZIPBALL) - up_headers = {'Content-Type': ZIPBALL} - upload_archive = requests.put(upload_url, headers=up_headers, - data=download_archive.raw) + with SpooledTemporaryFile(CHUNK_SIZE) as zipball: + for chunk in download_archive.iter_content(CHUNK_SIZE): + zipball.write(chunk) - if upload_archive.status_code / 100 != 2: - logger.debug('Failed to upload archive to s3') - raise BuildArchiveException('Unable to copy archie to s3 for ref: %s' % - ref) + dockerfile_id = user_files.store_file(zipball, ZIPBALL) logger.debug('Successfully prepared job') diff --git a/endpoints/web.py b/endpoints/web.py index 3aa3617e1..9d898fbf7 100644 --- a/endpoints/web.py +++ b/endpoints/web.py @@ -1,5 +1,4 @@ import logging -import requests import stripe from flask import (abort, redirect, request, url_for, make_response, Response, diff --git a/endpoints/webhooks.py b/endpoints/webhooks.py index a35708e03..f153e633d 100644 --- a/endpoints/webhooks.py +++ b/endpoints/webhooks.py @@ -61,8 +61,9 @@ def github_push_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.incoming_webhook(request, trigger.auth_token, - trigger.config) + df_id, tag, name = handler.handle_trigger_request(request, + trigger.auth_token, + trigger.config) host = urlparse.urlparse(request.url).netloc full_tag = '%s/%s/%s:%s' % (host, trigger.repository.namespace, diff --git a/test/teststorage.py b/test/teststorage.py index 41768e09d..51d1fc8eb 100644 --- a/test/teststorage.py +++ b/test/teststorage.py @@ -30,7 +30,7 @@ class FakeUserfiles(object): def prepare_for_drop(self, mime_type): return ('http://fake/url', uuid4()) - def store_file(self, flask_file): + def store_file(self, file_like_obj, content_type): raise NotImplementedError() def get_file_url(self, file_id, expires_in=300): From 9e426816a58dcf5e328236877a0100899e8c5538 Mon Sep 17 00:00:00 2001 From: jakedt Date: Wed, 19 Feb 2014 16:08:33 -0500 Subject: [PATCH 004/176] Pass trigger information on build status. Set up a trigger for the sample building repository. Allow to list the builds started from a trigger. Protect the callback with the proper auth for creating a trigger on a repo. --- data/database.py | 1 + data/model.py | 51 +++++++++++++++++++++++++---------------- endpoints/api.py | 42 ++++++++++++++++++++++++--------- endpoints/callbacks.py | 24 +++++++++++++------ endpoints/trigger.py | 6 ++--- initdb.py | 13 +++++++++-- test/data/test.db | Bin 151552 -> 152576 bytes 7 files changed, 94 insertions(+), 43 deletions(-) diff --git a/data/database.py b/data/database.py index c72fa86a5..6020f9abc 100644 --- a/data/database.py +++ b/data/database.py @@ -230,6 +230,7 @@ class RepositoryBuild(BaseModel): phase = CharField(default='waiting') started = DateTimeField(default=datetime.now) display_name = CharField() + trigger = ForeignKeyField(RepositoryBuildTrigger, null=True, index=True) class QueueItem(BaseModel): diff --git a/data/model.py b/data/model.py index 98d1e28b2..b07d453e8 100644 --- a/data/model.py +++ b/data/model.py @@ -1299,35 +1299,38 @@ def load_token_data(code): def get_repository_build(namespace_name, repository_name, build_uuid): - joined = RepositoryBuild.select().join(Repository) - fetched = list(joined.where(Repository.name == repository_name, - Repository.namespace == namespace_name, - RepositoryBuild.uuid == build_uuid)) + try: + query = list_repository_builds(namespace_name, repository_name) + return query.where(RepositoryBuild.uuid == build_uuid).get() - if not fetched: + except RepositoryBuild.DoesNotExist: msg = 'Unable to locate a build by id: %s' % build_uuid raise InvalidRepositoryBuildException(msg) - return fetched[0] - def list_repository_builds(namespace_name, repository_name, include_inactive=True): - joined = RepositoryBuild.select().join(Repository) - filtered = joined + query = (RepositoryBuild + .select(RepositoryBuild, RepositoryBuildTrigger, BuildTriggerService) + .join(Repository) + .switch(RepositoryBuild) + .join(RepositoryBuildTrigger, JOIN_LEFT_OUTER) + .join(BuildTriggerService, JOIN_LEFT_OUTER) + .where(Repository.name == repository_name, + Repository.namespace == namespace_name)) + if not include_inactive: - filtered = filtered.where(RepositoryBuild.phase != 'error', - RepositoryBuild.phase != 'complete') - fetched = list(filtered.where(Repository.name == repository_name, - Repository.namespace == namespace_name)) - return fetched + query = query.where(RepositoryBuild.phase != 'error', + RepositoryBuild.phase != 'complete') + + return query def create_repository_build(repo, access_token, resource_key, tag, - display_name): + display_name, trigger=None): return RepositoryBuild.create(repository=repo, access_token=access_token, resource_key=resource_key, tag=tag, - display_name=display_name) + display_name=display_name, trigger=trigger) def create_webhook(repo, params_obj): @@ -1386,11 +1389,8 @@ def log_action(kind_name, user_or_organization_name, performer=None, metadata_json=json.dumps(metadata), datetime=timestamp) -def create_build_trigger(namespace_name, repository_name, service_name, - auth_token, user): +def create_build_trigger(repo, service_name, auth_token, user): service = BuildTriggerService.get(name=service_name) - repo = Repository.get(namespace=namespace_name, name=repository_name) - trigger = RepositoryBuildTrigger.create(repository=repo, service=service, auth_token=auth_token, connected_user=user) @@ -1428,3 +1428,14 @@ 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) trigger.delete_instance() + + +def list_trigger_builds(namespace_name, repository_name, trigger_uuid, + limit=None): + query = (list_repository_builds(namespace_name, repository_name) + .where(RepositoryBuildTrigger.uuid == trigger_uuid)) + + if limit: + return query.limit(limit) + else: + return query diff --git a/endpoints/api.py b/endpoints/api.py index 4b307337d..df2260edf 100644 --- a/endpoints/api.py +++ b/endpoints/api.py @@ -1110,7 +1110,7 @@ def get_repo(namespace, repository): 'can_write': can_write, 'can_admin': can_admin, 'is_public': is_public, - 'is_building': len(active_builds) > 0, + 'is_building': len(list(active_builds)) > 0, 'is_organization': bool(organization) }) @@ -1118,6 +1118,18 @@ def get_repo(namespace, repository): abort(403) # Permission denied +def trigger_view(trigger): + if trigger and trigger.uuid: + return { + 'service': trigger.service.name, + 'config': json.loads(trigger.config), + 'id': trigger.uuid, + 'connected_user': trigger.connected_user.username, + } + + return None + + def build_status_view(build_obj, can_write=False): status = build_logs.get_status(build_obj.uuid) return { @@ -1127,7 +1139,8 @@ def build_status_view(build_obj, can_write=False): 'display_name': build_obj.display_name, 'status': status, 'resource_key': build_obj.resource_key if can_write else None, - 'is_writer': can_write + 'is_writer': can_write, + 'trigger': trigger_view(build_obj.trigger), } @@ -1328,15 +1341,6 @@ def delete_webhook(namespace, repository, public_id): abort(403) # Permission denied -def trigger_view(trigger): - return { - 'service': trigger.service.name, - 'config': json.loads(trigger.config), - 'id': trigger.uuid, - 'connected_user': trigger.connected_user.username, - } - - @api.route('/repository//trigger/', methods=['GET']) @api_login_required @@ -1354,6 +1358,22 @@ def get_build_trigger(namespace, repository, trigger_uuid): abort(403) # Permission denied +@api.route('/repository//trigger//builds', + methods=['GET']) +@api_login_required +@parse_repository_name +def list_trigger_recent_builds(namespace, repository, trigger_uuid): + permission = AdministerRepositoryPermission(namespace, repository) + if permission.can(): + limit = request.args.get('limit', 5) + builds = model.list_trigger_builds(namespace, repository, trigger_uuid, + limit) + return jsonify({ + 'builds': [build_status_view(build, True) for build in builds] + }) + + abort(403) # Permission denied + @api.route('/repository//trigger/', methods=['GET']) @api_login_required @parse_repository_name diff --git a/endpoints/callbacks.py b/endpoints/callbacks.py index e78bda55e..791c1a613 100644 --- a/endpoints/callbacks.py +++ b/endpoints/callbacks.py @@ -7,6 +7,8 @@ from endpoints.common import render_page_template, common_login from app import app, mixpanel from data import model from util.names import parse_repository_name +from util.http import abort +from auth.permissions import AdministerRepositoryPermission logger = logging.getLogger(__name__) @@ -113,10 +115,18 @@ def github_oauth_attach(): @login_required @parse_repository_name def attach_github_build_trigger(namespace, repository): - token = exchange_github_code_for_token(request.args.get('code')) - model.create_build_trigger(namespace, repository, 'github', token, - current_user.db_user()) - admin_path = '%s/%s/%s' % (namespace, repository, 'admin') - full_url = url_for('web.repository', path=admin_path) + '?tab=trigger' - logger.debug('Redirecting to full url: %s' % full_url) - return redirect(full_url) + permission = AdministerRepositoryPermission(namespace, repository) + if permission.can(): + token = exchange_github_code_for_token(request.args.get('code')) + repo = model.get_repository(namespace, repository) + if not repo: + msg = 'Invalid repository: %s/%s' % (namespace, repository) + abort(404, message=msg) + + model.create_build_trigger(repo, 'github', token, current_user.db_user()) + admin_path = '%s/%s/%s' % (namespace, repository, 'admin') + full_url = url_for('web.repository', path=admin_path) + '?tab=trigger' + logger.debug('Redirecting to full url: %s' % full_url) + return redirect(full_url) + + abort(403) \ No newline at end of file diff --git a/endpoints/trigger.py b/endpoints/trigger.py index 8242e7ee6..dfcb21b83 100644 --- a/endpoints/trigger.py +++ b/endpoints/trigger.py @@ -30,10 +30,10 @@ class BuildTrigger(object): def __init__(self): pass - def list_repositories(self, auth_token): + def list_build_sources(self, auth_token): """ Take the auth information for the specific trigger type and load the - list of repositories. + list of build sources(repositories). """ raise NotImplementedError @@ -73,7 +73,7 @@ class GithubBuildTrigger(BuildTrigger): def service_name(cls): return 'github' - def list_repositories(self, auth_token): + def list_build_sources(self, auth_token): gh_client = self._get_client(auth_token) usr = gh_client.get_user() diff --git a/initdb.py b/initdb.py index 075798e5a..ddef21e51 100644 --- a/initdb.py +++ b/initdb.py @@ -281,8 +281,17 @@ def populate_database(): token = model.create_access_token(building, 'write') tag = 'ci.devtable.com:5000/%s/%s' % (building.namespace, building.name) - build = model.create_repository_build(building, token, '701dcc3724fb4f2ea6c31400528343cd', - tag, 'build-name') + + trigger = model.create_build_trigger(building, 'github', '123authtoken', + new_user_1) + trigger.config = json.dumps({ + 'build_source': 'jakedt/testconnect', + }) + trigger.save() + + build = model.create_repository_build(building, token, + '701dcc3724fb4f2ea6c31400528343cd', + tag, 'build-name', trigger) build.uuid = 'deadbeef-dead-beef-dead-beefdeadbeef' build.save() diff --git a/test/data/test.db b/test/data/test.db index 36e07e99a25591e58d468feb4fe8f28527ccef8c..45dd94d43ffaee5403429bf30a98bcc3bb0b9281 100644 GIT binary patch delta 4379 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 delta 4120 zcmbtWc~p~E7SDa(V+kYziUKOKSVY9^J5`YoAcQPHfCO9v1PP*m0-}yZtF0naH+p=Y z&Z!l3Y)==pVtLhW+NpLQr#e+@)u|q>4v4kVy4A|OPmi_^p8ipjlixe{{@#1{es{~a z_!Z&eO~N(9!Zr#7g6G-q{>Qa;tcocmXOsMxDvW!LCo12poxq{e{EnU6;_QhoORg@@ zq_?;=YOBqdX?L3q2AiR%$d+DWW;#=mGq*@#a2Ds=3^u(@RistvHHDURdydtvE3)Ui z+iGn^n(_svDtG#f*=l!%&L?ZK<`!5yN~zqdw5Tc_d5t-aMq^&1ugc?Stn_LM+?hpA zb-J?c7u#sfg4|M7LqTC0-&4TUC{$r@#LVTDy+TC0<18O-_f6sDpIoyI#ySC%`=?$o=SX=a%%tD&}WPQD_2 zc2-WgExX)2zwK>%qQ+~Pn{GE4tu?xt^If%OO?{Kq>~?sHD`)6t&d#w*OA6C!@-hmG z?9Ni7Twd>}ncGzAZ3}m3Cfby){A_t&SOaY2SbZ(`q>xmuTTuED2CZ}fJYa7aTjw;BD|A)4NW9zSi`vbx89oQ?}+ ztGVXKtspb_3jmgg5&Bc%4gOtxcU1~S+#as3BP`I%kxl!+zZpHQdnnV7IVb#c&RK(g zOa88=rqpJYszRqM(`zIegFzut%hhs;r>tBl(a5zGWfi66dWA~MAsGr3K%tO@B|xK8 zdObRoS|V3z^b)nFqFhqy(JLi#wO6MpSIN~1uNrYOxO)Hu5TL8jt944=g1)R=!aB$x zF?i()iAt|hdjhmtgI=vK@3=$#^&MCZxWn(KK5h#hE20>~ro>5hmC`AYTPBQ=zlOm}|>5D^zByQE$)EYUM@QS(*Myxq>;X zlqmGE3T=u)m7-KhwR)A_V4!`;us<%PCRbNdW8CPF^nei zVs`Gl8%95313NdA=!?Pl2#y_2BD;$r>d{7FjYV9t}m~bK=LEF zizQGHrtEiwc{iApP6F8@xXqIwMcwbn!uWcxXI6YlZ2TOrr`$VdZv3Kq@t!6SOhcVq z47ZBK+Y!60ATuB$IvQ3itoM{wd#h_^cxTtonSXyRq(2sb9zhlvH`(zkkdXGN!Ia-s45r+-8&9Q8VB(tBRWW&O1%uV444#|H;IAbNT8kL8DScBxSHLnL*QN2J;daG{!S%h+!~y7=t;H4C=xe%ns$1vqTK4 z0~yT3h#}&(G8C-g4su)Bh$0+b5Y45Me|1ARsWl@1z$Y0yU&GaqAO8zV+Sd#s46CSU z84!S#Wa~4K1J9Ey{PGML_bgkMkQMy$7xHg@sVA|^*-}fY`K5-uy&SA;c;^a8&v$(|Nalg1W^Cm|~#9rB2C zC9CN&GR=?G_qvsE1M*30E6AX{O?DxY9%%)`VAhD@b{Gv+MB5G~C?X5n;R*1OO}ykJ z=iAwpds`uz^tIpZz)L??iXr}A!#fzh z|J=cg!|9o?*^}a^ilZK0jw0w7oWm2k=1k?qA)UuzHsF-;?|saRqv;FZfQSWY+y1k> zoInqq;4KEP8Y^Z-<})X85Ir14%&`I)r1NS(HN(2ci#f%KD{i zXxBjG2iV{1^U>Zxs25|g)iZ^5Me^Qi6P{`(H>1!+Jmzqeg`OXZ#4KXw%B}R;FoY2f zyzxORy%UXwv8}-`?I6)H=xaQF;fsOfS`500!&^RzAV0*TAw(F5_T#8|lgHc@>A5(> zx-;DJ^g>>~g%#oAT-q}db+IIDPnXkg6L>>@bvt_KrBR4=-}q5uFVVh4WcR}n7eY^w zJPA6&DLyz(5~b)v9J2PC8?;BtdmMN6?7QT)3|+(eOFJ9sH92C{Fk)NtG=h}qJT`pz z{TkY>L@Y_+U+p@|-Y0oJWPN8f4c745nCDhFX^a-JS{&DLD3Qo?=nI_m+MW{Hr$f{I zuZT=SqD$GV~5-3t-f2SiXynXTf$O_;l+AD~z5yrsaz<_)yZfdKyxCWypZ From 5b0300ab62a92d281fef0dd35783720469e9bae2 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Wed, 19 Feb 2014 17:38:00 -0500 Subject: [PATCH 005/176] Get initial build trigger UI working --- static/css/quay.css | 7 +++ static/js/controllers.js | 25 +++++++++-- static/partials/repo-admin.html | 78 ++++++++++++++++++++++++++++++--- 3 files changed, 101 insertions(+), 9 deletions(-) diff --git a/static/css/quay.css b/static/css/quay.css index 0bb108ed3..6fdc5b864 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -1899,6 +1899,13 @@ p.editable:hover i { left: 4px; } +.repo-admin .right-controls { + text-align: right; + margin-top: 10px; + padding-top: 10px; + border-top: 1px solid #eee; +} + .repo-admin .right-info { font-size: 11px; margin-top: 10px; diff --git a/static/js/controllers.js b/static/js/controllers.js index 897281520..a071e6572 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -1145,7 +1145,7 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope fetchRepository(); } -function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams, $rootScope) { +function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams, $rootScope, $location) { var namespace = $routeParams.namespace; var name = $routeParams.name; @@ -1378,19 +1378,38 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams }); }; + $scope.showBuild = function(buildInfo) { + $location.path('/repository/' + namespace + '/' + name + '/build'); + $location.search('current', buildInfo.id); + }; + + $scope.loadTriggerBuildHistory = function(trigger) { + trigger.$loadingHistory = true; + + var params = { + 'repository': namespace + '/' + name, + 'trigger_uuid': trigger.id, + 'limit': 3 + }; + + ApiService.listTriggerRecentBuilds(null, params).then(function(resp) { + trigger.$builds = resp['builds']; + trigger.$loadingHistory = false; + }); + }; + $scope.loadTriggers = function() { var params = { 'repository': namespace + '/' + name }; - $scope.newWebhook = {}; $scope.triggersResource = ApiService.listBuildTriggersAsResource(params).get(function(resp) { $scope.triggers = resp.triggers; return $scope.triggers; }); }; - $scope.deletetrigger = function(trigger) { + $scope.deleteTrigger = function(trigger) { var params = { 'repository': namespace + '/' + name, 'trigger_uuid': trigger.id diff --git a/static/partials/repo-admin.html b/static/partials/repo-admin.html index d320433b2..2964c8936 100644 --- a/static/partials/repo-admin.html +++ b/static/partials/repo-admin.html @@ -211,16 +211,82 @@
Build Triggers - +
- + +
+
No build triggers defined for this repository
+ + + + + + + + + + + + + + + +
Trigger
+
+
+ + Push to GitHub repository {{ trigger.config.build_source }} +
+
+ Unknown +
+
+
+ + + +
+ + + +
+ +
-
- Quay will do something.
From c494c889f56e79e0aa55c220e357604c8724c922 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Thu, 20 Feb 2014 13:27:59 -0500 Subject: [PATCH 006/176] Add info to the build pane that shows if a trigger started the build --- static/directives/trigger-description.html | 9 +++++++++ static/js/app.js | 17 +++++++++++++++++ static/js/controllers.js | 6 +++++- static/partials/repo-admin.html | 12 ++---------- static/partials/repo-build.html | 4 ++++ 5 files changed, 37 insertions(+), 11 deletions(-) create mode 100644 static/directives/trigger-description.html diff --git a/static/directives/trigger-description.html b/static/directives/trigger-description.html new file mode 100644 index 000000000..11f48e57d --- /dev/null +++ b/static/directives/trigger-description.html @@ -0,0 +1,9 @@ + + + + Push to GitHub repository {{ trigger.config.build_source }} + + + Unknown + + diff --git a/static/js/app.js b/static/js/app.js index cdb939745..20851b2b8 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -2527,6 +2527,23 @@ quayApp.directive('buildLogError', function () { }); +quayApp.directive('triggerDescription', function () { + var directiveDefinitionObject = { + priority: 0, + templateUrl: '/static/directives/trigger-description.html', + replace: false, + transclude: false, + restrict: 'C', + scope: { + 'trigger': '=trigger' + }, + controller: function($scope, $element) { + } + }; + return directiveDefinitionObject; +}); + + quayApp.directive('buildLogCommand', function () { var directiveDefinitionObject = { priority: 0, diff --git a/static/js/controllers.js b/static/js/controllers.js index a071e6572..fe7ca7004 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -936,7 +936,11 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope }; $scope.adjustLogHeight = function() { - $('.build-logs').height($(window).height() - 415); + var triggerOffset = 0; + if ($scope.currentBuild && $scope.currentBuild.trigger) { + triggerOffset = 85; + } + $('.build-logs').height($(window).height() - 415 - triggerOffset); }; $scope.askRestartBuild = function(build) { diff --git a/static/partials/repo-admin.html b/static/partials/repo-admin.html index 2964c8936..1c165e32f 100644 --- a/static/partials/repo-admin.html +++ b/static/partials/repo-admin.html @@ -211,7 +211,7 @@
Build Triggers - +
@@ -230,15 +230,7 @@ -
-
- - Push to GitHub repository {{ trigger.config.build_source }} -
-
- Unknown -
-
+