diff --git a/endpoints/api.py b/endpoints/api.py index df2260edf..7a3e469f7 100644 --- a/endpoints/api.py +++ b/endpoints/api.py @@ -25,8 +25,11 @@ from auth.permissions import (ReadRepositoryPermission, CreateRepositoryPermission, AdministerOrganizationPermission, OrganizationMemberPermission, - ViewTeamPermission) + ViewTeamPermission, + UserPermission + ) from endpoints.common import common_login, get_route_data +from endpoints.trigger import BuildTrigger, TriggerActivationException from util.cache import cache_control from datetime import datetime, timedelta @@ -1120,11 +1123,14 @@ def get_repo(namespace, repository): def trigger_view(trigger): if trigger and trigger.uuid: + config_dict = json.loads(trigger.config) + build_trigger = BuildTrigger.get_trigger_for_service(trigger.service.name) return { 'service': trigger.service.name, - 'config': json.loads(trigger.config), + 'config': config_dict, 'id': trigger.uuid, 'connected_user': trigger.connected_user.username, + 'is_active': build_trigger.is_active(config_dict) } return None @@ -1358,6 +1364,44 @@ def get_build_trigger(namespace, repository, trigger_uuid): abort(403) # Permission denied +@api.route('/repository//trigger//activate', + methods=['POST']) +@api_login_required +@parse_repository_name +def activate_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 + + handler = BuildTrigger.get_trigger_for_service(trigger.service.name) + existing_config_dict = json.loads(trigger.config) + if handler.is_active(existing_config_dict): + abort(400) + return + + user_permission = UserPermission(trigger.connected_user.username) + if user_permission.can(): + new_config_dict = request.get_json() + + try: + handler.activate(trigger.auth_token, new_config_dict) + except TriggerActivationException as e: + abort(400, message = e.msg) + return + + # Save the updated config. + trigger.config = json.dumps(new_config_dict) + trigger.save() + + return jsonify(trigger_view(trigger)) + + abort(403) # Permission denied + + @api.route('/repository//trigger//builds', methods=['GET']) @api_login_required @@ -1374,6 +1418,31 @@ def list_trigger_recent_builds(namespace, repository, trigger_uuid): abort(403) # Permission denied + +@api.route('/repository//trigger//sources', + methods=['GET']) +@api_login_required +@parse_repository_name +def list_trigger_build_sources(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) + + user_permission = UserPermission(trigger.connected_user.username) + if user_permission.can(): + trigger_handler = BuildTrigger.get_trigger_for_service(trigger.service.name) + + return jsonify({ + 'sources': trigger_handler.list_build_sources(trigger.auth_token) + }) + + 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 791c1a613..a32819aff 100644 --- a/endpoints/callbacks.py +++ b/endpoints/callbacks.py @@ -123,10 +123,10 @@ def attach_github_build_trigger(namespace, repository): msg = 'Invalid repository: %s/%s' % (namespace, repository) abort(404, message=msg) - model.create_build_trigger(repo, 'github', token, current_user.db_user()) + trigger = 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' + full_url = url_for('web.repository', path=admin_path) + '?tab=trigger&new_trigger=' + trigger.uuid logger.debug('Redirecting to full url: %s' % full_url) return redirect(full_url) - abort(403) \ No newline at end of file + abort(403) diff --git a/endpoints/trigger.py b/endpoints/trigger.py index dfcb21b83..67f9e5624 100644 --- a/endpoints/trigger.py +++ b/endpoints/trigger.py @@ -21,10 +21,12 @@ CHUNK_SIZE = 512 * 1024 class BuildArchiveException(Exception): pass - class InvalidServiceException(Exception): pass +class TriggerActivationException(Exception): + pass + class BuildTrigger(object): def __init__(self): @@ -43,6 +45,18 @@ class BuildTrigger(object): """ raise NotImplementedError + def is_active(self, config): + """ + Returns True if the current build trigger is active. Inactive means further setup is needed. + """ + raise NotImplementedError + + def activate(self, auth_token, config): + """ + Activates the trigger for the service, with the given new configuration. + """ + raise NotImplementedError + @classmethod def service_name(cls): """ @@ -73,15 +87,43 @@ class GithubBuildTrigger(BuildTrigger): def service_name(cls): return 'github' + def is_active(self, config): + return 'build_source' in config and len(config['build_source']) > 0 + + def activate(self, auth_token, config): + # TODO: Add the callback web hook to the github repository. + pass + def list_build_sources(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())) + personal = { + 'personal': True, + 'repos': [repo.full_name for repo in usr.get_repos()], + 'info': { + 'name': usr.login, + 'avatar_url': usr.avatar_url, + } + } - return repo_list + repos_by_org = [personal] + + for org in usr.get_orgs(): + repo_list = [] + for repo in org.get_repos(): + repo_list.append(repo.full_name) + + repos_by_org.append({ + 'personal': False, + 'repos': repo_list, + 'info': { + 'name': org.name, + 'avatar_url': org.avatar_url + } + }) + + return repos_by_org def handle_trigger_request(self, request, auth_token, config): payload = request.get_json() @@ -110,4 +152,4 @@ class GithubBuildTrigger(BuildTrigger): logger.debug('Successfully prepared job') - return dockerfile_id, branch_name, commit_id \ No newline at end of file + return dockerfile_id, branch_name, commit_id diff --git a/static/css/quay.css b/static/css/quay.css index 6fdc5b864..c1d8aa8c6 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -3247,4 +3247,61 @@ pre.command:before { .label.MAINTAINER { border-color: #aaa !important; +} + +.dropdown-select { + margin: 10px; + position: relative; +} + +.dropdown-select .dropdown-icon { + position: absolute; + top: 6px; + left: 6px; + z-index: 2; +} + +.dropdown-select .dropdown-icon.none-icon { + top: 10px; + left: 8px; + font-size: 20px; + color: #ccc; +} + +.dropdown-select .lookahead-input { + padding-left: 32px; +} + +.dropdown-select .twitter-typeahead { + display: block !important; +} + +.dropdown-select .twitter-typeahead .tt-hint { + padding-left: 32px; +} + +.dropdown-select .dropdown { + position: absolute; + right: 0px; + top: 0px; +} + +.dropdown-select .dropdown button.dropdown-toggle { + border-top-left-radius: 0px; + border-bottom-left-radius: 0px; +} + +.trigger-setup-github-element .github-org-icon { + width: 20px; + margin-right: 8px; + vertical-align: middle; +} + +.trigger-setup-github-element li.github-repo-listing i { + margin-right: 10px; + margin-left: 6px; +} + +.trigger-setup-github-element li.github-org-header { + padding-left: 6px; } \ No newline at end of file diff --git a/static/directives/trigger-setup-github.html b/static/directives/trigger-setup-github.html new file mode 100644 index 000000000..02f44498b --- /dev/null +++ b/static/directives/trigger-setup-github.html @@ -0,0 +1,30 @@ +
+
+ + Loading Repository List +
+
+
Please choose the GitHub repository that will trigger the build:
+ + +
+
diff --git a/static/js/app.js b/static/js/app.js index 20851b2b8..7f3119c36 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -2544,6 +2544,96 @@ quayApp.directive('triggerDescription', function () { }); +quayApp.directive('triggerSetupGithub', function () { + var directiveDefinitionObject = { + priority: 0, + templateUrl: '/static/directives/trigger-setup-github.html', + replace: false, + transclude: false, + restrict: 'C', + scope: { + 'repository': '=repository', + 'trigger': '=trigger' + }, + controller: function($scope, $element, ApiService) { + $scope.setupReady = false; + $scope.loading = true; + + var input = $($element).find('.lookahead-input'); + + $scope.clearSelectedRepo = function() { + $scope.currentRepo = null; + $scope.trigger.$ready = false; + }; + + $scope.selectRepo = function(repo, org) { + $(input).val(repo); + $scope.selectRepoInternal(repo, org); + }; + + $scope.selectRepoInternal = function(repo, org) { + $scope.currentRepo = { + 'name': repo, + 'avatar_url': org['info']['avatar_url'] + }; + $scope.trigger['config'] = { + 'build_source': repo + }; + $scope.trigger.$ready = true; + }; + + var setupTypeahead = function() { + var repos = []; + for (var i = 0; i < $scope.orgs.length; ++i) { + var org = $scope.orgs[i]; + var orepos = org['repos']; + for (var j = 0; j < orepos.length; ++j) { + repos.push({'name': orepos[j], 'org': org, 'value': orepos[j]}); + } + } + + $(input).typeahead({ + name: 'repos-' + $scope.trigger.id, + local: repos, + template: function (datum) { + template = datum['name']; + return template; + } + }); + }; + + var loadSources = function() { + var params = { + 'repository': $scope.repository.namespace + '/' + $scope.repository.name, + 'trigger_uuid': $scope.trigger.id + }; + + ApiService.listTriggerBuildSources(null, params).then(function(resp) { + $scope.orgs = resp['sources']; + setupTypeahead(); + $scope.loading = false; + }); + }; + + loadSources(); + + $(input).on('input', function(e) { + $scope.$apply(function() { + $scope.clearSelectedRepo(); + }); + }); + + $(input).on('typeahead:selected', function(e, datum) { + $scope.$apply(function() { + $scope.selectRepoInternal(datum.repo, datum.org); + }); + }); + } + }; + return directiveDefinitionObject; +}); + + quayApp.directive('buildLogCommand', function () { var directiveDefinitionObject = { priority: 0, @@ -3115,8 +3205,14 @@ quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanServi var tabName = e.target.getAttribute('data-target').substr(1); $rootScope.$apply(function() { var isDefaultTab = $('a[data-toggle="tab"]')[0] == e.target; - var data = isDefaultTab ? {} : {'tab': tabName}; - $location.search(data); + var newSearch = $.extend($location.search(), {}); + if (isDefaultTab) { + delete newSearch['tab']; + } else { + newSearch['tab'] = tabName; + } + + $location.search(newSearch); }); e.preventDefault(); diff --git a/static/js/controllers.js b/static/js/controllers.js index fe7ca7004..b398838a8 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -1409,10 +1409,60 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams $scope.triggersResource = ApiService.listBuildTriggersAsResource(params).get(function(resp) { $scope.triggers = resp.triggers; + + // Check to see if we need to setup any trigger. + var newTriggerId = $routeParams.new_trigger; + if (newTriggerId) { + for (var i = 0; i < $scope.triggers.length; ++i) { + var trigger = $scope.triggers[i]; + if (trigger['id'] == newTriggerId && !trigger['is_active']) { + $scope.setupTrigger(trigger); + break; + } + } + } + return $scope.triggers; }); }; + $scope.setupTrigger = function(trigger) { + $scope.triggerSetupReady = false; + $scope.currentSetupTrigger = trigger; + $('#setupTriggerModal').modal({}); + }; + + $scope.finishSetupTrigger = function(trigger) { + $('#setupTriggerModal').modal('hide'); + $scope.currentSetupTrigger = null; + + var params = { + 'repository': namespace + '/' + name, + 'trigger_uuid': trigger.id + }; + + ApiService.activateBuildTrigger(trigger['config'], params).then(function(resp) { + trigger['is_active'] = true; + }, function(resp) { + bootbox.dialog({ + "message": resp['message'] || 'The build trigger setup could not be completed', + "title": "Could not activate build trigger", + "buttons": { + "close": { + "label": "Close", + "className": "btn-primary" + } + } + }); + }); + }; + + $scope.cancelSetupTrigger = function() { + $('#setupTriggerModal').modal('hide'); + $scope.deleteTrigger($scope.currentSetupTrigger); + $scope.currentSetupTrigger = null; + }; + $scope.deleteTrigger = function(trigger) { var params = { 'repository': namespace + '/' + name, diff --git a/static/partials/repo-admin.html b/static/partials/repo-admin.html index 1c165e32f..edc2d1ebe 100644 --- a/static/partials/repo-admin.html +++ b/static/partials/repo-admin.html @@ -230,10 +230,14 @@ -
+
+ + Setting up trigger +
+
- + + + +