- Fix initdb

- Add ability to specific custom fields for manual running of build triggers and add a "branch name" selector for running github builds
This commit is contained in:
Joseph Schorr 2014-09-30 16:29:32 -04:00
parent 6c520b8b0b
commit 039d53ea6c
10 changed files with 346 additions and 41 deletions

View file

@ -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/<repopath:repository>/trigger/<trigger_uuid>/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,7 +404,9 @@ 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)
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)
@ -397,6 +414,8 @@ class ActivateBuildTrigger(RepositoryParamResource):
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/<repopath:repository>/trigger/<trigger_uuid>/fields/<field_name>')
@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/<repopath:repository>/trigger/<trigger_uuid>/sources')
@internal_only
class BuildTriggerSources(RepositoryParamResource):

View file

@ -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):
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
ref = 'refs/heads/%s' % (run_parameters.get('branch_name') or repo.default_branch)
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

View file

@ -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))

View file

@ -4706,3 +4706,14 @@ 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;
}

View file

@ -0,0 +1,38 @@
<!-- Modal message dialog -->
<div class="modal fade" id="startTriggerDialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Manully Start Build Trigger</h4>
</div>
<div class="modal-body">
<div class="trigger-description" trigger="trigger"></div>
<form name="runForm" id="runForm">
<table width="100%">
<tr ng-repeat="field in runParameters">
<td class="field-title" valign="top">{{ field.title }}:</td>
<td>
<div ng-switch on="field.type">
<span ng-switch-when="option">
<span class="quay-spinner" ng-show="!fieldOptions[field.name]"></span>
<select ng-model="parameters[field.name]" ng-show="fieldOptions[field.name]"
ng-options="value for value in fieldOptions[field.name]"
required>
</select>
</span>
<input type="text" class="form-control" ng-model="parameters[field.name]" ng-switch-when="string" required>
</div>
</td>
</tr>
</table>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" ng-disabled="runForm.$invalid" ng-click="startTrigger()">Start Build</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->

View file

@ -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 = '<i class="fa fa-github fa-lg" style="margin-left: 2px; margin-right: 2px"></i> Push to Github Repository ';
desc += '<a href="https://github.com/' + source + '" target="_blank">' + source + '</a>';
desc += '<br>Dockerfile folder: //' + UtilService.textToSafeHtml(config['subdir']);
return desc;
},
default:
'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',

View file

@ -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'));

View file

@ -19,7 +19,7 @@
<ul class="nav nav-pills nav-stacked">
<li class="active"><a href="javascript:void(0)" data-toggle="tab" data-target="#permissions">Permissions</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#trigger" ng-click="loadTriggers()"
quay-require="['BUILD_SUPPORT']">Build Triggers</a></li>
quay-show="Features.BUILD_SUPPORT">Build Triggers</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#badge">Status Badge</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#notification" ng-click="loadNotifications()">Notifications</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#publicprivate">Public/Private</a></li>
@ -226,7 +226,7 @@
</div>
<!-- Triggers tab -->
<div id="trigger" class="tab-pane" quay-require="['BUILD_SUPPORT']">
<div id="trigger" class="tab-pane" quay-show="['BUILD_SUPPORT']">
<div class="panel panel-default">
<div class="panel-heading">Build Triggers
<i class="info-icon fa fa-info-circle" data-placement="left" data-content="Triggers from various services (such as GitHub) which tell the repository to be built and updated."></i>
@ -378,6 +378,12 @@
counter="showNewNotificationCounter"
notification-created="handleNotificationCreated(notification)"></div>
<!-- Manual trigger dialog -->
<div class="manual-trigger-build-dialog" repository="repo"
trigger="currentStartTrigger"
counter="showManualBuildDialog"
start-build="startTrigger(trigger, parameters)"></div>
<!-- Modal message dialog -->
<div class="modal fade" id="makepublicModal">
<div class="modal-dialog">

View file

