Add ability for triggers to be disabled
Will be used in the followup commit to automatically disable broken triggers
This commit is contained in:
parent
1e54a4d9e9
commit
c35eec0615
18 changed files with 358 additions and 37 deletions
|
@ -22,7 +22,8 @@ from endpoints.api import (RepositoryParamResource, parse_args, query_param, nic
|
|||
require_repo_read, require_repo_write, validate_json_request,
|
||||
ApiResource, internal_only, format_date, api, path_param,
|
||||
require_repo_admin, abort, disallow_for_app_repositories)
|
||||
from endpoints.building import start_build, PreparedBuild, MaximumBuildsQueuedException
|
||||
from endpoints.building import (start_build, PreparedBuild, MaximumBuildsQueuedException,
|
||||
BuildTriggerDisabledException)
|
||||
from endpoints.exception import Unauthorized, NotFound, InvalidRequest
|
||||
from util.names import parse_robot_username
|
||||
|
||||
|
@ -69,6 +70,8 @@ def trigger_view(trigger, can_read=False, can_admin=False, for_build=False):
|
|||
|
||||
'config': build_trigger.config if can_admin else {},
|
||||
'can_invoke': can_admin,
|
||||
'enabled': trigger.enabled,
|
||||
'disabled_reason': trigger.disabled_reason.name if trigger.disabled_reason else None,
|
||||
}
|
||||
|
||||
if not for_build and can_admin and trigger.pull_robot:
|
||||
|
@ -309,6 +312,8 @@ class RepositoryBuildList(RepositoryParamResource):
|
|||
build_request = start_build(repo, prepared, pull_robot_name=pull_robot_name)
|
||||
except MaximumBuildsQueuedException:
|
||||
abort(429, message='Maximum queued build rate exceeded.')
|
||||
except BuildTriggerDisabledException:
|
||||
abort(400, message='Build trigger is disabled')
|
||||
|
||||
resp = build_status_view(build_request)
|
||||
repo_string = '%s/%s' % (namespace, repository)
|
||||
|
|
|
@ -13,7 +13,11 @@ from endpoints.api.signing import RepositorySignatures
|
|||
from endpoints.api.search import ConductRepositorySearch
|
||||
from endpoints.api.superuser import SuperUserRepositoryBuildLogs, SuperUserRepositoryBuildResource
|
||||
from endpoints.api.superuser import SuperUserRepositoryBuildStatus
|
||||
<<<<<<< HEAD
|
||||
from endpoints.api.appspecifictokens import AppTokens, AppToken
|
||||
=======
|
||||
from endpoints.api.trigger import BuildTrigger
|
||||
>>>>>>> Add ability for triggers to be disabled
|
||||
from endpoints.test.shared import client_with_identity, toggle_feature
|
||||
|
||||
from test.fixtures import *
|
||||
|
@ -24,6 +28,7 @@ REPO_PARAMS = {'repository': 'devtable/someapp'}
|
|||
SEARCH_PARAMS = {'query': ''}
|
||||
NOTIFICATION_PARAMS = {'namespace': 'devtable', 'repository': 'devtable/simple', 'uuid': 'some uuid'}
|
||||
TOKEN_PARAMS = {'token_uuid': 'someuuid'}
|
||||
TRIGGER_PARAMS = {'repository': 'devtable/simple', 'trigger_uuid': 'someuuid'}
|
||||
|
||||
@pytest.mark.parametrize('resource,method,params,body,identity,expected', [
|
||||
(AppTokens, 'GET', {}, {}, None, 401),
|
||||
|
@ -89,7 +94,22 @@ TOKEN_PARAMS = {'token_uuid': 'someuuid'}
|
|||
(RepositoryTrust, 'POST', REPO_PARAMS, {'trust_enabled': True}, 'freshuser', 403),
|
||||
(RepositoryTrust, 'POST', REPO_PARAMS, {'trust_enabled': True}, 'reader', 403),
|
||||
(RepositoryTrust, 'POST', REPO_PARAMS, {'trust_enabled': True}, 'devtable', 404),
|
||||
|
||||
|
||||
(BuildTrigger, 'GET', TRIGGER_PARAMS, {}, None, 401),
|
||||
(BuildTrigger, 'GET', TRIGGER_PARAMS, {}, 'freshuser', 403),
|
||||
(BuildTrigger, 'GET', TRIGGER_PARAMS, {}, 'reader', 403),
|
||||
(BuildTrigger, 'GET', TRIGGER_PARAMS, {}, 'devtable', 404),
|
||||
|
||||
(BuildTrigger, 'DELETE', TRIGGER_PARAMS, {}, None, 403),
|
||||
(BuildTrigger, 'DELETE', TRIGGER_PARAMS, {}, 'freshuser', 403),
|
||||
(BuildTrigger, 'DELETE', TRIGGER_PARAMS, {}, 'reader', 403),
|
||||
(BuildTrigger, 'DELETE', TRIGGER_PARAMS, {}, 'devtable', 404),
|
||||
|
||||
(BuildTrigger, 'PUT', TRIGGER_PARAMS, {}, None, 403),
|
||||
(BuildTrigger, 'PUT', TRIGGER_PARAMS, {}, 'freshuser', 403),
|
||||
(BuildTrigger, 'PUT', TRIGGER_PARAMS, {}, 'reader', 403),
|
||||
(BuildTrigger, 'PUT', TRIGGER_PARAMS, {}, 'devtable', 400),
|
||||
|
||||
(RepositoryUserTransitivePermission, 'GET', {'username': 'A2O9','repository': 'public/publicrepo'}, None, None, 401),
|
||||
(RepositoryUserTransitivePermission, 'GET', {'username': 'A2O9','repository': 'public/publicrepo'}, None, 'freshuser', 403),
|
||||
(RepositoryUserTransitivePermission, 'GET', {'username': 'A2O9','repository': 'public/publicrepo'}, None, 'reader', 403),
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
import pytest
|
||||
import json
|
||||
|
||||
from data import model
|
||||
from endpoints.api.trigger_analyzer import is_parent
|
||||
from endpoints.api.trigger import BuildTrigger
|
||||
from endpoints.api.test.shared import conduct_api_call
|
||||
from endpoints.test.shared import client_with_identity
|
||||
from test.fixtures import *
|
||||
|
||||
|
||||
@pytest.mark.parametrize('context,dockerfile_path,expected', [
|
||||
|
@ -20,3 +26,30 @@ from endpoints.api.trigger_analyzer import is_parent
|
|||
])
|
||||
def test_super_user_build_endpoints(context, dockerfile_path, expected):
|
||||
assert is_parent(context, dockerfile_path) == expected
|
||||
|
||||
|
||||
def test_enabled_disabled_trigger(app, client):
|
||||
trigger = model.build.list_build_triggers('devtable', 'building')[0]
|
||||
trigger.config = json.dumps({'hook_id': 'someid'})
|
||||
trigger.save()
|
||||
|
||||
params = {
|
||||
'repository': 'devtable/building',
|
||||
'trigger_uuid': trigger.uuid,
|
||||
}
|
||||
|
||||
body = {
|
||||
'enabled': False,
|
||||
}
|
||||
|
||||
with client_with_identity('devtable', client) as cl:
|
||||
result = conduct_api_call(cl, BuildTrigger, 'PUT', params, body, 200).json
|
||||
assert not result['enabled']
|
||||
|
||||
body = {
|
||||
'enabled': True,
|
||||
}
|
||||
|
||||
with client_with_identity('devtable', client) as cl:
|
||||
result = conduct_api_call(cl, BuildTrigger, 'PUT', params, body, 200).json
|
||||
assert result['enabled']
|
||||
|
|
|
@ -22,7 +22,8 @@ from endpoints.api import (RepositoryParamResource, nickname, resource, require_
|
|||
disallow_for_app_repositories)
|
||||
from endpoints.api.build import build_status_view, trigger_view, RepositoryBuildStatus
|
||||
from endpoints.api.trigger_analyzer import TriggerAnalyzer
|
||||
from endpoints.building import start_build, MaximumBuildsQueuedException
|
||||
from endpoints.building import (start_build, MaximumBuildsQueuedException,
|
||||
BuildTriggerDisabledException)
|
||||
from endpoints.exception import NotFound, Unauthorized, InvalidRequest
|
||||
from util.names import parse_robot_username
|
||||
|
||||
|
@ -62,6 +63,21 @@ class BuildTriggerList(RepositoryParamResource):
|
|||
@path_param('trigger_uuid', 'The UUID of the build trigger')
|
||||
class BuildTrigger(RepositoryParamResource):
|
||||
""" Resource for managing specific build triggers. """
|
||||
schemas = {
|
||||
'UpdateTrigger': {
|
||||
'type': 'object',
|
||||
'description': 'Options for updating a build trigger',
|
||||
'required': [
|
||||
'enabled',
|
||||
],
|
||||
'properties': {
|
||||
'enabled': {
|
||||
'type': 'boolean',
|
||||
'description': 'Whether the build trigger is enabled',
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@require_repo_admin
|
||||
@disallow_for_app_repositories
|
||||
|
@ -70,6 +86,27 @@ class BuildTrigger(RepositoryParamResource):
|
|||
""" Get information for the specified build trigger. """
|
||||
return trigger_view(get_trigger(trigger_uuid), can_admin=True)
|
||||
|
||||
@require_repo_admin
|
||||
@disallow_for_app_repositories
|
||||
@nickname('updateBuildTrigger')
|
||||
@validate_json_request('UpdateTrigger')
|
||||
def put(self, namespace_name, repo_name, trigger_uuid):
|
||||
""" Updates the specified build trigger. """
|
||||
trigger = get_trigger(trigger_uuid)
|
||||
|
||||
handler = BuildTriggerHandler.get_handler(trigger)
|
||||
if not handler.is_active():
|
||||
raise InvalidRequest('Cannot update an unactivated trigger')
|
||||
|
||||
enable = request.get_json()['enabled']
|
||||
model.build.toggle_build_trigger(trigger, enable)
|
||||
log_action('toggle_repo_trigger', namespace_name,
|
||||
{'repo': repo_name, 'trigger_id': trigger_uuid,
|
||||
'service': trigger.service.name, 'enabled': enable},
|
||||
repo=model.repository.get_repository(namespace_name, repo_name))
|
||||
|
||||
return trigger_view(trigger)
|
||||
|
||||
@require_repo_admin
|
||||
@disallow_for_app_repositories
|
||||
@nickname('deleteBuildTrigger')
|
||||
|
@ -340,6 +377,8 @@ class ActivateBuildTrigger(RepositoryParamResource):
|
|||
def post(self, namespace_name, repo_name, trigger_uuid):
|
||||
""" Manually start a build from the specified trigger. """
|
||||
trigger = get_trigger(trigger_uuid)
|
||||
if not trigger.enabled:
|
||||
raise InvalidRequest('Trigger is not enabled.')
|
||||
|
||||
handler = BuildTriggerHandler.get_handler(trigger)
|
||||
if not handler.is_active():
|
||||
|
@ -356,6 +395,8 @@ class ActivateBuildTrigger(RepositoryParamResource):
|
|||
raise InvalidRequest(tse.message)
|
||||
except MaximumBuildsQueuedException:
|
||||
abort(429, message='Maximum queued build rate exceeded.')
|
||||
except BuildTriggerDisabledException:
|
||||
abort(400, message='Build trigger is disabled')
|
||||
|
||||
resp = build_status_view(build_request)
|
||||
repo_string = '%s/%s' % (namespace_name, repo_name)
|
||||
|
@ -485,3 +526,4 @@ class BuildTriggerSourceNamespaces(RepositoryParamResource):
|
|||
raise InvalidRequest(rre.message)
|
||||
else:
|
||||
raise Unauthorized()
|
||||
|
||||
|
|
|
@ -25,10 +25,22 @@ class MaximumBuildsQueuedException(Exception):
|
|||
pass
|
||||
|
||||
|
||||
class BuildTriggerDisabledException(Exception):
|
||||
"""
|
||||
This exception is raised when a build is required, but the build trigger has been disabled.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def start_build(repository, prepared_build, pull_robot_name=None):
|
||||
# Ensure that builds are only run in image repositories.
|
||||
if repository.kind.name != 'image':
|
||||
raise Exception('Attempt to start a build for application repository %s' % repository.id)
|
||||
|
||||
# Ensure that disabled triggers are not run.
|
||||
if prepared_build.trigger is not None and not prepared_build.trigger.enabled:
|
||||
raise BuildTriggerDisabledException
|
||||
|
||||
if repository.namespace_user.maximum_queued_builds_count is not None:
|
||||
queue_item_canonical_name = [repository.namespace_user.username]
|
||||
alive_builds = dockerfile_build_queue.num_alive_jobs(queue_item_canonical_name)
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import pytest
|
||||
|
||||
from data import model
|
||||
from endpoints.building import start_build, PreparedBuild, MaximumBuildsQueuedException
|
||||
from endpoints.building import (start_build, PreparedBuild, MaximumBuildsQueuedException,
|
||||
BuildTriggerDisabledException)
|
||||
|
||||
from test.fixtures import *
|
||||
|
||||
|
@ -29,3 +30,14 @@ def test_maximum_builds(app):
|
|||
# Try to queue a second build; should fail.
|
||||
with pytest.raises(MaximumBuildsQueuedException):
|
||||
start_build(repo, prepared_build)
|
||||
|
||||
|
||||
def test_start_build_disabled_trigger(app):
|
||||
trigger = model.build.list_build_triggers('devtable', 'building')[0]
|
||||
trigger.enabled = False
|
||||
trigger.save()
|
||||
|
||||
build = PreparedBuild(trigger=trigger)
|
||||
|
||||
with pytest.raises(BuildTriggerDisabledException):
|
||||
start_build(trigger.repository, build)
|
||||
|
|
24
endpoints/test/test_webhooks.py
Normal file
24
endpoints/test/test_webhooks.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
import base64
|
||||
import pytest
|
||||
|
||||
from flask import url_for
|
||||
|
||||
from data import model
|
||||
from endpoints.test.shared import conduct_call
|
||||
from test.fixtures import *
|
||||
|
||||
def test_start_build_disabled_trigger(app, client):
|
||||
trigger = model.build.list_build_triggers('devtable', 'building')[0]
|
||||
trigger.enabled = False
|
||||
trigger.save()
|
||||
|
||||
params = {
|
||||
'trigger_uuid': trigger.uuid,
|
||||
}
|
||||
|
||||
headers = {
|
||||
'Authorization': 'Basic ' + base64.b64encode('devtable:password'),
|
||||
}
|
||||
|
||||
conduct_call(client, 'webhooks.build_trigger_webhook', url_for, 'POST', params, None, 400,
|
||||
headers=headers)
|
|
@ -12,7 +12,8 @@ from util.http import abort
|
|||
from buildtrigger.basehandler import BuildTriggerHandler
|
||||
from buildtrigger.triggerutil import (ValidationRequestException, SkipRequestException,
|
||||
InvalidPayloadException)
|
||||
from endpoints.building import start_build, MaximumBuildsQueuedException
|
||||
from endpoints.building import (start_build, MaximumBuildsQueuedException,
|
||||
BuildTriggerDisabledException)
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -91,9 +92,7 @@ def build_trigger_webhook(trigger_uuid, **kwargs):
|
|||
|
||||
namespace = trigger.repository.namespace_user.username
|
||||
repository = trigger.repository.name
|
||||
permission = ModifyRepositoryPermission(namespace, repository)
|
||||
|
||||
if permission.can():
|
||||
if ModifyRepositoryPermission(namespace, repository).can():
|
||||
handler = BuildTriggerHandler.get_handler(trigger)
|
||||
|
||||
if trigger.repository.kind.name != 'image':
|
||||
|
@ -121,6 +120,9 @@ def build_trigger_webhook(trigger_uuid, **kwargs):
|
|||
start_build(repo, prepared, pull_robot_name=pull_robot_name)
|
||||
except MaximumBuildsQueuedException:
|
||||
abort(429, message='Maximum queued build rate exceeded.')
|
||||
except BuildTriggerDisabledException:
|
||||
logger.debug('Build trigger %s is disabled', trigger_uuid)
|
||||
abort(400, message='This build trigger is currently disabled. Please re-enable to continue.')
|
||||
|
||||
return make_response('Okay')
|
||||
|
||||
|
|
Reference in a new issue