From b5d49193647477276d582642e3cab4c085ef2474 Mon Sep 17 00:00:00 2001 From: jakedt Date: Tue, 18 Feb 2014 15:50:15 -0500 Subject: [PATCH] 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