@ -20,7 +20,7 @@ from endpoints.api.robot import (UserRobotList, OrgRobot, OrgRobotList, UserRobo
from endpoints.api.trigger import (BuildTriggerActivate, BuildTriggerSources, BuildTriggerSubdirs,
TriggerBuildList, ActivateBuildTrigger, BuildTrigger,
BuildTriggerList, BuildTriggerAnalyze)
BuildTriggerList, BuildTriggerAnalyze, BuildTriggerFieldValues)
from endpoints.api.repoemail import RepositoryAuthorizedEmail
from endpoints.api.repositorynotification import RepositoryNotification, RepositoryNotificationList
from endpoints.api.user import (PrivateRepositories, ConvertToOrganization, Recovery, Signout,
@ -1063,6 +1063,62 @@ class TestBuildTriggerActivateSwo1BuynlargeOrgrepo(ApiTestCase):
def test_post_devtable(self):
self._run_test('POST', 404, 'devtable', {'config': {}})
class TestBuildTriggerFieldValuesSwo1PublicPublicrepo(ApiTestCase):
def setUp(self):
ApiTestCase.setUp(self)
self._set_url(BuildTriggerFieldValues, trigger_uuid="SWO1", repository="public/publicrepo",
field_name="test_field")
def test_get_anonymous(self):
self._run_test('GET', 401, None, {})
def test_get_freshuser(self):
self._run_test('GET', 403, 'freshuser', {})
def test_get_reader(self):
self._run_test('GET', 403, 'reader', {})
def test_get_devtable(self):
self._run_test('GET', 403, 'devtable', {})
class TestBuildTriggerFieldValuesSwo1DevtableShared(ApiTestCase):
def setUp(self):
ApiTestCase.setUp(self)
self._set_url(BuildTriggerFieldValues, trigger_uuid="SWO1", repository="devtable/shared",
field_name="test_field")
def test_get_anonymous(self):
self._run_test('GET', 401, None, {})
def test_get_freshuser(self):
self._run_test('GET', 403, 'freshuser', {})
def test_get_reader(self):
self._run_test('GET', 403, 'reader', {})
def test_get_devtable(self):
self._run_test('GET', 404, 'devtable', {'config': {}})
class TestBuildTriggerFieldValuesSwo1BuynlargeOrgrepo(ApiTestCase):
def setUp(self):
ApiTestCase.setUp(self)
self._set_url(BuildTriggerFieldValues, trigger_uuid="SWO1", repository="buynlarge/orgrepo",
field_name="test_field")
def test_get_anonymous(self):
self._run_test('GET', 401, None, {})
def test_get_freshuser(self):
self._run_test('GET', 403, 'freshuser', {})
def test_get_reader(self):
self._run_test('GET', 403, 'reader', {})
def test_get_devtable(self):
self._run_test('GET', 404, 'devtable', {'config': {}})
class TestBuildTriggerSources831cPublicPublicrepo(ApiTestCase):
def setUp(self):
@ -1294,7 +1350,7 @@ class TestActivateBuildTrigger0byeDevtableShared(ApiTestCase):
self._run_test('POST', 403, 'reader', None)
def test_post_devtable(self):
self._run_test('POST', 404, 'devtable', None)
self._run_test('POST', 404, 'devtable', {})
class TestActivateBuildTrigger0byeBuynlargeOrgrepo(ApiTestCase):
@ -1312,7 +1368,7 @@ class TestActivateBuildTrigger0byeBuynlargeOrgrepo(ApiTestCase):
self._run_test('POST', 403, 'reader', None)
def test_post_devtable(self):
self._run_test('POST', 404, 'devtable', None)
self._run_test('POST', 404, 'devtable', {})
class TestBuildTriggerAnalyze0byePublicPublicrepo(ApiTestCase):

View file

@ -22,7 +22,7 @@ from endpoints.api.robot import (UserRobotList, OrgRobot, OrgRobotList, UserRobo
RegenerateUserRobot, RegenerateOrgRobot)
from endpoints.api.trigger import (BuildTriggerActivate, BuildTriggerSources, BuildTriggerSubdirs,
TriggerBuildList, ActivateBuildTrigger, BuildTrigger,
BuildTriggerList, BuildTriggerAnalyze)
BuildTriggerList, BuildTriggerAnalyze, BuildTriggerFieldValues)
from endpoints.api.repoemail import RepositoryAuthorizedEmail
from endpoints.api.repositorynotification import RepositoryNotification, RepositoryNotificationList
from endpoints.api.user import (PrivateRepositories, ConvertToOrganization, Signout, Signin, User,
@ -2034,7 +2034,7 @@ class FakeBuildTrigger(BuildTriggerBase):
config['active'] = False
return config
def manual_start(self, auth_token, config):
def manual_start(self, auth_token, config, run_parameters=None):
return ('foo', ['bar'], 'build-name', 'subdir')
def dockerfile_url(self, auth_token, config):
@ -2046,6 +2046,12 @@ class FakeBuildTrigger(BuildTriggerBase):
return config['dockerfile']
def list_field_values(self, auth_token, config, field_name):
if field_name == 'test_field':
return [1, 2, 3]
return None
class TestBuildTriggers(ApiTestCase):
def test_list_build_triggers(self):
@ -2218,9 +2224,22 @@ class TestBuildTriggers(ApiTestCase):
data={'config': trigger_config},
expected_code=400)
# Retrieve values for a field.
result = self.getJsonResponse(BuildTriggerFieldValues,
params=dict(repository=ADMIN_ACCESS_USER + '/simple',
trigger_uuid=trigger.uuid, field_name="test_field"))
self.assertEquals(result['values'], [1, 2, 3])
self.getResponse(BuildTriggerFieldValues,
params=dict(repository=ADMIN_ACCESS_USER + '/simple',
trigger_uuid=trigger.uuid, field_name="another_field"),
expected_code = 404)
# Start a manual build.
start_json = self.postJsonResponse(ActivateBuildTrigger,
params=dict(repository=ADMIN_ACCESS_USER + '/simple', trigger_uuid=trigger.uuid),
data=dict(),
expected_code=201)
assert 'id' in start_json
@ -2285,6 +2304,7 @@ class TestBuildTriggers(ApiTestCase):
# Start a manual build.
start_json = self.postJsonResponse(ActivateBuildTrigger,
params=dict(repository=ADMIN_ACCESS_USER + '/simple', trigger_uuid=trigger.uuid),
data=dict(),
expected_code=201)
assert 'id' in start_json