This repository has been archived on 2020-03-24. You can view files and clone it, but cannot push or open issues or pull requests.
quay/endpoints/api/trigger.py

530 lines
19 KiB
Python
Raw Normal View History

2015-05-14 20:47:38 +00:00
""" Create, list and manage build triggers. """
2014-03-14 16:11:48 +00:00
import logging
from os import path
2014-03-14 16:11:48 +00:00
from urllib import quote
from urlparse import urlunparse
2016-03-09 21:20:28 +00:00
from flask import request, url_for
2014-03-14 16:11:48 +00:00
from app import app
from auth.permissions import (UserAdminPermission, AdministerOrganizationPermission,
ReadRepositoryPermission, AdministerRepositoryPermission)
from buildtrigger.basehandler import BuildTriggerHandler
from buildtrigger.triggerutil import (TriggerDeactivationException,
TriggerActivationException, EmptyRepositoryException,
RepositoryReadException, TriggerStartException)
from data import model
from data.model.build import update_build_trigger
2014-03-14 16:11:48 +00:00
from endpoints.api import (RepositoryParamResource, nickname, resource, require_repo_admin,
log_action, request_error, query_param, parse_args, internal_only,
validate_json_request, api, path_param, abort,
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,
BuildTriggerDisabledException)
from endpoints.exception import NotFound, Unauthorized, InvalidRequest
from util.names import parse_robot_username
2014-03-14 16:11:48 +00:00
logger = logging.getLogger(__name__)
def _prepare_webhook_url(scheme, username, password, hostname, path):
auth_hostname = '%s:%s@%s' % (username, password, hostname)
2014-03-14 16:11:48 +00:00
return urlunparse((scheme, auth_hostname, path, '', '', ''))
def get_trigger(trigger_uuid):
try:
trigger = model.build.get_build_trigger(trigger_uuid)
except model.InvalidBuildTriggerException:
raise NotFound()
return trigger
@resource('/v1/repository/<apirepopath:repository>/trigger/')
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
2014-03-14 16:11:48 +00:00
class BuildTriggerList(RepositoryParamResource):
""" Resource for listing repository build triggers. """
@require_repo_admin
@disallow_for_app_repositories
2014-03-14 16:11:48 +00:00
@nickname('listBuildTriggers')
2016-03-09 21:20:28 +00:00
def get(self, namespace_name, repo_name):
2014-03-14 16:11:48 +00:00
""" List the triggers for the specified repository. """
2016-03-09 21:20:28 +00:00
triggers = model.build.list_build_triggers(namespace_name, repo_name)
2014-03-14 16:11:48 +00:00
return {
2015-04-22 18:30:06 +00:00
'triggers': [trigger_view(trigger, can_admin=True) for trigger in triggers]
2014-03-14 16:11:48 +00:00
}
@resource('/v1/repository/<apirepopath:repository>/trigger/<trigger_uuid>')
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
@path_param('trigger_uuid', 'The UUID of the build trigger')
2014-03-14 16:11:48 +00:00
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',
},
}
},
}
2014-03-14 16:11:48 +00:00
@require_repo_admin
@disallow_for_app_repositories
2014-03-14 16:11:48 +00:00
@nickname('getBuildTrigger')
2016-03-09 21:20:28 +00:00
def get(self, namespace_name, repo_name, trigger_uuid):
2014-03-14 16:11:48 +00:00
""" Get information for the specified build trigger. """
return trigger_view(get_trigger(trigger_uuid), can_admin=True)
2014-03-14 16:11:48 +00:00
@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)
2014-03-14 16:11:48 +00:00
@require_repo_admin
@disallow_for_app_repositories
2014-03-14 16:11:48 +00:00
@nickname('deleteBuildTrigger')
2016-03-09 21:20:28 +00:00
def delete(self, namespace_name, repo_name, trigger_uuid):
2014-03-14 16:11:48 +00:00
""" Delete the specified build trigger. """
trigger = get_trigger(trigger_uuid)
2014-03-14 16:11:48 +00:00
2015-04-24 22:36:48 +00:00
handler = BuildTriggerHandler.get_handler(trigger)
if handler.is_active():
2014-03-14 16:11:48 +00:00
try:
2015-04-24 22:36:48 +00:00
handler.deactivate()
2014-03-14 16:11:48 +00:00
except TriggerDeactivationException as ex:
# We are just going to eat this error
logger.warning('Trigger deactivation problem: %s', ex)
2016-03-09 21:20:28 +00:00
log_action('delete_repo_trigger', namespace_name,
{'repo': repo_name, 'trigger_id': trigger_uuid,
2015-04-24 22:36:48 +00:00
'service': trigger.service.name},
2016-03-09 21:20:28 +00:00
repo=model.repository.get_repository(namespace_name, repo_name))
2014-03-14 16:11:48 +00:00
trigger.delete_instance(recursive=True)
if trigger.write_token is not None:
trigger.write_token.delete_instance()
2014-03-14 16:11:48 +00:00
return 'No Content', 204
@resource('/v1/repository/<apirepopath:repository>/trigger/<trigger_uuid>/subdir')
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
@path_param('trigger_uuid', 'The UUID of the build trigger')
@internal_only
2014-03-14 16:11:48 +00:00
class BuildTriggerSubdirs(RepositoryParamResource):
""" Custom verb for fetching the subdirs which are buildable for a trigger. """
schemas = {
'BuildTriggerSubdirRequest': {
'type': 'object',
'description': 'Arbitrary json.',
},
}
@require_repo_admin
@disallow_for_app_repositories
2014-03-14 16:11:48 +00:00
@nickname('listBuildTriggerSubdirs')
@validate_json_request('BuildTriggerSubdirRequest')
2016-03-09 21:20:28 +00:00
def post(self, namespace_name, repo_name, trigger_uuid):
2014-03-14 16:11:48 +00:00
""" List the subdirectories available for the specified build trigger and source. """
trigger = get_trigger(trigger_uuid)
2014-03-14 16:11:48 +00:00
user_permission = UserAdminPermission(trigger.connected_user.username)
2014-03-14 16:11:48 +00:00
if user_permission.can():
new_config_dict = request.get_json()
2015-04-24 22:36:48 +00:00
handler = BuildTriggerHandler.get_handler(trigger, new_config_dict)
2014-03-14 16:11:48 +00:00
try:
2015-04-24 22:36:48 +00:00
subdirs = handler.list_build_subdirs()
context_map = {}
for file in subdirs:
context_map = handler.get_parent_directory_mappings(file, context_map)
2014-03-14 16:11:48 +00:00
return {
'dockerfile_paths': ['/' + subdir for subdir in subdirs],
'contextMap': context_map,
'status': 'success',
2014-03-14 16:11:48 +00:00
}
except EmptyRepositoryException as exc:
return {
'status': 'success',
'contextMap': {},
'dockerfile_paths': [],
}
except RepositoryReadException as exc:
2014-03-14 16:11:48 +00:00
return {
'status': 'error',
'message': exc.message,
2014-03-14 16:11:48 +00:00
}
else:
raise Unauthorized()
2014-03-14 16:11:48 +00:00
@resource('/v1/repository/<apirepopath:repository>/trigger/<trigger_uuid>/activate')
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
@path_param('trigger_uuid', 'The UUID of the build trigger')
2014-03-14 16:11:48 +00:00
class BuildTriggerActivate(RepositoryParamResource):
""" Custom verb for activating a build trigger once all required information has been collected.
"""
schemas = {
'BuildTriggerActivateRequest': {
'type': 'object',
'required': [
'config'
],
'properties': {
'config': {
'type': 'object',
'description': 'Arbitrary json.',
},
'pull_robot': {
'type': 'string',
'description': 'The name of the robot that will be used to pull images.'
}
}
2014-03-14 16:11:48 +00:00
},
}
@require_repo_admin
@disallow_for_app_repositories
2014-03-14 16:11:48 +00:00
@nickname('activateBuildTrigger')
@validate_json_request('BuildTriggerActivateRequest')
2016-03-09 21:20:28 +00:00
def post(self, namespace_name, repo_name, trigger_uuid):
2014-03-14 16:11:48 +00:00
""" Activate the specified build trigger. """
trigger = get_trigger(trigger_uuid)
2015-04-24 22:36:48 +00:00
handler = BuildTriggerHandler.get_handler(trigger)
if handler.is_active():
raise InvalidRequest('Trigger config is not sufficient for activation.')
2014-03-14 16:11:48 +00:00
user_permission = UserAdminPermission(trigger.connected_user.username)
2014-03-14 16:11:48 +00:00
if user_permission.can():
# Update the pull robot (if any).
pull_robot_name = request.get_json().get('pull_robot', None)
if pull_robot_name:
try:
pull_robot = model.user.lookup_robot(pull_robot_name)
except model.InvalidRobotException:
raise NotFound()
# Make sure the user has administer permissions for the robot's namespace.
(robot_namespace, _) = parse_robot_username(pull_robot_name)
if not AdministerOrganizationPermission(robot_namespace).can():
raise Unauthorized()
# Make sure the namespace matches that of the trigger.
2016-03-09 21:20:28 +00:00
if robot_namespace != namespace_name:
2014-11-24 21:07:38 +00:00
raise Unauthorized()
# Set the pull robot.
trigger.pull_robot = pull_robot
# Update the config.
new_config_dict = request.get_json()['config']
2014-03-14 16:11:48 +00:00
write_token_name = 'Build Trigger: %s' % trigger.service.name
2016-03-09 21:20:28 +00:00
write_token = model.token.create_delegate_token(namespace_name, repo_name, write_token_name,
'write')
2014-03-14 16:11:48 +00:00
try:
path = url_for('webhooks.build_trigger_webhook', trigger_uuid=trigger.uuid)
authed_url = _prepare_webhook_url(app.config['PREFERRED_URL_SCHEME'],
'$token', write_token.code,
app.config['SERVER_HOSTNAME'], path)
2014-03-14 16:11:48 +00:00
2015-04-24 22:36:48 +00:00
handler = BuildTriggerHandler.get_handler(trigger, new_config_dict)
final_config, private_config = handler.activate(authed_url)
if 'private_key' in private_config:
trigger.private_key = private_config['private_key']
2014-03-14 16:11:48 +00:00
except TriggerActivationException as exc:
write_token.delete_instance()
raise request_error(message=exc.message)
2014-03-14 16:11:48 +00:00
# Save the updated config.
update_build_trigger(trigger, final_config, write_token=write_token)
2014-03-14 16:11:48 +00:00
# Log the trigger setup.
2016-03-09 21:20:28 +00:00
repo = model.repository.get_repository(namespace_name, repo_name)
log_action('setup_repo_trigger', namespace_name,
{'repo': repo_name, 'namespace': namespace_name,
'trigger_id': trigger.uuid, 'service': trigger.service.name,
'pull_robot': trigger.pull_robot.username if trigger.pull_robot else None,
'config': final_config},
repo=repo)
2014-03-14 16:11:48 +00:00
2015-04-22 18:30:06 +00:00
return trigger_view(trigger, can_admin=True)
2014-03-14 16:11:48 +00:00
else:
raise Unauthorized()
2014-03-14 16:11:48 +00:00
@resource('/v1/repository/<apirepopath:repository>/trigger/<trigger_uuid>/analyze')
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
@path_param('trigger_uuid', 'The UUID of the build trigger')
@internal_only
class BuildTriggerAnalyze(RepositoryParamResource):
""" Custom verb for analyzing the config for a build trigger and suggesting various changes
(such as a robot account to use for pulling)
"""
schemas = {
'BuildTriggerAnalyzeRequest': {
'type': 'object',
'required': [
'config'
],
'properties': {
'config': {
'type': 'object',
'description': 'Arbitrary json.',
}
}
},
}
@require_repo_admin
@disallow_for_app_repositories
@nickname('analyzeBuildTrigger')
@validate_json_request('BuildTriggerAnalyzeRequest')
2016-03-09 21:20:28 +00:00
def post(self, namespace_name, repo_name, trigger_uuid):
""" Analyze the specified build trigger configuration. """
trigger = get_trigger(trigger_uuid)
if trigger.repository.namespace_user.username != namespace_name:
raise NotFound()
if trigger.repository.name != repo_name:
raise NotFound()
new_config_dict = request.get_json()['config']
2015-04-24 22:36:48 +00:00
handler = BuildTriggerHandler.get_handler(trigger, new_config_dict)
server_hostname = app.config['SERVER_HOSTNAME']
try:
trigger_analyzer = TriggerAnalyzer(handler,
namespace_name,
server_hostname,
new_config_dict,
AdministerOrganizationPermission(namespace_name).can())
return trigger_analyzer.analyze_trigger()
except RepositoryReadException as rre:
return {
'status': 'error',
'message': 'Could not analyze the repository: %s' % rre.message,
}
2015-04-06 18:53:54 +00:00
except NotImplementedError:
return {
'status': 'notimplemented',
}
@resource('/v1/repository/<apirepopath:repository>/trigger/<trigger_uuid>/start')
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
@path_param('trigger_uuid', 'The UUID of the build trigger')
2014-03-14 16:11:48 +00:00
class ActivateBuildTrigger(RepositoryParamResource):
""" Custom verb to manually activate a build trigger. """
schemas = {
'RunParameters': {
'type': 'object',
'description': 'Optional run parameters for activating the build trigger',
'properties': {
'branch_name': {
'type': 'string',
2015-07-29 22:25:44 +00:00
'description': '(SCM only) If specified, the name of the branch to build.'
2015-04-03 21:10:57 +00:00
},
'commit_sha': {
'type': 'string',
'description': '(Custom Only) If specified, the ref/SHA1 used to checkout a git repository.'
},
'refs': {
'type': ['object', 'null'],
'description': '(SCM Only) If specified, the ref to build.'
}
2015-07-29 22:25:44 +00:00
},
'additionalProperties': False
}
}
2014-03-14 16:11:48 +00:00
@require_repo_admin
@disallow_for_app_repositories
2014-03-14 16:11:48 +00:00
@nickname('manuallyStartBuildTrigger')
@validate_json_request('RunParameters')
2016-03-09 21:20:28 +00:00
def post(self, namespace_name, repo_name, trigger_uuid):
2014-03-14 16:11:48 +00:00
""" Manually start a build from the specified trigger. """
trigger = get_trigger(trigger_uuid)
if not trigger.enabled:
raise InvalidRequest('Trigger is not enabled.')
2014-03-14 16:11:48 +00:00
2015-04-24 22:36:48 +00:00
handler = BuildTriggerHandler.get_handler(trigger)
if not handler.is_active():
raise InvalidRequest('Trigger is not active.')
2014-03-14 16:11:48 +00:00
try:
2016-03-09 21:20:28 +00:00
repo = model.repository.get_repository(namespace_name, repo_name)
pull_robot_name = model.build.get_pull_robot_name(trigger)
2014-03-14 16:11:48 +00:00
run_parameters = request.get_json()
prepared = handler.manual_start(run_parameters=run_parameters)
build_request = start_build(repo, prepared, pull_robot_name=pull_robot_name)
except TriggerStartException as tse:
raise InvalidRequest(tse.message)
2016-12-05 21:07:00 +00:00
except MaximumBuildsQueuedException:
abort(429, message='Maximum queued build rate exceeded.')
except BuildTriggerDisabledException:
abort(400, message='Build trigger is disabled')
2014-03-14 16:11:48 +00:00
2015-04-30 19:33:19 +00:00
resp = build_status_view(build_request)
2016-03-09 21:20:28 +00:00
repo_string = '%s/%s' % (namespace_name, repo_name)
2014-03-14 16:11:48 +00:00
headers = {
2014-03-17 19:23:49 +00:00
'Location': api.url_for(RepositoryBuildStatus, repository=repo_string,
build_uuid=build_request.uuid),
2014-03-14 16:11:48 +00:00
}
return resp, 201, headers
@resource('/v1/repository/<apirepopath:repository>/trigger/<trigger_uuid>/builds')
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
@path_param('trigger_uuid', 'The UUID of the build trigger')
2014-03-14 16:11:48 +00:00
class TriggerBuildList(RepositoryParamResource):
""" Resource to represent builds that were activated from the specified trigger. """
@require_repo_admin
@disallow_for_app_repositories
@parse_args()
2014-03-14 16:11:48 +00:00
@query_param('limit', 'The maximum number of builds to return', type=int, default=5)
@nickname('listTriggerRecentBuilds')
2016-03-09 21:20:28 +00:00
def get(self, namespace_name, repo_name, trigger_uuid, parsed_args):
2014-03-14 16:11:48 +00:00
""" List the builds started by the specified trigger. """
limit = parsed_args['limit']
2016-03-09 21:20:28 +00:00
builds = model.build.list_trigger_builds(namespace_name, repo_name, trigger_uuid, limit)
2014-03-14 16:11:48 +00:00
return {
'builds': [build_status_view(bld) for bld in builds]
2014-03-14 16:11:48 +00:00
}
FIELD_VALUE_LIMIT = 30
@resource('/v1/repository/<apirepopath: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
@disallow_for_app_repositories
@nickname('listTriggerFieldValues')
2016-03-09 21:20:28 +00:00
def post(self, namespace_name, repo_name, trigger_uuid, field_name):
""" List the field values for a custom run field. """
trigger = get_trigger(trigger_uuid)
2015-04-24 22:36:48 +00:00
config = request.get_json() or None
if AdministerRepositoryPermission(namespace_name, repo_name).can():
2015-04-24 22:36:48 +00:00
handler = BuildTriggerHandler.get_handler(trigger, config)
values = handler.list_field_values(field_name, limit=FIELD_VALUE_LIMIT)
if values is None:
raise NotFound()
2014-11-24 21:07:38 +00:00
return {
'values': values
}
else:
raise Unauthorized()
@resource('/v1/repository/<apirepopath:repository>/trigger/<trigger_uuid>/sources')
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
@path_param('trigger_uuid', 'The UUID of the build trigger')
@internal_only
2014-03-14 16:11:48 +00:00
class BuildTriggerSources(RepositoryParamResource):
""" Custom verb to fetch the list of build sources for the trigger config. """
schemas = {
'BuildTriggerSourcesRequest': {
'type': 'object',
'description': 'Specifies the namespace under which to fetch sources',
'properties': {
'namespace': {
'type': 'string',
'description': 'The namespace for which to fetch sources'
},
},
}
}
2014-03-14 16:11:48 +00:00
@require_repo_admin
@disallow_for_app_repositories
2014-03-14 16:11:48 +00:00
@nickname('listTriggerBuildSources')
@validate_json_request('BuildTriggerSourcesRequest')
def post(self, namespace_name, repo_name, trigger_uuid):
""" List the build sources for the trigger configuration thus far. """
namespace = request.get_json()['namespace']
trigger = get_trigger(trigger_uuid)
user_permission = UserAdminPermission(trigger.connected_user.username)
if user_permission.can():
handler = BuildTriggerHandler.get_handler(trigger)
try:
return {
'sources': handler.list_build_sources_for_namespace(namespace)
}
except RepositoryReadException as rre:
raise InvalidRequest(rre.message)
else:
raise Unauthorized()
@resource('/v1/repository/<apirepopath:repository>/trigger/<trigger_uuid>/namespaces')
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
@path_param('trigger_uuid', 'The UUID of the build trigger')
@internal_only
class BuildTriggerSourceNamespaces(RepositoryParamResource):
""" Custom verb to fetch the list of namespaces (orgs, projects, etc) for the trigger config. """
@require_repo_admin
@disallow_for_app_repositories
@nickname('listTriggerBuildSourceNamespaces')
2016-03-09 21:20:28 +00:00
def get(self, namespace_name, repo_name, trigger_uuid):
2014-03-14 16:11:48 +00:00
""" List the build sources for the trigger configuration thus far. """
trigger = get_trigger(trigger_uuid)
2014-03-14 16:11:48 +00:00
user_permission = UserAdminPermission(trigger.connected_user.username)
2014-03-14 16:11:48 +00:00
if user_permission.can():
2015-04-24 22:36:48 +00:00
handler = BuildTriggerHandler.get_handler(trigger)
2014-11-24 21:07:38 +00:00
2015-04-24 22:36:48 +00:00
try:
return {
'namespaces': handler.list_build_source_namespaces()
2015-04-24 22:36:48 +00:00
}
except RepositoryReadException as rre:
raise InvalidRequest(rre.message)
2014-03-14 16:11:48 +00:00
else:
raise Unauthorized()