Add ability for triggers to be disabled

Will be used in the followup commit to automatically disable broken triggers
This commit is contained in:
Joseph Schorr 2017-10-17 17:01:59 -04:00 committed by Joseph Schorr
parent 1e54a4d9e9
commit c35eec0615
18 changed files with 358 additions and 37 deletions

View file

@ -694,6 +694,10 @@ class BuildTriggerService(BaseModel):
name = CharField(index=True, unique=True) name = CharField(index=True, unique=True)
class DisableReason(BaseModel):
name = CharField(index=True, unique=True)
class RepositoryBuildTrigger(BaseModel): class RepositoryBuildTrigger(BaseModel):
uuid = CharField(default=uuid_generator) uuid = CharField(default=uuid_generator)
service = ForeignKeyField(BuildTriggerService) service = ForeignKeyField(BuildTriggerService)
@ -705,6 +709,8 @@ class RepositoryBuildTrigger(BaseModel):
write_token = ForeignKeyField(AccessToken, null=True) write_token = ForeignKeyField(AccessToken, null=True)
pull_robot = QuayUserField(allows_robots=True, null=True, related_name='triggerpullrobot', pull_robot = QuayUserField(allows_robots=True, null=True, related_name='triggerpullrobot',
robot_null_delete=True) robot_null_delete=True)
enabled = BooleanField(default=True)
disabled_reason = ForeignKeyField(DisableReason, null=True)
class EmailConfirmation(BaseModel): class EmailConfirmation(BaseModel):

View file

@ -0,0 +1,56 @@
"""Add ability for build triggers to be disabled
Revision ID: 61cadbacb9fc
Revises: d8989249f8f6
Create Date: 2017-10-18 12:07:26.190901
"""
# revision identifiers, used by Alembic.
revision = '61cadbacb9fc'
down_revision = 'd8989249f8f6'
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql
def upgrade(tables):
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('disablereason',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=255), nullable=False),
sa.PrimaryKeyConstraint('id', name=op.f('pk_disablereason'))
)
op.create_index('disablereason_name', 'disablereason', ['name'], unique=True)
op.bulk_insert(
tables.disablereason,
[
{'id': 1, 'name': 'user_toggled'},
],
)
op.bulk_insert(tables.logentrykind, [
{'name': 'toggle_repo_trigger'},
])
op.add_column(u'repositorybuildtrigger', sa.Column('disabled_reason_id', sa.Integer(), nullable=True))
op.add_column(u'repositorybuildtrigger', sa.Column('enabled', sa.Boolean(), nullable=False))
op.create_index('repositorybuildtrigger_disabled_reason_id', 'repositorybuildtrigger', ['disabled_reason_id'], unique=False)
op.create_foreign_key(op.f('fk_repositorybuildtrigger_disabled_reason_id_disablereason'), 'repositorybuildtrigger', 'disablereason', ['disabled_reason_id'], ['id'])
# ### end Alembic commands ###
def downgrade(tables):
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(op.f('fk_repositorybuildtrigger_disabled_reason_id_disablereason'), 'repositorybuildtrigger', type_='foreignkey')
op.drop_index('repositorybuildtrigger_disabled_reason_id', table_name='repositorybuildtrigger')
op.drop_column(u'repositorybuildtrigger', 'enabled')
op.drop_column(u'repositorybuildtrigger', 'disabled_reason_id')
op.drop_table('disablereason')
# ### end Alembic commands ###
op.execute(tables
.logentrykind
.delete()
.where(tables.logentrykind.c.name == op.inline_literal('toggle_repo_trigger')))

View file

@ -10,7 +10,8 @@ Create Date: 2017-03-17 10:00:19.739858
import json import json
import os import os
from data.database import RepositoryBuildTrigger from peewee import *
from data.database import BaseModel
revision = 'a6c463dfb9fe' revision = 'a6c463dfb9fe'
down_revision = 'b4df55dea4b3' down_revision = 'b4df55dea4b3'
@ -18,6 +19,9 @@ down_revision = 'b4df55dea4b3'
from alembic import op from alembic import op
class RepositoryBuildTrigger(BaseModel):
config = TextField(default='{}')
def upgrade(tables): def upgrade(tables):
repostioryBuildTriggers = RepositoryBuildTrigger.select() repostioryBuildTriggers = RepositoryBuildTrigger.select()
for repositoryBuildTrigger in repostioryBuildTriggers: for repositoryBuildTrigger in repostioryBuildTriggers:

