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

528 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 TriggerException, EmptyRepositoryException
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()
except TriggerException as ex:
2014-03-14 16:11:48 +00:00
# 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 TriggerException 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']
except TriggerException 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 TriggerException 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 TriggerException 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 TriggerException 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 TriggerException as rre:
2015-04-24 22:36:48 +00:00
raise InvalidRequest(rre.message)
2014-03-14 16:11:48 +00:00
else:
raise Unauthorized()