From 81ce4c771e7a71ec61d79f8b735133aca60e3d95 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Fri, 13 Feb 2015 15:54:01 -0500 Subject: [PATCH] Add ability to cancel builds that are in the waiting state --- data/database.py | 3 +- data/model/legacy.py | 28 ++- endpoints/api/build.py | 27 ++- static/js/controllers.js | 251 ------------------------- static/js/controllers/repo-build.js | 272 ++++++++++++++++++++++++++++ static/partials/repo-build.html | 9 +- test/test_api_security.py | 56 +++++- test/test_api_usage.py | 100 +++++++++- 8 files changed, 489 insertions(+), 257 deletions(-) create mode 100644 static/js/controllers/repo-build.js diff --git a/data/database.py b/data/database.py index 269b7bf67..359072268 100644 --- a/data/database.py +++ b/data/database.py @@ -473,6 +473,7 @@ class BUILD_PHASE(object): PULLING = 'pulling' BUILDING = 'building' PUSHING = 'pushing' + WAITING = 'waiting' COMPLETE = 'complete' @@ -491,7 +492,7 @@ class RepositoryBuild(BaseModel): access_token = ForeignKeyField(AccessToken) resource_key = CharField(index=True) job_config = TextField() - phase = CharField(default='waiting') + phase = CharField(default=BUILD_PHASE.WAITING) started = DateTimeField(default=datetime.now) display_name = CharField() trigger = ForeignKeyField(RepositoryBuildTrigger, null=True, index=True) diff --git a/data/model/legacy.py b/data/model/legacy.py index ff1a5ba59..deac1f02b 100644 --- a/data/model/legacy.py +++ b/data/model/legacy.py @@ -14,7 +14,7 @@ from data.database import (User, Repository, Image, AccessToken, Role, Repositor ExternalNotificationEvent, ExternalNotificationMethod, RepositoryNotification, RepositoryAuthorizedEmail, TeamMemberInvite, DerivedImageStorage, ImageStorageTransformation, random_string_generator, - db, BUILD_PHASE, QuayUserField, ImageStorageSignature, + db, BUILD_PHASE, QuayUserField, ImageStorageSignature, QueueItem, ImageStorageSignatureKind, validate_database_url, db_for_update) from peewee import JOIN_LEFT_OUTER, fn from util.validation import (validate_username, validate_email, validate_password, @@ -2431,6 +2431,32 @@ def confirm_team_invite(code, user): found.delete_instance() return (team, inviter) +def cancel_repository_build(build): + with config.app_config['DB_TRANSACTION_FACTORY'](db): + # Reload the build for update. + try: + build = db_for_update(RepositoryBuild.select().where(RepositoryBuild.id == build.id)).get() + except RepositoryBuild.DoesNotExist: + return False + + if build.phase != BUILD_PHASE.WAITING or not build.queue_item: + return False + + # Load the build queue item for update. + try: + queue_item = db_for_update(QueueItem.select() + .where(QueueItem.id == build.queue_item.id)).get() + except QueueItem.DoesNotExist: + return False + + # Check the queue item. + if not queue_item.available or queue_item.retries_remaining == 0: + return False + + # Delete the queue item and build. + queue_item.delete_instance(recursive=True) + build.delete_instance() + return True def get_repository_usage(): one_month_ago = date.today() - timedelta(weeks=4) diff --git a/endpoints/api/build.py b/endpoints/api/build.py index 604c7258d..476c9ef72 100644 --- a/endpoints/api/build.py +++ b/endpoints/api/build.py @@ -9,7 +9,7 @@ from app import app, userfiles as user_files, build_logs, log_archive from endpoints.api import (RepositoryParamResource, parse_args, query_param, nickname, resource, require_repo_read, require_repo_write, validate_json_request, ApiResource, internal_only, format_date, api, Unauthorized, NotFound, - path_param) + path_param, InvalidRequest, require_repo_admin) from endpoints.common import start_build from endpoints.trigger import BuildTrigger from data import model, database @@ -207,6 +207,31 @@ class RepositoryBuildList(RepositoryParamResource): return resp, 201, headers + + +@resource('/v1/repository//build/') +@path_param('repository', 'The full path of the repository. e.g. namespace/name') +@path_param('build_uuid', 'The UUID of the build') +class RepositoryBuildResource(RepositoryParamResource): + """ Resource for dealing with repository builds. """ + @require_repo_admin + @nickname('cancelRepoBuild') + def delete(self, namespace, repository, build_uuid): + """ Cancels a repository build if it has not yet been picked up by a build worker. """ + try: + build = model.get_repository_build(build_uuid) + except model.InvalidRepositoryBuildException: + raise NotFound() + + if build.repository.name != repository or build.repository.namespace_user.username != namespace: + raise NotFound() + + if model.cancel_repository_build(build): + return 'Okay', 201 + else: + raise InvalidRequest('Build is currently running or has finished') + + @resource('/v1/repository//build//status') @path_param('repository', 'The full path of the repository. e.g. namespace/name') @path_param('build_uuid', 'The UUID of the build') diff --git a/static/js/controllers.js b/static/js/controllers.js index d78ef4bd5..61c624fce 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -1072,257 +1072,6 @@ function BuildPackageCtrl($scope, Restangular, ApiService, DataFileService, $rou getBuildInfo(); } -function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $location, $interval, $sanitize, - ansi2html, AngularViewArray, AngularPollChannel) { - var namespace = $routeParams.namespace; - var name = $routeParams.name; - - // Watch for changes to the current parameter. - $scope.$on('$routeUpdate', function(){ - if ($location.search().current) { - $scope.setCurrentBuild($location.search().current, false); - } - }); - - $scope.builds = null; - $scope.pollChannel = null; - $scope.buildDialogShowCounter = 0; - - $scope.showNewBuildDialog = function() { - $scope.buildDialogShowCounter++; - }; - - $scope.handleBuildStarted = function(newBuild) { - if (!$scope.builds) { return; } - - $scope.builds.unshift(newBuild); - $scope.setCurrentBuild(newBuild['id'], true); - }; - - $scope.adjustLogHeight = function() { - var triggerOffset = 0; - if ($scope.currentBuild && $scope.currentBuild.trigger) { - triggerOffset = 85; - } - $('.build-logs').height($(window).height() - 415 - triggerOffset); - }; - - $scope.askRestartBuild = function(build) { - $('#confirmRestartBuildModal').modal({}); - }; - - $scope.restartBuild = function(build) { - $('#confirmRestartBuildModal').modal('hide'); - - var subdirectory = ''; - if (build['job_config']) { - subdirectory = build['job_config']['build_subdir'] || ''; - } - - var data = { - 'file_id': build['resource_key'], - 'subdirectory': subdirectory, - 'docker_tags': build['job_config']['docker_tags'] - }; - - if (build['pull_robot']) { - data['pull_robot'] = build['pull_robot']['name']; - } - - var params = { - 'repository': namespace + '/' + name - }; - - ApiService.requestRepoBuild(data, params).then(function(newBuild) { - if (!$scope.builds) { return; } - - $scope.builds.unshift(newBuild); - $scope.setCurrentBuild(newBuild['id'], true); - }); - }; - - $scope.hasLogs = function(container) { - return container.logs.hasEntries; - }; - - $scope.setCurrentBuild = function(buildId, opt_updateURL) { - if (!$scope.builds) { return; } - - // Find the build. - for (var i = 0; i < $scope.builds.length; ++i) { - if ($scope.builds[i].id == buildId) { - $scope.setCurrentBuildInternal(i, $scope.builds[i], opt_updateURL); - return; - } - } - }; - - $scope.processANSI = function(message, container) { - var filter = container.logs._filter = (container.logs._filter || ansi2html.create()); - - // Note: order is important here. - var setup = filter.getSetupHtml(); - var stream = filter.addInputToStream(message); - var teardown = filter.getTeardownHtml(); - return setup + stream + teardown; - }; - - $scope.setCurrentBuildInternal = function(index, build, opt_updateURL) { - if (build == $scope.currentBuild) { return; } - - $scope.logEntries = null; - $scope.logStartIndex = null; - $scope.currentParentEntry = null; - - $scope.currentBuild = build; - - if (opt_updateURL) { - if (build) { - $location.search('current', build.id); - } else { - $location.search('current', null); - } - } - - // Timeout needed to ensure the log element has been created - // before its height is adjusted. - setTimeout(function() { - $scope.adjustLogHeight(); - }, 1); - - // Stop any existing polling. - if ($scope.pollChannel) { - $scope.pollChannel.stop(); - } - - // Create a new channel for polling the build status and logs. - var conductStatusAndLogRequest = function(callback) { - getBuildStatusAndLogs(build, callback); - }; - - $scope.pollChannel = AngularPollChannel.create($scope, conductStatusAndLogRequest, 5 * 1000 /* 5s */); - $scope.pollChannel.start(); - }; - - var processLogs = function(logs, startIndex, endIndex) { - if (!$scope.logEntries) { $scope.logEntries = []; } - - // If the start index given is less than that requested, then we've received a larger - // pool of logs, and we need to only consider the new ones. - if (startIndex < $scope.logStartIndex) { - logs = logs.slice($scope.logStartIndex - startIndex); - } - - for (var i = 0; i < logs.length; ++i) { - var entry = logs[i]; - var type = entry['type'] || 'entry'; - if (type == 'command' || type == 'phase' || type == 'error') { - entry['logs'] = AngularViewArray.create(); - entry['index'] = $scope.logStartIndex + i; - - $scope.logEntries.push(entry); - $scope.currentParentEntry = entry; - } else if ($scope.currentParentEntry) { - $scope.currentParentEntry['logs'].push(entry); - } - } - - return endIndex; - }; - - var getBuildStatusAndLogs = function(build, callback) { - var params = { - 'repository': namespace + '/' + name, - 'build_uuid': build.id - }; - - ApiService.getRepoBuildStatus(null, params, true).then(function(resp) { - if (build != $scope.currentBuild) { callback(false); return; } - - // Note: We use extend here rather than replacing as Angular is depending on the - // root build object to remain the same object. - var matchingBuilds = $.grep($scope.builds, function(elem) { - return elem['id'] == resp['id'] - }); - - var currentBuild = matchingBuilds.length > 0 ? matchingBuilds[0] : null; - if (currentBuild) { - currentBuild = $.extend(true, currentBuild, resp); - } else { - currentBuild = resp; - $scope.builds.push(currentBuild); - } - - // Load the updated logs for the build. - var options = { - 'start': $scope.logStartIndex - }; - - ApiService.getRepoBuildLogsAsResource(params, true).withOptions(options).get(function(resp) { - if (build != $scope.currentBuild) { callback(false); return; } - - // Process the logs we've received. - $scope.logStartIndex = processLogs(resp['logs'], resp['start'], resp['total']); - - // If the build status is an error, open the last two log entries. - if (currentBuild['phase'] == 'error' && $scope.logEntries.length > 1) { - var openLogEntries = function(entry) { - if (entry.logs) { - entry.logs.setVisible(true); - } - }; - - openLogEntries($scope.logEntries[$scope.logEntries.length - 2]); - openLogEntries($scope.logEntries[$scope.logEntries.length - 1]); - } - - // If the build phase is an error or a complete, then we mark the channel - // as closed. - callback(currentBuild['phase'] != 'error' && currentBuild['phase'] != 'complete'); - }, function() { - callback(false); - }); - }, function() { - callback(false); - }); - }; - - var fetchRepository = function() { - var params = {'repository': namespace + '/' + name}; - $rootScope.title = 'Loading Repository...'; - $scope.repository = ApiService.getRepoAsResource(params).get(function(repo) { - if (!repo.can_write) { - $rootScope.title = 'Unknown builds'; - $scope.accessDenied = true; - return; - } - - $rootScope.title = 'Repository Builds'; - $scope.repo = repo; - - getBuildInfo(); - }); - }; - - var getBuildInfo = function(repo) { - var params = { - 'repository': namespace + '/' + name - }; - - ApiService.getRepoBuilds(null, params).then(function(resp) { - $scope.builds = resp.builds; - - if ($location.search().current) { - $scope.setCurrentBuild($location.search().current, false); - } else if ($scope.builds.length > 0) { - $scope.setCurrentBuild($scope.builds[0].id, true); - } - }); - }; - - fetchRepository(); -} - function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, TriggerService, $routeParams, $rootScope, $location, UserService, Config, Features, ExternalNotificationData) { diff --git a/static/js/controllers/repo-build.js b/static/js/controllers/repo-build.js new file mode 100644 index 000000000..887efb55b --- /dev/null +++ b/static/js/controllers/repo-build.js @@ -0,0 +1,272 @@ +function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $location, $interval, $sanitize, + ansi2html, AngularViewArray, AngularPollChannel) { + var namespace = $routeParams.namespace; + var name = $routeParams.name; + + // Watch for changes to the current parameter. + $scope.$on('$routeUpdate', function(){ + if ($location.search().current) { + $scope.setCurrentBuild($location.search().current, false); + } + }); + + $scope.builds = null; + $scope.pollChannel = null; + $scope.buildDialogShowCounter = 0; + + $scope.showNewBuildDialog = function() { + $scope.buildDialogShowCounter++; + }; + + $scope.handleBuildStarted = function(newBuild) { + if (!$scope.builds) { return; } + + $scope.builds.unshift(newBuild); + $scope.setCurrentBuild(newBuild['id'], true); + }; + + $scope.adjustLogHeight = function() { + var triggerOffset = 0; + if ($scope.currentBuild && $scope.currentBuild.trigger) { + triggerOffset = 85; + } + $('.build-logs').height($(window).height() - 415 - triggerOffset); + }; + + $scope.askRestartBuild = function(build) { + $('#confirmRestartBuildModal').modal({}); + }; + + $scope.askCancelBuild = function(build) { + bootbox.confirm('Are you sure you want to cancel this build?', function(r) { + if (r) { + var params = { + 'repository': namespace + '/' + name, + 'build_uuid': build.id + }; + + ApiService.cancelRepoBuild(null, params).then(function() { + if (!$scope.builds) { return; } + $scope.builds.splice($.inArray(build, $scope.builds), 1); + + if ($scope.builds.length) { + $scope.currentBuild = $scope.builds[0]; + } else { + $scope.currentBuild = null; + } + }, ApiService.errorDisplay('Cannot cancel build')); + } + }); + }; + + $scope.restartBuild = function(build) { + $('#confirmRestartBuildModal').modal('hide'); + + var subdirectory = ''; + if (build['job_config']) { + subdirectory = build['job_config']['build_subdir'] || ''; + } + + var data = { + 'file_id': build['resource_key'], + 'subdirectory': subdirectory, + 'docker_tags': build['job_config']['docker_tags'] + }; + + if (build['pull_robot']) { + data['pull_robot'] = build['pull_robot']['name']; + } + + var params = { + 'repository': namespace + '/' + name + }; + + ApiService.requestRepoBuild(data, params).then(function(newBuild) { + if (!$scope.builds) { return; } + + $scope.builds.unshift(newBuild); + $scope.setCurrentBuild(newBuild['id'], true); + }); + }; + + $scope.hasLogs = function(container) { + return container.logs.hasEntries; + }; + + $scope.setCurrentBuild = function(buildId, opt_updateURL) { + if (!$scope.builds) { return; } + + // Find the build. + for (var i = 0; i < $scope.builds.length; ++i) { + if ($scope.builds[i].id == buildId) { + $scope.setCurrentBuildInternal(i, $scope.builds[i], opt_updateURL); + return; + } + } + }; + + $scope.processANSI = function(message, container) { + var filter = container.logs._filter = (container.logs._filter || ansi2html.create()); + + // Note: order is important here. + var setup = filter.getSetupHtml(); + var stream = filter.addInputToStream(message); + var teardown = filter.getTeardownHtml(); + return setup + stream + teardown; + }; + + $scope.setCurrentBuildInternal = function(index, build, opt_updateURL) { + if (build == $scope.currentBuild) { return; } + + $scope.logEntries = null; + $scope.logStartIndex = null; + $scope.currentParentEntry = null; + + $scope.currentBuild = build; + + if (opt_updateURL) { + if (build) { + $location.search('current', build.id); + } else { + $location.search('current', null); + } + } + + // Timeout needed to ensure the log element has been created + // before its height is adjusted. + setTimeout(function() { + $scope.adjustLogHeight(); + }, 1); + + // Stop any existing polling. + if ($scope.pollChannel) { + $scope.pollChannel.stop(); + } + + // Create a new channel for polling the build status and logs. + var conductStatusAndLogRequest = function(callback) { + getBuildStatusAndLogs(build, callback); + }; + + $scope.pollChannel = AngularPollChannel.create($scope, conductStatusAndLogRequest, 5 * 1000 /* 5s */); + $scope.pollChannel.start(); + }; + + var processLogs = function(logs, startIndex, endIndex) { + if (!$scope.logEntries) { $scope.logEntries = []; } + + // If the start index given is less than that requested, then we've received a larger + // pool of logs, and we need to only consider the new ones. + if (startIndex < $scope.logStartIndex) { + logs = logs.slice($scope.logStartIndex - startIndex); + } + + for (var i = 0; i < logs.length; ++i) { + var entry = logs[i]; + var type = entry['type'] || 'entry'; + if (type == 'command' || type == 'phase' || type == 'error') { + entry['logs'] = AngularViewArray.create(); + entry['index'] = $scope.logStartIndex + i; + + $scope.logEntries.push(entry); + $scope.currentParentEntry = entry; + } else if ($scope.currentParentEntry) { + $scope.currentParentEntry['logs'].push(entry); + } + } + + return endIndex; + }; + + var getBuildStatusAndLogs = function(build, callback) { + var params = { + 'repository': namespace + '/' + name, + 'build_uuid': build.id + }; + + ApiService.getRepoBuildStatus(null, params, true).then(function(resp) { + if (build != $scope.currentBuild) { callback(false); return; } + + // Note: We use extend here rather than replacing as Angular is depending on the + // root build object to remain the same object. + var matchingBuilds = $.grep($scope.builds, function(elem) { + return elem['id'] == resp['id'] + }); + + var currentBuild = matchingBuilds.length > 0 ? matchingBuilds[0] : null; + if (currentBuild) { + currentBuild = $.extend(true, currentBuild, resp); + } else { + currentBuild = resp; + $scope.builds.push(currentBuild); + } + + // Load the updated logs for the build. + var options = { + 'start': $scope.logStartIndex + }; + + ApiService.getRepoBuildLogsAsResource(params, true).withOptions(options).get(function(resp) { + if (build != $scope.currentBuild) { callback(false); return; } + + // Process the logs we've received. + $scope.logStartIndex = processLogs(resp['logs'], resp['start'], resp['total']); + + // If the build status is an error, open the last two log entries. + if (currentBuild['phase'] == 'error' && $scope.logEntries.length > 1) { + var openLogEntries = function(entry) { + if (entry.logs) { + entry.logs.setVisible(true); + } + }; + + openLogEntries($scope.logEntries[$scope.logEntries.length - 2]); + openLogEntries($scope.logEntries[$scope.logEntries.length - 1]); + } + + // If the build phase is an error or a complete, then we mark the channel + // as closed. + callback(currentBuild['phase'] != 'error' && currentBuild['phase'] != 'complete'); + }, function() { + callback(false); + }); + }, function() { + callback(false); + }); + }; + + var fetchRepository = function() { + var params = {'repository': namespace + '/' + name}; + $rootScope.title = 'Loading Repository...'; + $scope.repository = ApiService.getRepoAsResource(params).get(function(repo) { + if (!repo.can_write) { + $rootScope.title = 'Unknown builds'; + $scope.accessDenied = true; + return; + } + + $rootScope.title = 'Repository Builds'; + $scope.repo = repo; + + getBuildInfo(); + }); + }; + + var getBuildInfo = function(repo) { + var params = { + 'repository': namespace + '/' + name + }; + + ApiService.getRepoBuilds(null, params).then(function(resp) { + $scope.builds = resp.builds; + + if ($location.search().current) { + $scope.setCurrentBuild($location.search().current, false); + } else if ($scope.builds.length > 0) { + $scope.setCurrentBuild($scope.builds[0].id, true); + } + }); + }; + + fetchRepository(); +} \ No newline at end of file diff --git a/static/partials/repo-build.html b/static/partials/repo-build.html index 6b24dc5e8..5fe3439e3 100644 --- a/static/partials/repo-build.html +++ b/static/partials/repo-build.html @@ -94,13 +94,20 @@
- + + + {{ build.id }}
diff --git a/test/test_api_security.py b/test/test_api_security.py index a6714aa28..6cc790fbc 100644 --- a/test/test_api_security.py +++ b/test/test_api_security.py @@ -15,7 +15,7 @@ from endpoints.api.tag import RepositoryTagImages, RepositoryTag from endpoints.api.search import FindRepositories, EntitySearch from endpoints.api.image import RepositoryImageChanges, RepositoryImage, RepositoryImageList from endpoints.api.build import (FileDropResource, RepositoryBuildStatus, RepositoryBuildLogs, - RepositoryBuildList) + RepositoryBuildList, RepositoryBuildResource) from endpoints.api.robot import (UserRobotList, OrgRobot, OrgRobotList, UserRobot, RegenerateOrgRobot, RegenerateUserRobot) @@ -1571,6 +1571,60 @@ class TestRepositoryBuildStatusFg86BuynlargeOrgrepo(ApiTestCase): self._run_test('GET', 400, 'devtable', None) +class TestRepositoryBuildResourceFg86PublicPublicrepo(ApiTestCase): + def setUp(self): + ApiTestCase.setUp(self) + self._set_url(RepositoryBuildResource, build_uuid="FG86", repository="public/publicrepo") + + def test_delete_anonymous(self): + self._run_test('DELETE', 401, None, None) + + def test_delete_freshuser(self): + self._run_test('DELETE', 403, 'freshuser', None) + + def test_delete_reader(self): + self._run_test('DELETE', 403, 'reader', None) + + def test_delete_devtable(self): + self._run_test('DELETE', 403, 'devtable', None) + + +class TestRepositoryBuildResourceFg86DevtableShared(ApiTestCase): + def setUp(self): + ApiTestCase.setUp(self) + self._set_url(RepositoryBuildResource, build_uuid="FG86", repository="devtable/shared") + + def test_delete_anonymous(self): + self._run_test('DELETE', 401, None, None) + + def test_delete_freshuser(self): + self._run_test('DELETE', 403, 'freshuser', None) + + def test_delete_reader(self): + self._run_test('DELETE', 403, 'reader', None) + + def test_delete_devtable(self): + self._run_test('DELETE', 404, 'devtable', None) + + +class TestRepositoryBuildResourceFg86BuynlargeOrgrepo(ApiTestCase): + def setUp(self): + ApiTestCase.setUp(self) + self._set_url(RepositoryBuildResource, build_uuid="FG86", repository="buynlarge/orgrepo") + + def test_delete_anonymous(self): + self._run_test('DELETE', 401, None, None) + + def test_delete_freshuser(self): + self._run_test('DELETE', 403, 'freshuser', None) + + def test_delete_reader(self): + self._run_test('DELETE', 403, 'reader', None) + + def test_delete_devtable(self): + self._run_test('DELETE', 404, 'devtable', None) + + class TestRepositoryBuildLogsS5j8PublicPublicrepo(ApiTestCase): def setUp(self): ApiTestCase.setUp(self) diff --git a/test/test_api_usage.py b/test/test_api_usage.py index 27384daa8..1c97d2983 100644 --- a/test/test_api_usage.py +++ b/test/test_api_usage.py @@ -17,7 +17,8 @@ from endpoints.api.team import TeamMember, TeamMemberList, TeamMemberInvite, Org from endpoints.api.tag import RepositoryTagImages, RepositoryTag from endpoints.api.search import FindRepositories, EntitySearch from endpoints.api.image import RepositoryImage, RepositoryImageList -from endpoints.api.build import RepositoryBuildStatus, RepositoryBuildLogs, RepositoryBuildList +from endpoints.api.build import (RepositoryBuildStatus, RepositoryBuildLogs, RepositoryBuildList, + RepositoryBuildResource) from endpoints.api.robot import (UserRobotList, OrgRobot, OrgRobotList, UserRobot, RegenerateUserRobot, RegenerateOrgRobot) from endpoints.api.trigger import (BuildTriggerActivate, BuildTriggerSources, BuildTriggerSubdirs, @@ -1303,6 +1304,103 @@ class TestGetRepository(ApiTestCase): self.assertEquals(True, json['is_organization']) + +class TestRepositoryBuildResource(ApiTestCase): + def test_cancel_invalidbuild(self): + self.login(ADMIN_ACCESS_USER) + + self.deleteResponse(RepositoryBuildResource, + params=dict(repository=ADMIN_ACCESS_USER + '/simple', build_uuid='invalid'), + expected_code=404) + + def test_cancel_waitingbuild(self): + self.login(ADMIN_ACCESS_USER) + + # Request a (fake) build. + json = self.postJsonResponse(RepositoryBuildList, + params=dict(repository=ADMIN_ACCESS_USER + '/simple'), + data=dict(file_id='foobarbaz'), + expected_code=201) + + uuid = json['id'] + + # Check for the build. + json = self.getJsonResponse(RepositoryBuildList, + params=dict(repository=ADMIN_ACCESS_USER + '/simple')) + + self.assertEquals(1, len(json['builds'])) + self.assertEquals(uuid, json['builds'][0]['id']) + + # Cancel the build. + self.deleteResponse(RepositoryBuildResource, + params=dict(repository=ADMIN_ACCESS_USER + '/simple', build_uuid=uuid), + expected_code=201) + + # Check for the build. + json = self.getJsonResponse(RepositoryBuildList, + params=dict(repository=ADMIN_ACCESS_USER + '/simple')) + + self.assertEquals(0, len(json['builds'])) + + + def test_attemptcancel_scheduledbuild(self): + self.login(ADMIN_ACCESS_USER) + + # Request a (fake) build. + json = self.postJsonResponse(RepositoryBuildList, + params=dict(repository=ADMIN_ACCESS_USER + '/simple'), + data=dict(file_id='foobarbaz'), + expected_code=201) + + uuid = json['id'] + + # Check for the build. + json = self.getJsonResponse(RepositoryBuildList, + params=dict(repository=ADMIN_ACCESS_USER + '/simple')) + + self.assertEquals(1, len(json['builds'])) + self.assertEquals(uuid, json['builds'][0]['id']) + + # Set queue item to be picked up. + qi = database.QueueItem.get(id=1) + qi.available = False + qi.save() + + # Try to cancel the build. + self.deleteResponse(RepositoryBuildResource, + params=dict(repository=ADMIN_ACCESS_USER + '/simple', build_uuid=uuid), + expected_code=400) + + + def test_attemptcancel_workingbuild(self): + self.login(ADMIN_ACCESS_USER) + + # Request a (fake) build. + json = self.postJsonResponse(RepositoryBuildList, + params=dict(repository=ADMIN_ACCESS_USER + '/simple'), + data=dict(file_id='foobarbaz'), + expected_code=201) + + uuid = json['id'] + + # Check for the build. + json = self.getJsonResponse(RepositoryBuildList, + params=dict(repository=ADMIN_ACCESS_USER + '/simple')) + + self.assertEquals(1, len(json['builds'])) + self.assertEquals(uuid, json['builds'][0]['id']) + + # Set the build to a different phase. + rb = database.RepositoryBuild.get(uuid=uuid) + rb.phase = database.BUILD_PHASE.BUILDING + rb.save() + + # Try to cancel the build. + self.deleteResponse(RepositoryBuildResource, + params=dict(repository=ADMIN_ACCESS_USER + '/simple', build_uuid=uuid), + expected_code=400) + + class TestRepoBuilds(ApiTestCase): def test_getrepo_nobuilds(self): self.login(ADMIN_ACCESS_USER)