View file

@ -6,7 +6,8 @@ from peewee import JOIN_LEFT_OUTER
import features import features
from data.database import (BuildTriggerService, RepositoryBuildTrigger, Repository, Namespace, User, from data.database import (BuildTriggerService, RepositoryBuildTrigger, Repository, Namespace, User,
RepositoryBuild, BUILD_PHASE, db_random_func, UseThenDisconnect) RepositoryBuild, BUILD_PHASE, db_random_func, UseThenDisconnect,
DisableReason)
from data.model import (InvalidBuildTriggerException, InvalidRepositoryBuildException, from data.model import (InvalidBuildTriggerException, InvalidRepositoryBuildException,
db_transaction, user as user_model, config) db_transaction, user as user_model, config)
@ -255,3 +256,13 @@ def mark_build_archived(build_uuid):
.where(RepositoryBuild.uuid == build_uuid, .where(RepositoryBuild.uuid == build_uuid,
RepositoryBuild.logs_archived == False) RepositoryBuild.logs_archived == False)
.execute()) > 0 .execute()) > 0
def toggle_build_trigger(trigger, enabled, reason='user_toggled'):
""" Toggles the enabled status of a build trigger. """
trigger.enabled = enabled
if not enabled:
trigger.disabled_reason = DisableReason.get(name=reason)
trigger.save()

View file

@ -22,7 +22,8 @@ from endpoints.api import (RepositoryParamResource, parse_args, query_param, nic
require_repo_read, require_repo_write, validate_json_request, require_repo_read, require_repo_write, validate_json_request,
ApiResource, internal_only, format_date, api, path_param, ApiResource, internal_only, format_date, api, path_param,
require_repo_admin, abort, disallow_for_app_repositories) 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 endpoints.exception import Unauthorized, NotFound, InvalidRequest
from util.names import parse_robot_username 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 {}, 'config': build_trigger.config if can_admin else {},
'can_invoke': can_admin, '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: 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) build_request = start_build(repo, prepared, pull_robot_name=pull_robot_name)
except MaximumBuildsQueuedException: except MaximumBuildsQueuedException:
abort(429, message='Maximum queued build rate exceeded.') abort(429, message='Maximum queued build rate exceeded.')
except BuildTriggerDisabledException:
abort(400, message='Build trigger is disabled')
resp = build_status_view(build_request) resp = build_status_view(build_request)
repo_string = '%s/%s' % (namespace, repository) repo_string = '%s/%s' % (namespace, repository)

View file

