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 775f2c82d..c768c3120 100644 Binary files a/test/data/test.db and b/test/data/test.db differ