From 039d53ea6cd7b56bf64771611604c4e0a865d7bf Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Tue, 30 Sep 2014 16:29:32 -0400 Subject: [PATCH] - Fix initdb - Add ability to specific custom fields for manual running of build triggers and add a "branch name" selector for running github builds --- endpoints/api/trigger.py | 63 +++++++++-- endpoints/trigger.py | 53 +++++++-- initdb.py | 2 +- static/css/quay.css | 11 ++ .../manual-trigger-build-dialog.html | 38 +++++++ static/js/app.js | 106 ++++++++++++++++-- static/js/controllers.js | 18 ++- static/partials/repo-admin.html | 10 +- test/test_api_security.py | 62 +++++++++- test/test_api_usage.py | 24 +++- 10 files changed, 346 insertions(+), 41 deletions(-) create mode 100644 static/directives/manual-trigger-build-dialog.html diff --git a/endpoints/api/trigger.py b/endpoints/api/trigger.py index ccccf8010..081641e00 100644 --- a/endpoints/api/trigger.py +++ b/endpoints/api/trigger.py @@ -14,7 +14,7 @@ from endpoints.api.build import (build_status_view, trigger_view, RepositoryBuil from endpoints.common import start_build from endpoints.trigger import (BuildTrigger as BuildTriggerBase, TriggerDeactivationException, TriggerActivationException, EmptyRepositoryException, - RepositoryReadException) + RepositoryReadException, TriggerStartException) from data import model from auth.permissions import UserAdminPermission, AdministerOrganizationPermission, ReadRepositoryPermission from util.names import parse_robot_username @@ -374,9 +374,24 @@ class BuildTriggerAnalyze(RepositoryParamResource): @resource('/v1/repository//trigger//start') class ActivateBuildTrigger(RepositoryParamResource): """ Custom verb to manually activate a build trigger. """ + schemas = { + 'RunParameters': { + 'id': 'RunParameters', + 'type': 'object', + 'description': 'Optional run parameters for activating the build trigger', + 'additional_properties': False, + 'properties': { + 'branch_name': { + 'type': 'string', + 'description': '(GitHub Only) If specified, the name of the GitHub branch to build.' + } + } + } + } @require_repo_admin @nickname('manuallyStartBuildTrigger') + @validate_json_request('RunParameters') def post(self, namespace, repository, trigger_uuid): """ Manually start a build from the specified trigger. """ try: @@ -389,14 +404,18 @@ class ActivateBuildTrigger(RepositoryParamResource): if not handler.is_active(config_dict): raise InvalidRequest('Trigger is not active.') - specs = handler.manual_start(trigger.auth_token, config_dict) - dockerfile_id, tags, name, subdir = specs + try: + run_parameters = request.get_json() + specs = handler.manual_start(trigger.auth_token, config_dict, run_parameters=run_parameters) + dockerfile_id, tags, name, subdir = specs - repo = model.get_repository(namespace, repository) - pull_robot_name = model.get_pull_robot_name(trigger) + repo = model.get_repository(namespace, repository) + pull_robot_name = model.get_pull_robot_name(trigger) - build_request = start_build(repo, dockerfile_id, tags, name, subdir, True, - pull_robot_name=pull_robot_name) + build_request = start_build(repo, dockerfile_id, tags, name, subdir, True, + pull_robot_name=pull_robot_name) + except TriggerStartException as tse: + raise InvalidRequest(tse.message) resp = build_status_view(build_request, True) repo_string = '%s/%s' % (namespace, repository) @@ -424,6 +443,36 @@ class TriggerBuildList(RepositoryParamResource): } + +@resource('/v1/repository//trigger//fields/') +@internal_only +class BuildTriggerFieldValues(RepositoryParamResource): + """ Custom verb to fetch a values list for a particular field name. """ + @require_repo_admin + @nickname('listTriggerFieldValues') + def get(self, namespace, repository, trigger_uuid, field_name): + """ List the field values for a custom run field. """ + try: + trigger = model.get_build_trigger(namespace, repository, trigger_uuid) + except model.InvalidBuildTriggerException: + raise NotFound() + + user_permission = UserAdminPermission(trigger.connected_user.username) + if user_permission.can(): + trigger_handler = BuildTriggerBase.get_trigger_for_service(trigger.service.name) + values = trigger_handler.list_field_values(trigger.auth_token, json.loads(trigger.config), + field_name) + + if values is None: + raise NotFound() + + return { + 'values': values + } + else: + raise Unauthorized() + + @resource('/v1/repository//trigger//sources') @internal_only class BuildTriggerSources(RepositoryParamResource): diff --git a/endpoints/trigger.py b/endpoints/trigger.py index ae0b4b2b7..4a10485ae 100644 --- a/endpoints/trigger.py +++ b/endpoints/trigger.py @@ -36,6 +36,9 @@ class TriggerActivationException(Exception): class TriggerDeactivationException(Exception): pass +class TriggerStartException(Exception): + pass + class ValidationRequestException(Exception): pass @@ -109,12 +112,19 @@ class BuildTrigger(object): """ raise NotImplementedError - def manual_start(self, auth_token, config): + def manual_start(self, auth_token, config, run_parameters = None): """ Manually creates a repository build for this trigger. """ raise NotImplementedError + def list_field_values(self, auth_token, config, field_name): + """ + Lists all values for the given custom trigger field. For example, a trigger might have a + field named "branches", and this method would return all branches. + """ + raise NotImplementedError + @classmethod def service_name(cls): """ @@ -345,14 +355,37 @@ class GithubBuildTrigger(BuildTrigger): return GithubBuildTrigger._prepare_build(config, repo, commit_sha, short_sha, ref) - def manual_start(self, auth_token, config): - source = config['build_source'] + def manual_start(self, auth_token, config, run_parameters = None): + try: + source = config['build_source'] + run_parameters = run_parameters or {} - gh_client = self._get_client(auth_token) - repo = gh_client.get_repo(source) - master = repo.get_branch(repo.default_branch) - master_sha = master.commit.sha - short_sha = GithubBuildTrigger.get_display_name(master_sha) - ref = 'refs/heads/%s' % repo.default_branch + gh_client = self._get_client(auth_token) + repo = gh_client.get_repo(source) + master = repo.get_branch(repo.default_branch) + master_sha = master.commit.sha + short_sha = GithubBuildTrigger.get_display_name(master_sha) + ref = 'refs/heads/%s' % (run_parameters.get('branch_name') or repo.default_branch) - return self._prepare_build(config, repo, master_sha, short_sha, ref) + return self._prepare_build(config, repo, master_sha, short_sha, ref) + except GithubException as ghe: + raise TriggerStartException(ghe.data['message']) + + + def list_field_values(self, auth_token, config, field_name): + if field_name == 'branch_name': + gh_client = self._get_client(auth_token) + source = config['build_source'] + repo = gh_client.get_repo(source) + branches = [branch['name'] for branch in repo.get_branches()] + + if not repo.default_branch in branches: + branches.insert(0, repo.default_branch) + + if branches[0] != repo.default_branch: + branches.remove(repo.default_branch) + branches.insert(0, repo.default_branch) + + return branches + + return None diff --git a/initdb.py b/initdb.py index cfb67a39a..6be72f6ff 100644 --- a/initdb.py +++ b/initdb.py @@ -80,7 +80,7 @@ def __create_subtree(repo, structure, creator_username, parent): command_list = SAMPLE_CMDS[image_num % len(SAMPLE_CMDS)] command = json.dumps(command_list) if command_list else None new_image = model.set_image_metadata(docker_image_id, repo.namespace_user.username, repo.name, - str(creation_time), 'no comment', command, 0, parent) + str(creation_time), 'no comment', command, parent) model.set_image_size(docker_image_id, repo.namespace_user.username, repo.name, random.randrange(1, 1024 * 1024 * 1024)) diff --git a/static/css/quay.css b/static/css/quay.css index 78053c1e8..7e40e18b1 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -4705,4 +4705,15 @@ i.slack-icon { .team-view .organization-header .popover-content { min-width: 500px; +} + +#startTriggerDialog .trigger-description { + margin-bottom: 20px; + padding-bottom: 20px; + border-bottom: 1px solid #eee; +} + +#startTriggerDialog #runForm .field-title { + width: 120px; + padding-right: 10px; } \ No newline at end of file diff --git a/static/directives/manual-trigger-build-dialog.html b/static/directives/manual-trigger-build-dialog.html new file mode 100644 index 000000000..e82879e8a --- /dev/null +++ b/static/directives/manual-trigger-build-dialog.html @@ -0,0 +1,38 @@ + + \ No newline at end of file diff --git a/static/js/app.js b/static/js/app.js index af5f47bb5..186571e5e 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -620,24 +620,46 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading }]); - $provide.factory('TriggerDescriptionBuilder', ['UtilService', '$sanitize', function(UtilService, $sanitize) { - var builderService = {}; + $provide.factory('TriggerService', ['UtilService', '$sanitize', function(UtilService, $sanitize) { + var triggerService = {}; - builderService.getDescription = function(name, config) { - switch (name) { - case 'github': + var triggerTypes = { + 'github': { + 'description': function(config) { var source = UtilService.textToSafeHtml(config['build_source']); var desc = ' Push to Github Repository '; desc += '' + source + ''; desc += '
Dockerfile folder: //' + UtilService.textToSafeHtml(config['subdir']); return desc; + }, - default: - return 'Unknown'; + 'run_parameters': [ + { + 'title': 'Branch', + 'type': 'option', + 'name': 'branch_name' + } + ] } + } + + triggerService.getDescription = function(name, config) { + var type = triggerTypes[name]; + if (!type) { + return 'Unknown'; + } + return type['description'](config); }; - return builderService; + triggerService.getRunParameters = function(name, config) { + var type = triggerTypes[name]; + if (!type) { + return []; + } + return type['run_parameters']; + } + + return triggerService; }]); $provide.factory('StringBuilderService', ['$sce', 'UtilService', function($sce, UtilService) { @@ -3053,7 +3075,7 @@ quayApp.directive('logsView', function () { 'repository': '=repository', 'performer': '=performer' }, - controller: function($scope, $element, $sce, Restangular, ApiService, TriggerDescriptionBuilder, + controller: function($scope, $element, $sce, Restangular, ApiService, TriggerService, StringBuilderService, ExternalNotificationData) { $scope.loading = true; $scope.logs = null; @@ -3118,7 +3140,7 @@ quayApp.directive('logsView', function () { 'set_repo_description': 'Change description for repository {repo}: {description}', 'build_dockerfile': function(metadata) { if (metadata.trigger_id) { - var triggerDescription = TriggerDescriptionBuilder.getDescription( + var triggerDescription = TriggerService.getDescription( metadata['service'], metadata['config']); return 'Build image from Dockerfile for repository {repo} triggered by ' + triggerDescription; } @@ -3170,12 +3192,12 @@ quayApp.directive('logsView', function () { } }, 'setup_repo_trigger': function(metadata) { - var triggerDescription = TriggerDescriptionBuilder.getDescription( + var triggerDescription = TriggerService.getDescription( metadata['service'], metadata['config']); return 'Setup build trigger - ' + triggerDescription; }, 'delete_repo_trigger': function(metadata) { - var triggerDescription = TriggerDescriptionBuilder.getDescription( + var triggerDescription = TriggerService.getDescription( metadata['service'], metadata['config']); return 'Delete build trigger - ' + triggerDescription; }, @@ -4873,6 +4895,66 @@ quayApp.directive('dropdownSelectMenu', function () { }); +quayApp.directive('manualTriggerBuildDialog', function () { + var directiveDefinitionObject = { + templateUrl: '/static/directives/manual-trigger-build-dialog.html', + replace: false, + transclude: false, + restrict: 'C', + scope: { + 'repository': '=repository', + 'counter': '=counter', + 'trigger': '=trigger', + 'startBuild': '&startBuild' + }, + controller: function($scope, $element, ApiService, TriggerService) { + $scope.parameters = {}; + $scope.fieldOptions = {}; + + $scope.startTrigger = function() { + $('#startTriggerDialog').modal('hide'); + $scope.startBuild({ + 'trigger': $scope.trigger, + 'parameters': $scope.parameters + }); + }; + + $scope.show = function() { + $scope.parameters = {}; + $scope.fieldOptions = {}; + + var parameters = TriggerService.getRunParameters($scope.trigger.service); + for (var i = 0; i < parameters.length; ++i) { + var parameter = parameters[i]; + if (parameter['type'] == 'option') { + // Load the values for this parameter. + var params = { + 'repository': $scope.repository.namespace + '/' + $scope.repository.name, + 'trigger_uuid': $scope.trigger.id, + 'field_name': parameter['name'] + }; + + ApiService.listTriggerFieldValues(null, params).then(function(resp) { + $scope.fieldOptions[parameter['name']] = resp['values']; + }); + } + } + $scope.runParameters = parameters; + + $('#startTriggerDialog').modal('show'); + }; + + $scope.$watch('counter', function(counter) { + if (counter) { + $scope.show(); + } + }); + } + }; + return directiveDefinitionObject; +}); + + quayApp.directive('setupTriggerDialog', function () { var directiveDefinitionObject = { templateUrl: '/static/directives/setup-trigger-dialog.html', diff --git a/static/js/controllers.js b/static/js/controllers.js index 07b73fa97..a3ed8890e 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -1234,7 +1234,9 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope fetchRepository(); } -function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams, $rootScope, $location, UserService, Config, Features, ExternalNotificationData) { +function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, TriggerService, $routeParams, + $rootScope, $location, UserService, Config, Features, ExternalNotificationData) { + var namespace = $routeParams.namespace; var name = $routeParams.name; @@ -1539,14 +1541,22 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams $scope.deleteTrigger(trigger); }; - $scope.startTrigger = function(trigger) { + $scope.showManualBuildDialog = 0; + + $scope.startTrigger = function(trigger, opt_custom) { + var parameters = TriggerService.getRunParameters(trigger.service); + if (parameters.length && !opt_custom) { + $scope.currentStartTrigger = trigger; + $scope.showManualBuildDialog++; + return; + } + var params = { 'repository': namespace + '/' + name, 'trigger_uuid': trigger.id }; - ApiService.manuallyStartBuildTrigger(null, params).then(function(resp) { - window.console.log(resp); + ApiService.manuallyStartBuildTrigger(opt_custom || {}, params).then(function(resp) { var url = '/repository/' + namespace + '/' + name + '/build?current=' + resp['id']; document.location = url; }, ApiService.errorDisplay('Could not start build')); diff --git a/static/partials/repo-admin.html b/static/partials/repo-admin.html index 6bd329091..f22431232 100644 --- a/static/partials/repo-admin.html +++ b/static/partials/repo-admin.html @@ -19,7 +19,7 @@