@ -13,7 +13,11 @@ from endpoints.api.signing import RepositorySignatures
from endpoints.api.search import ConductRepositorySearch from endpoints.api.search import ConductRepositorySearch
from endpoints.api.superuser import SuperUserRepositoryBuildLogs, SuperUserRepositoryBuildResource from endpoints.api.superuser import SuperUserRepositoryBuildLogs, SuperUserRepositoryBuildResource
from endpoints.api.superuser import SuperUserRepositoryBuildStatus from endpoints.api.superuser import SuperUserRepositoryBuildStatus
<<<<<<< HEAD
from endpoints.api.appspecifictokens import AppTokens, AppToken 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 endpoints.test.shared import client_with_identity, toggle_feature
from test.fixtures import * from test.fixtures import *
@ -24,6 +28,7 @@ REPO_PARAMS = {'repository': 'devtable/someapp'}
SEARCH_PARAMS = {'query': ''} SEARCH_PARAMS = {'query': ''}
NOTIFICATION_PARAMS = {'namespace': 'devtable', 'repository': 'devtable/simple', 'uuid': 'some uuid'} NOTIFICATION_PARAMS = {'namespace': 'devtable', 'repository': 'devtable/simple', 'uuid': 'some uuid'}
TOKEN_PARAMS = {'token_uuid': 'someuuid'} TOKEN_PARAMS = {'token_uuid': 'someuuid'}
TRIGGER_PARAMS = {'repository': 'devtable/simple', 'trigger_uuid': 'someuuid'}
@pytest.mark.parametrize('resource,method,params,body,identity,expected', [ @pytest.mark.parametrize('resource,method,params,body,identity,expected', [
(AppTokens, 'GET', {}, {}, None, 401), (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}, 'freshuser', 403),
(RepositoryTrust, 'POST', REPO_PARAMS, {'trust_enabled': True}, 'reader', 403), (RepositoryTrust, 'POST', REPO_PARAMS, {'trust_enabled': True}, 'reader', 403),
(RepositoryTrust, 'POST', REPO_PARAMS, {'trust_enabled': True}, 'devtable', 404), (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, None, 401),
(RepositoryUserTransitivePermission, 'GET', {'username': 'A2O9','repository': 'public/publicrepo'}, None, 'freshuser', 403), (RepositoryUserTransitivePermission, 'GET', {'username': 'A2O9','repository': 'public/publicrepo'}, None, 'freshuser', 403),
(RepositoryUserTransitivePermission, 'GET', {'username': 'A2O9','repository': 'public/publicrepo'}, None, 'reader', 403), (RepositoryUserTransitivePermission, 'GET', {'username': 'A2O9','repository': 'public/publicrepo'}, None, 'reader', 403),

View file

@ -1,6 +1,12 @@
import pytest import pytest
import json
from data import model
from endpoints.api.trigger_analyzer import is_parent 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', [ @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): def test_super_user_build_endpoints(context, dockerfile_path, expected):
assert is_parent(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']

View file

@ -22,7 +22,8 @@ from endpoints.api import (RepositoryParamResource, nickname, resource, require_
disallow_for_app_repositories) disallow_for_app_repositories)
from endpoints.api.build import build_status_view, trigger_view, RepositoryBuildStatus from endpoints.api.build import build_status_view, trigger_view, RepositoryBuildStatus
from endpoints.api.trigger_analyzer import TriggerAnalyzer 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 endpoints.exception import NotFound, Unauthorized, InvalidRequest
from util.names import parse_robot_username from util.names import parse_robot_username
@ -62,6 +63,21 @@ class BuildTriggerList(RepositoryParamResource):
@path_param('trigger_uuid', 'The UUID of the build trigger') @path_param('trigger_uuid', 'The UUID of the build trigger')
class BuildTrigger(RepositoryParamResource): class BuildTrigger(RepositoryParamResource):
""" Resource for managing specific build triggers. """ """ 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 @require_repo_admin
@disallow_for_app_repositories @disallow_for_app_repositories
@ -70,6 +86,27 @@ class BuildTrigger(RepositoryParamResource):
""" Get information for the specified build trigger. """ """ Get information for the specified build trigger. """
return trigger_view(get_trigger(trigger_uuid), can_admin=True) 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 @require_repo_admin
@disallow_for_app_repositories @disallow_for_app_repositories
@nickname('deleteBuildTrigger') @nickname('deleteBuildTrigger')
@ -340,6 +377,8 @@ class ActivateBuildTrigger(RepositoryParamResource):
def post(self, namespace_name, repo_name, trigger_uuid): def post(self, namespace_name, repo_name, trigger_uuid):
""" Manually start a build from the specified trigger. """ """ Manually start a build from the specified trigger. """
trigger = get_trigger(trigger_uuid) trigger = get_trigger(trigger_uuid)
if not trigger.enabled:
raise InvalidRequest('Trigger is not enabled.')
handler = BuildTriggerHandler.get_handler(trigger) handler = BuildTriggerHandler.get_handler(trigger)
if not handler.is_active(): if not handler.is_active():
@ -356,6 +395,8 @@ class ActivateBuildTrigger(RepositoryParamResource):
raise InvalidRequest(tse.message) raise InvalidRequest(tse.message)
except MaximumBuildsQueuedException: except MaximumBuildsQueuedException:
abort(429, message='Maximum queued build rate exceeded.') abort(429, message='Maximum queued build rate exceeded.')
except BuildTriggerDisabledException:
abort(400, message='Build trigger is disabled')
resp = build_status_view(build_request) resp = build_status_view(build_request)
repo_string = '%s/%s' % (namespace_name, repo_name) repo_string = '%s/%s' % (namespace_name, repo_name)
@ -485,3 +526,4 @@ class BuildTriggerSourceNamespaces(RepositoryParamResource):
raise InvalidRequest(rre.message) raise InvalidRequest(rre.message)
else: else:
raise Unauthorized() raise Unauthorized()

View file

@ -25,10 +25,22 @@ class MaximumBuildsQueuedException(Exception):
pass 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): def start_build(repository, prepared_build, pull_robot_name=None):
# Ensure that builds are only run in image repositories.
if repository.kind.name != 'image': if repository.kind.name != 'image':
raise Exception('Attempt to start a build for application repository %s' % repository.id) 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: if repository.namespace_user.maximum_queued_builds_count is not None:
queue_item_canonical_name = [repository.namespace_user.username] queue_item_canonical_name = [repository.namespace_user.username]
alive_builds = dockerfile_build_queue.num_alive_jobs(queue_item_canonical_name) alive_builds = dockerfile_build_queue.num_alive_jobs(queue_item_canonical_name)

View file

@ -1,7 +1,8 @@
import pytest import pytest
from data import model 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 * from test.fixtures import *
@ -29,3 +30,14 @@ def test_maximum_builds(app):
# Try to queue a second build; should fail. # Try to queue a second build; should fail.
with pytest.raises(MaximumBuildsQueuedException): with pytest.raises(MaximumBuildsQueuedException):
start_build(repo, prepared_build) 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)

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

View file

@ -12,7 +12,8 @@ from util.http import abort
from buildtrigger.basehandler import BuildTriggerHandler from buildtrigger.basehandler import BuildTriggerHandler
from buildtrigger.triggerutil import (ValidationRequestException, SkipRequestException, from buildtrigger.triggerutil import (ValidationRequestException, SkipRequestException,
InvalidPayloadException) InvalidPayloadException)
from endpoints.building import start_build, MaximumBuildsQueuedException from endpoints.building import (start_build, MaximumBuildsQueuedException,
BuildTriggerDisabledException)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -91,9 +92,7 @@ def build_trigger_webhook(trigger_uuid, **kwargs):
namespace = trigger.repository.namespace_user.username namespace = trigger.repository.namespace_user.username
repository = trigger.repository.name repository = trigger.repository.name
permission = ModifyRepositoryPermission(namespace, repository) if ModifyRepositoryPermission(namespace, repository).can():
if permission.can():
handler = BuildTriggerHandler.get_handler(trigger) handler = BuildTriggerHandler.get_handler(trigger)
if trigger.repository.kind.name != 'image': 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) start_build(repo, prepared, pull_robot_name=pull_robot_name)
except MaximumBuildsQueuedException: except MaximumBuildsQueuedException:
abort(429, message='Maximum queued build rate exceeded.') 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') return make_response('Okay')

View file

@ -20,7 +20,7 @@ from data.database import (db, all_models, beta_classes, Role, TeamRole, Visibil
ExternalNotificationEvent, ExternalNotificationMethod, NotificationKind, ExternalNotificationEvent, ExternalNotificationMethod, NotificationKind,
QuayRegion, QuayService, UserRegion, OAuthAuthorizationCode, QuayRegion, QuayService, UserRegion, OAuthAuthorizationCode,
ServiceKeyApprovalType, MediaType, LabelSourceType, UserPromptKind, ServiceKeyApprovalType, MediaType, LabelSourceType, UserPromptKind,
RepositoryKind, TagKind, BlobPlacementLocation, User, RepositoryKind, TagKind, BlobPlacementLocation, User, DisableReason,
DeletedNamespace) DeletedNamespace)
from data import model from data import model
from data.queue import WorkQueue from data.queue import WorkQueue
@ -353,6 +353,7 @@ def initialize_database():
LogEntryKind.create(name='manifest_label_delete') LogEntryKind.create(name='manifest_label_delete')
LogEntryKind.create(name='change_tag_expiration') LogEntryKind.create(name='change_tag_expiration')
LogEntryKind.create(name='toggle_repo_trigger')
LogEntryKind.create(name='create_app_specific_token') LogEntryKind.create(name='create_app_specific_token')
LogEntryKind.create(name='revoke_app_specific_token') LogEntryKind.create(name='revoke_app_specific_token')
@ -434,6 +435,8 @@ def initialize_database():
TagKind.create(name='release') TagKind.create(name='release')
TagKind.create(name='channel') TagKind.create(name='channel')
DisableReason.create(name='user_toggled')
def wipe_database(): def wipe_database():
logger.debug('Wiping all data from the DB.') logger.debug('Wiping all data from the DB.')

View file

@ -13,4 +13,22 @@
.repo-panel-builds .heading-controls { .repo-panel-builds .heading-controls {
white-space: nowrap; white-space: nowrap;
} }
.repo-panel-builds .trigger-disabled {
background-color: #fcfcfc;
}
.repo-panel-builds .trigger-disabled td {
border-bottom: 0px;
color: #ccc;
}
.repo-panel-builds .trigger-disabled-message {
font-size: 13px;
}
.repo-panel-builds i.fa-exclamation-triangle {
color: #f5c77d;
margin-right: 4px;
}

View file

@ -137,31 +137,45 @@
<a ng-click="deleteTrigger(trigger)">Delete Trigger</a> <a ng-click="deleteTrigger(trigger)">Delete Trigger</a>
</td> </td>
</tr> </tr>
<tr ng-repeat="trigger in triggers | filter:{'is_active':true}"> <tbody ng-repeat="trigger in triggers | filter:{'is_active':true}">
<td><trigger-description trigger="trigger"></trigger-description></td> <tr ng-class="{'trigger-disabled': !trigger.enabled}">
<td>{{ trigger.config.dockerfile_path || '/Dockerfile' }}</td> <td><trigger-description trigger="trigger"></trigger-description></td>
<td>{{ trigger.config.context || '/' }}</td> <td>{{ trigger.config.dockerfile_path || '/Dockerfile' }}</td>
<td>{{ trigger.config.branchtag_regex || 'All' }}</td> <td>{{ trigger.config.context || '/' }}</td>
<td> <td>{{ trigger.config.branchtag_regex || 'All' }}</td>
<span class="entity-reference" entity="trigger.pull_robot" ng-if="trigger.pull_robot"></span> <td>
<span class="empty" ng-if="!trigger.pull_robot">(None)</span> <span class="entity-reference" entity="trigger.pull_robot" ng-if="trigger.pull_robot"></span>
</td> <span class="empty" ng-if="!trigger.pull_robot">(None)</span>
<td> </td>
<span class="cor-options-menu"> <td>
<span ng-if="trigger.config.credentials" class="cor-option" option-click="showTriggerCredentialsModal(trigger)"> <span class="cor-options-menu">
<i class="fa fa-unlock-alt"></i> View Credentials <span ng-if="trigger.config.credentials" class="cor-option" option-click="showTriggerCredentialsModal(trigger)">
<i class="fa fa-unlock-alt"></i> View Credentials
</span>
<span class="cor-option" option-click="askRunTrigger(trigger)"
ng-class="trigger.can_invoke && trigger.enabled ? '' : 'disabled'">
<i class="fa fa-chevron-right"></i> Run Trigger Now
</span>
<span class="cor-option" option-click="askToggleTrigger(trigger)">
<i class="fa fa-adjust"></i>
<span ng-if="trigger.enabled">Disable Trigger</span>
<span ng-if="!trigger.enabled">Enable Trigger</span>
</span>
<span class="cor-option" option-click="askDeleteTrigger(trigger)">
<i class="fa fa-times"></i> Delete Trigger
</span>
</span> </span>
<span class="cor-option" option-click="askRunTrigger(trigger)" </td>
ng-class="trigger.can_invoke ? '' : 'disabled'"> </tr>
<i class="fa fa-chevron-right"></i> Run Trigger Now <tr class="trigger-disabled-message" ng-if="!trigger.enabled">
</span> <td colspan="5" style="text-align: center">
<span class="cor-option" option-click="askDeleteTrigger(trigger)"> <i class="fa fa-exclamation-triangle"></i>
<i class="fa fa-times"></i> Delete Trigger This build trigger is currently disabled and will not build:
</span> <a ng-click="askToggleTrigger(trigger)">Re-enable this trigger</a>
</span> </td>
</td> </tr>
</tr> </tbody>
</table> </table>
@ -174,7 +188,16 @@
<!-- Trigger Credentials dialog --> <!-- Trigger Credentials dialog -->
<div class="trigger-credentials-dialog" trigger="triggerCredentialsModalTrigger" counter="triggerCredentialsModalCounter"></div> <div class="trigger-credentials-dialog" trigger="triggerCredentialsModalTrigger" counter="triggerCredentialsModalCounter"></div>
<!-- Delete Tag Confirm --> <!-- Toggle Trigger Confirm -->
<div class="cor-confirm-dialog"
dialog-context="toggleTriggerInfo"
dialog-action="toggleTrigger(info.trigger, callback)"
dialog-title="Toggle Trigger"
dialog-action-title="Toggle Trigger">
Are you sure you want to <span ng-if="toggleTriggerInfo.trigger.enabled">disable</span><span ng-if="!toggleTriggerInfo.trigger.enabled">enable</span> this trigger?
</div>
<!-- Delete Trigger Confirm -->
<div class="cor-confirm-dialog" <div class="cor-confirm-dialog"
dialog-context="deleteTriggerInfo" dialog-context="deleteTriggerInfo"
dialog-action="deleteTrigger(info.trigger, callback)" dialog-action="deleteTrigger(info.trigger, callback)"

View file

@ -187,6 +187,10 @@ angular.module('quay').directive('repoPanelBuilds', function () {
}; };
$scope.askRunTrigger = function(trigger) { $scope.askRunTrigger = function(trigger) {
if (!trigger.enabled) {
return;
}
if (!trigger.can_invoke) { if (!trigger.can_invoke) {
bootbox.alert('You do not have permission to manually invoke this trigger'); bootbox.alert('You do not have permission to manually invoke this trigger');
return; return;
@ -196,6 +200,40 @@ angular.module('quay').directive('repoPanelBuilds', function () {
$scope.showTriggerStartDialogCounter++; $scope.showTriggerStartDialogCounter++;
}; };
$scope.askToggleTrigger = function(trigger) {
if (!trigger.can_invoke) {
bootbox.alert('You do not have permission to edit this trigger');
return;
}
$scope.toggleTriggerInfo = {
'trigger': trigger
};
};
$scope.toggleTrigger = function(trigger, opt_callback) {
if (!trigger) { return; }
var params = {
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
'trigger_uuid': trigger.id
};
var data = {
'enabled': !trigger.enabled
};
var errorHandler = ApiService.errorDisplay('Could not toggle build trigger', function() {
opt_callback && opt_callback(false);
});
ApiService.updateBuildTrigger(data, params).then(function(resp) {
trigger.enabled = !trigger.enabled;
trigger.disabled_reason = 'user_toggled';
opt_callback && opt_callback(true);
}, errorHandler);
};
$scope.deleteTrigger = function(trigger, opt_callback) { $scope.deleteTrigger = function(trigger, opt_callback) {
if (!trigger) { return; } if (!trigger) { return; }

View file

@ -223,6 +223,15 @@ angular.module('quay').directive('logsView', function () {
metadata['service'], metadata['config']); metadata['service'], metadata['config']);
return 'Delete build trigger - ' + triggerDescription; return 'Delete build trigger - ' + triggerDescription;
}, },
'toggle_repo_trigger': function(metadata) {
var triggerDescription = TriggerService.getDescription(
metadata['service'], metadata['config']);
if (metadata.enabled) {
return 'Build trigger enabled - ' + triggerDescription;
} else {
return 'Build trigger disabled - ' + triggerDescription;
}
},
'create_application': 'Create application {application_name} with client ID {client_id}', 'create_application': 'Create application {application_name} with client ID {client_id}',
'update_application': 'Update application to {application_name} for client ID {client_id}', 'update_application': 'Update application to {application_name} for client ID {client_id}',
'delete_application': 'Delete application {application_name} with client ID {client_id}', 'delete_application': 'Delete application {application_name} with client ID {client_id}',
@ -330,6 +339,7 @@ angular.module('quay').directive('logsView', function () {
'delete_prototype_permission': 'Delete default permission', 'delete_prototype_permission': 'Delete default permission',
'setup_repo_trigger': 'Setup build trigger', 'setup_repo_trigger': 'Setup build trigger',
'delete_repo_trigger': 'Delete build trigger', 'delete_repo_trigger': 'Delete build trigger',
'toggle_repo_trigger': 'Enable/disable build trigger',
'create_application': 'Create Application', 'create_application': 'Create Application',
'update_application': 'Update Application', 'update_application': 'Update Application',
'delete_application': 'Delete Application', 'delete_application': 'Delete Application',

View file

@ -17,6 +17,7 @@ from endpoints.appr import appr_bp
from endpoints.web import web from endpoints.web import web
from endpoints.v2 import v2_bp from endpoints.v2 import v2_bp
from endpoints.verbs import verbs as verbs_bp from endpoints.verbs import verbs as verbs_bp
from endpoints.webhooks import webhooks
from initdb import initialize_database, populate_database from initdb import initialize_database, populate_database
@ -175,6 +176,7 @@ def app(appconfig, initialized_db):
app.register_blueprint(web, url_prefix='/') app.register_blueprint(web, url_prefix='/')
app.register_blueprint(verbs_bp, url_prefix='/c1') app.register_blueprint(verbs_bp, url_prefix='/c1')
app.register_blueprint(v2_bp, url_prefix='/v2') app.register_blueprint(v2_bp, url_prefix='/v2')
app.register_blueprint(webhooks, url_prefix='/webhooks')
app.config.update(appconfig) app.config.update(appconfig)
return app return app