Work in progress: bitbucket support

This commit is contained in:
Joseph Schorr 2015-04-24 18:36:48 -04:00
parent c480fb2105
commit 5cc91ed202
11 changed files with 352 additions and 259 deletions

View file

@ -11,7 +11,7 @@ from endpoints.api import (RepositoryParamResource, parse_args, query_param, nic
ApiResource, internal_only, format_date, api, Unauthorized, NotFound, ApiResource, internal_only, format_date, api, Unauthorized, NotFound,
path_param, InvalidRequest, require_repo_admin) path_param, InvalidRequest, require_repo_admin)
from endpoints.common import start_build from endpoints.common import start_build
from endpoints.trigger import BuildTrigger from endpoints.trigger import BuildTriggerHandler
from data import model, database from data import model, database
from auth.auth_context import get_authenticated_user from auth.auth_context import get_authenticated_user
from auth.permissions import ModifyRepositoryPermission, AdministerOrganizationPermission from auth.permissions import ModifyRepositoryPermission, AdministerOrganizationPermission
@ -45,14 +45,13 @@ def user_view(user):
def trigger_view(trigger, can_admin=False): def trigger_view(trigger, can_admin=False):
if trigger and trigger.uuid: if trigger and trigger.uuid:
config_dict = get_trigger_config(trigger) build_trigger = BuildTriggerHandler.get_handler(trigger)
build_trigger = BuildTrigger.get_trigger_for_service(trigger.service.name)
return { return {
'service': trigger.service.name, 'service': trigger.service.name,
'config': config_dict if can_admin else {}, 'config': build_trigger.config if can_admin else {},
'id': trigger.uuid, 'id': trigger.uuid,
'connected_user': trigger.connected_user.username, 'connected_user': trigger.connected_user.username,
'is_active': build_trigger.is_active(config_dict), 'is_active': build_trigger.is_active(),
'pull_robot': user_view(trigger.pull_robot) if trigger.pull_robot else None 'pull_robot': user_view(trigger.pull_robot) if trigger.pull_robot else None
} }

View file

@ -13,7 +13,7 @@ from endpoints.api import (RepositoryParamResource, nickname, resource, require_
from endpoints.api.build import (build_status_view, trigger_view, RepositoryBuildStatus, from endpoints.api.build import (build_status_view, trigger_view, RepositoryBuildStatus,
get_trigger_config) get_trigger_config)
from endpoints.common import start_build from endpoints.common import start_build
from endpoints.trigger import (BuildTrigger as BuildTriggerBase, TriggerDeactivationException, from endpoints.trigger import (BuildTriggerHandler, TriggerDeactivationException,
TriggerActivationException, EmptyRepositoryException, TriggerActivationException, EmptyRepositoryException,
RepositoryReadException, TriggerStartException) RepositoryReadException, TriggerStartException)
from data import model from data import model
@ -71,18 +71,17 @@ class BuildTrigger(RepositoryParamResource):
except model.InvalidBuildTriggerException: except model.InvalidBuildTriggerException:
raise NotFound() raise NotFound()
handler = BuildTriggerBase.get_trigger_for_service(trigger.service.name) handler = BuildTriggerHandler.get_handler(trigger)
config_dict = get_trigger_config(trigger) if handler.is_active():
if handler.is_active(config_dict):
try: try:
handler.deactivate(trigger.auth_token, config_dict) handler.deactivate()
except TriggerDeactivationException as ex: except TriggerDeactivationException as ex:
# We are just going to eat this error # We are just going to eat this error
logger.warning('Trigger deactivation problem: %s', ex) logger.warning('Trigger deactivation problem: %s', ex)
log_action('delete_repo_trigger', namespace, log_action('delete_repo_trigger', namespace,
{'repo': repository, 'trigger_id': trigger_uuid, {'repo': repository, 'trigger_id': trigger_uuid,
'service': trigger.service.name, 'config': config_dict}, 'service': trigger.service.name},
repo=model.get_repository(namespace, repository)) repo=model.get_repository(namespace, repository))
trigger.delete_instance(recursive=True) trigger.delete_instance(recursive=True)
@ -117,13 +116,13 @@ class BuildTriggerSubdirs(RepositoryParamResource):
except model.InvalidBuildTriggerException: except model.InvalidBuildTriggerException:
raise NotFound() raise NotFound()
handler = BuildTriggerBase.get_trigger_for_service(trigger.service.name)
user_permission = UserAdminPermission(trigger.connected_user.username) user_permission = UserAdminPermission(trigger.connected_user.username)
if user_permission.can(): if user_permission.can():
new_config_dict = request.get_json() new_config_dict = request.get_json()
handler = BuildTriggerHandler.get_handler(trigger, new_config_dict)
try: try:
subdirs = handler.list_build_subdirs(trigger.auth_token, new_config_dict) subdirs = handler.list_build_subdirs()
return { return {
'subdir': subdirs, 'subdir': subdirs,
'status': 'success' 'status': 'success'
@ -178,9 +177,8 @@ class BuildTriggerActivate(RepositoryParamResource):
except model.InvalidBuildTriggerException: except model.InvalidBuildTriggerException:
raise NotFound() raise NotFound()
handler = BuildTriggerBase.get_trigger_for_service(trigger.service.name) handler = BuildTriggerHandler.get_handler(trigger)
existing_config_dict = get_trigger_config(trigger) if handler.is_active():
if handler.is_active(existing_config_dict):
raise InvalidRequest('Trigger config is not sufficient for activation.') raise InvalidRequest('Trigger config is not sufficient for activation.')
user_permission = UserAdminPermission(trigger.connected_user.username) user_permission = UserAdminPermission(trigger.connected_user.username)
@ -217,8 +215,8 @@ class BuildTriggerActivate(RepositoryParamResource):
'$token', write_token.code, '$token', write_token.code,
app.config['SERVER_HOSTNAME'], path) app.config['SERVER_HOSTNAME'], path)
final_config, private_config = handler.activate(trigger.uuid, authed_url, handler = BuildTriggerHandler.get_handler(trigger, new_config_dict)
trigger.auth_token, new_config_dict) final_config, private_config = handler.activate(authed_url)
if 'private_key' in private_config: if 'private_key' in private_config:
trigger.private_key = private_config['private_key'] trigger.private_key = private_config['private_key']
@ -279,12 +277,12 @@ class BuildTriggerAnalyze(RepositoryParamResource):
except model.InvalidBuildTriggerException: except model.InvalidBuildTriggerException:
raise NotFound() raise NotFound()
handler = BuildTriggerBase.get_trigger_for_service(trigger.service.name)
new_config_dict = request.get_json()['config'] new_config_dict = request.get_json()['config']
handler = BuildTriggerHandler.get_handler(trigger, new_config_dict)
try: try:
# Load the contents of the Dockerfile. # Load the contents of the Dockerfile.
contents = handler.load_dockerfile_contents(trigger.auth_token, new_config_dict) contents = handler.load_dockerfile_contents()
if not contents: if not contents:
return { return {
'status': 'error', 'status': 'error',
@ -370,7 +368,7 @@ class BuildTriggerAnalyze(RepositoryParamResource):
'is_public': found_repository.visibility.name == 'public', 'is_public': found_repository.visibility.name == 'public',
'robots': read_robots, 'robots': read_robots,
'status': 'analyzed', 'status': 'analyzed',
'dockerfile_url': handler.dockerfile_url(trigger.auth_token, new_config_dict) 'dockerfile_url': handler.dockerfile_url()
} }
except RepositoryReadException as rre: except RepositoryReadException as rre:
@ -420,14 +418,13 @@ class ActivateBuildTrigger(RepositoryParamResource):
except model.InvalidBuildTriggerException: except model.InvalidBuildTriggerException:
raise NotFound() raise NotFound()
handler = BuildTriggerBase.get_trigger_for_service(trigger.service.name) handler = BuildTriggerHandler.get_handler(trigger)
config_dict = get_trigger_config(trigger) if not handler.is_active():
if not handler.is_active(config_dict):
raise InvalidRequest('Trigger is not active.') raise InvalidRequest('Trigger is not active.')
try: try:
run_parameters = request.get_json() run_parameters = request.get_json()
specs = handler.manual_start(trigger, run_parameters=run_parameters) specs = handler.manual_start(run_parameters=run_parameters)
dockerfile_id, tags, name, subdir, metadata = specs dockerfile_id, tags, name, subdir, metadata = specs
repo = model.get_repository(namespace, repository) repo = model.get_repository(namespace, repository)
@ -481,11 +478,11 @@ class BuildTriggerFieldValues(RepositoryParamResource):
except model.InvalidBuildTriggerException: except model.InvalidBuildTriggerException:
raise NotFound() raise NotFound()
config = request.get_json() or json.loads(trigger.config) config = request.get_json() or None
user_permission = UserAdminPermission(trigger.connected_user.username) user_permission = UserAdminPermission(trigger.connected_user.username)
if user_permission.can(): if user_permission.can():
trigger_handler = BuildTriggerBase.get_trigger_for_service(trigger.service.name) handler = BuildTriggerHandler.get_handler(trigger, config)
values = trigger_handler.list_field_values(trigger.auth_token, config, field_name) values = handler.list_field_values(field_name)
if values is None: if values is None:
raise NotFound() raise NotFound()
@ -514,10 +511,13 @@ class BuildTriggerSources(RepositoryParamResource):
user_permission = UserAdminPermission(trigger.connected_user.username) user_permission = UserAdminPermission(trigger.connected_user.username)
if user_permission.can(): if user_permission.can():
trigger_handler = BuildTriggerBase.get_trigger_for_service(trigger.service.name) handler = BuildTriggerHandler.get_handler(trigger)
return { try:
'sources': trigger_handler.list_build_sources(trigger.auth_token) return {
} 'sources': handler.list_build_sources()
}
except RepositoryReadException as rre:
raise InvalidRequest(rre.message)
else: else:
raise Unauthorized() raise Unauthorized()

View file

@ -3,7 +3,7 @@ import logging
from flask import request, redirect, url_for, Blueprint from flask import request, redirect, url_for, Blueprint
from flask.ext.login import current_user from flask.ext.login import current_user
from endpoints.trigger import BitbucketBuildTrigger from endpoints.trigger import BitbucketBuildTrigger, BuildTriggerHandler
from endpoints.common import route_show_if from endpoints.common import route_show_if
from app import app from app import app
from data import model from data import model
@ -30,14 +30,18 @@ def attach_bitbucket_build_trigger(trigger_uuid):
abort(404) abort(404)
verifier = request.args.get('oauth_verifier') verifier = request.args.get('oauth_verifier')
result = BitbucketBuildTrigger.exchange_verifier(trigger, verifier) handler = BuildTriggerHandler.get_handler(trigger)
print result result = handler.exchange_verifier(verifier)
return 'hello' if not result:
trigger.delete_instance()
return 'Token has expired'
namespace = trigger.repository.namespace_user.username
repository = trigger.repository.name
repo_path = '%s/%s' % (namespace, repository) repo_path = '%s/%s' % (namespace, repository)
full_url = '%s%s%s' % (url_for('web.repository', path=repo_path), '?tab=builds&newtrigger=', full_url = '%s%s%s' % (url_for('web.repository', path=repo_path), '?tab=builds&newtrigger=',
trigger.uuid) trigger.uuid)
logger.debug('Redirecting to full url: %s', full_url) logger.debug('Redirecting to full url: %s', full_url)
return redirect(full_url) return redirect(full_url)

View file

@ -10,6 +10,7 @@ from github import Github, UnknownObjectException, GithubException
from bitbucket.bitbucket import Bitbucket from bitbucket.bitbucket import Bitbucket
from tempfile import SpooledTemporaryFile from tempfile import SpooledTemporaryFile
from jsonschema import validate from jsonschema import validate
from data import model
from app import app, userfiles as user_files, github_trigger, get_app_url from app import app, userfiles as user_files, github_trigger, get_app_url
from util.tarfileappender import TarfileAppender from util.tarfileappender import TarfileAppender
@ -63,59 +64,75 @@ class TriggerProviderException(Exception):
pass pass
class BuildTrigger(object): def raise_unsupported():
def __init__(self): raise io.UnsupportedOperation
pass
def dockerfile_url(self, auth_token, config): def get_trigger_config(trigger):
try:
return json.loads(trigger.config)
except:
return {}
class BuildTriggerHandler(object):
def __init__(self, trigger, override_config=None):
self.trigger = trigger
self.config = override_config or get_trigger_config(trigger)
@property
def auth_token(self):
""" Returns the auth token for the trigger. """
return self.trigger.auth_token
def dockerfile_url(self):
""" """
Returns the URL at which the Dockerfile for the trigger is found or None if none/not applicable. Returns the URL at which the Dockerfile for the trigger is found or None if none/not applicable.
""" """
raise NotImplementedError raise NotImplementedError
def load_dockerfile_contents(self, auth_token, config): def load_dockerfile_contents(self):
""" """
Loads the Dockerfile found for the trigger's config and returns them or None if none could Loads the Dockerfile found for the trigger's config and returns them or None if none could
be found/loaded. be found/loaded.
""" """
raise NotImplementedError raise NotImplementedError
def list_build_sources(self, auth_token): def list_build_sources(self):
""" """
Take the auth information for the specific trigger type and load the Take the auth information for the specific trigger type and load the
list of build sources(repositories). list of build sources(repositories).
""" """
raise NotImplementedError raise NotImplementedError
def list_build_subdirs(self, auth_token, config): def list_build_subdirs(self):
""" """
Take the auth information and the specified config so far and list all of Take the auth information and the specified config so far and list all of
the possible subdirs containing dockerfiles. the possible subdirs containing dockerfiles.
""" """
raise NotImplementedError raise NotImplementedError
def handle_trigger_request(self, request, trigger): def handle_trigger_request(self):
""" """
Transform the incoming request data into a set of actions. Returns a tuple Transform the incoming request data into a set of actions. Returns a tuple
of usefiles resource id, docker tags, build name, and resource subdir. of usefiles resource id, docker tags, build name, and resource subdir.
""" """
raise NotImplementedError raise NotImplementedError
def is_active(self, config): def is_active(self):
""" """
Returns True if the current build trigger is active. Inactive means further Returns True if the current build trigger is active. Inactive means further
setup is needed. setup is needed.
""" """
raise NotImplementedError raise NotImplementedError
def activate(self, trigger_uuid, standard_webhook_url, auth_token, config): def activate(self, standard_webhook_url):
""" """
Activates the trigger for the service, with the given new configuration. Activates the trigger for the service, with the given new configuration.
Returns new public and private config that should be stored if successful. Returns new public and private config that should be stored if successful.
""" """
raise NotImplementedError raise NotImplementedError
def deactivate(self, auth_token, config): def deactivate(self):
""" """
Deactivates the trigger for the service, removing any hooks installed in Deactivates the trigger for the service, removing any hooks installed in
the remote service. Returns the new config that should be stored if this the remote service. Returns the new config that should be stored if this
@ -123,13 +140,13 @@ class BuildTrigger(object):
""" """
raise NotImplementedError raise NotImplementedError
def manual_start(self, trigger, run_parameters=None): def manual_start(self, run_parameters=None):
""" """
Manually creates a repository build for this trigger. Manually creates a repository build for this trigger.
""" """
raise NotImplementedError raise NotImplementedError
def list_field_values(self, auth_token, config, field_name): def list_field_values(self, field_name):
""" """
Lists all values for the given custom trigger field. For example, a trigger might have a Lists all values for the given custom trigger field. For example, a trigger might have a
field named "branches", and this method would return all branches. field named "branches", and this method would return all branches.
@ -144,25 +161,24 @@ class BuildTrigger(object):
raise NotImplementedError raise NotImplementedError
@classmethod @classmethod
def get_trigger_for_service(cls, service): def get_handler(cls, trigger, override_config=None):
for subc in cls.__subclasses__(): for subc in cls.__subclasses__():
if subc.service_name() == service: if subc.service_name() == trigger.service.name:
return subc() return subc(trigger, override_config)
raise InvalidServiceException('Unable to find service: %s' % service) raise InvalidServiceException('Unable to find service: %s' % service)
def put_config_key(self, key, value):
""" Updates a config key in the trigger, saving it to the DB. """
self.config[key] = value
model.update_build_trigger(self.trigger, self.config)
def raise_unsupported(): def set_auth_token(self, auth_token):
raise io.UnsupportedOperation """ Sets the auth token for the trigger, saving it to the DB. """
model.update_build_trigger(self.trigger, self.config, auth_token=auth_token)
def get_trigger_config(trigger):
try:
return json.loads(trigger.config)
except:
return {}
class BitbucketBuildTrigger(BuildTrigger): class BitbucketBuildTrigger(BuildTriggerHandler):
""" """
BuildTrigger for Bitbucket. BuildTrigger for Bitbucket.
""" """
@ -170,23 +186,25 @@ class BitbucketBuildTrigger(BuildTrigger):
def service_name(cls): def service_name(cls):
return 'bitbucket' return 'bitbucket'
@staticmethod def _get_authorized_client(self, namespace=None):
def _get_authorized_client(trigger_uuid):
key = app.config.get('BITBUCKET_TRIGGER_CONFIG', {}).get('CONSUMER_KEY', '') key = app.config.get('BITBUCKET_TRIGGER_CONFIG', {}).get('CONSUMER_KEY', '')
secret = app.config.get('BITBUCKET_TRIGGER_CONFIG', {}).get('CONSUMER_SECRET', '') secret = app.config.get('BITBUCKET_TRIGGER_CONFIG', {}).get('CONSUMER_SECRET', '')
trigger_uuid = self.trigger.uuid
callback_url = '%s/oauth1/bitbucket/callback/trigger/%s' % (get_app_url(), trigger_uuid) callback_url = '%s/oauth1/bitbucket/callback/trigger/%s' % (get_app_url(), trigger_uuid)
bitbucket_client = Bitbucket()
(result, err_message) = bitbucket_client.authorize(key, secret, callback_url) bitbucket_client = Bitbucket(username=namespace or self.config.get('username', ''))
(result, err_message) = bitbucket_client.authorize(key, secret, callback_url,
access_token=self.config.get('access_token'),
access_token_secret=self.auth_token)
if not result: if not result:
raise TriggerProviderException(err_message) raise TriggerProviderException(err_message)
return bitbucket_client return bitbucket_client
def get_oauth_url(self):
@staticmethod bitbucket_client = self._get_authorized_client()
def get_oauth_url(trigger_uuid):
bitbucket_client = BitbucketBuildTrigger._get_authorized_client(trigger_uuid)
url = bitbucket_client.url('AUTHENTICATE', token=bitbucket_client.access_token) url = bitbucket_client.url('AUTHENTICATE', token=bitbucket_client.access_token)
return { return {
'access_token': bitbucket_client.access_token, 'access_token': bitbucket_client.access_token,
@ -194,62 +212,104 @@ class BitbucketBuildTrigger(BuildTrigger):
'url': url 'url': url
} }
@staticmethod def exchange_verifier(self, verifier):
def exchange_verifier(trigger, verifier): bitbucket_client = self._get_authorized_client()
trigger_config = get_trigger_config(trigger.config) (result, data) = bitbucket_client.verify(verifier,
bitbucket_client = BitbucketBuildTrigger._get_authorized_client(trigger.uuid) access_token=self.config.get('access_token', ''),
print trigger.config access_token_secret=self.auth_token)
print trigger.auth_token
print bitbucket_client.verify(verifier, access_token=trigger_config.get('access_token', ''),
access_token_secret=trigger.auth_token)
return None
#(result, _) = bitbucket_client.verify(verifier)
#if not result: if not result:
# return None return False
#return (bitbucket_client.access_token, bitbucket_client.access_token_secret) # Request the user's information and save it and the access token to the config.
user_url = bitbucket_client.URLS['BASE'] % 'user'
(result, data) = bitbucket_client.dispatch('GET', user_url, auth=bitbucket_client.auth)
if not result:
return False
def is_active(self, config): username = data['user']['username']
new_access_token = bitbucket_client.access_token
self.put_config_key('username', username)
self.put_config_key('access_token', new_access_token)
self.set_auth_token(bitbucket_client.access_token_secret)
return True
def is_active(self):
return False return False
def activate(self, trigger_uuid, standard_webhook_url, auth_token, config): def activate(self, standard_webhook_url):
return {} return {}
def deactivate(self, auth_token, config): def deactivate(self):
return config return self.config
def list_build_sources(self, auth_token): def list_build_sources(self):
bitbucket_client = self._get_authorized_client()
success, repositories = bitbucket_client.repository.all()
if not success:
raise RepositoryReadException('Could not read repository list')
namespaces = {}
for repo in repositories:
if not repo['scm'] == 'git':
continue
owner = repo['owner']
if not owner in namespaces:
namespaces[owner] = {
'personal': owner == self.config.get('username'),
'repos': [],
'info': {
'name': owner
}
}
namespaces[owner]['repos'].append(owner + '/' + repo['slug'])
return namespaces.values()
def list_build_subdirs(self):
source = self.config['build_source']
(namespace, name) = source.split('/')
(result, data) = self._get_authorized_client(namespace=namespace).repository.get(name)
print result
print data
return []
def dockerfile_url(self):
return None
def load_dockerfile_contents(self):
raise RepositoryReadException('Not supported')
def handle_trigger_request(self, request):
return
def manual_start(self, run_parameters=None):
return None
def list_field_values(self, field_name):
source = self.config['build_source']
(namespace, name) = source.split('/')
(result, data) = self._get_authorized_client(namespace=namespace).repository.get(name)
print result
print data
return [] return []
def list_build_subdirs(self, auth_token, config): class GithubBuildTrigger(BuildTriggerHandler):
raise RepositoryReadException('Not supported')
def dockerfile_url(self, auth_token, config):
return None
def load_dockerfile_contents(self, auth_token, config):
raise RepositoryReadException('Not supported')
@staticmethod
def _build_commit_info(repo, commit_sha):
return {}
def handle_trigger_request(self, request, trigger):
return
def manual_start(self, trigger, run_parameters=None):
return None
class GithubBuildTrigger(BuildTrigger):
""" """
BuildTrigger for GitHub that uses the archive API and buildpacks. BuildTrigger for GitHub that uses the archive API and buildpacks.
""" """
@staticmethod def _get_client(self):
def _get_client(auth_token): return Github(self.auth_token,
return Github(auth_token,
base_url=github_trigger.api_endpoint(), base_url=github_trigger.api_endpoint(),
client_id=github_trigger.client_id(), client_id=github_trigger.client_id(),
client_secret=github_trigger.client_secret()) client_secret=github_trigger.client_secret())
@ -258,12 +318,13 @@ class GithubBuildTrigger(BuildTrigger):
def service_name(cls): def service_name(cls):
return 'github' return 'github'
def is_active(self, config): def is_active(self):
return 'hook_id' in config return 'hook_id' in self.config
def activate(self, trigger_uuid, standard_webhook_url, auth_token, config): def activate(self, standard_webhook_url):
config = self.config
new_build_source = config['build_source'] new_build_source = config['build_source']
gh_client = self._get_client(auth_token) gh_client = self._get_client()
# Find the GitHub repository. # Find the GitHub repository.
try: try:
@ -303,8 +364,9 @@ class GithubBuildTrigger(BuildTrigger):
return config, {'private_key': private_key} return config, {'private_key': private_key}
def deactivate(self, auth_token, config): def deactivate(self):
gh_client = self._get_client(auth_token) config = self.config
gh_client = self._get_client()
# Find the GitHub repository. # Find the GitHub repository.
try: try:
@ -334,11 +396,11 @@ class GithubBuildTrigger(BuildTrigger):
raise TriggerDeactivationException(msg) raise TriggerDeactivationException(msg)
config.pop('hook_id', None) config.pop('hook_id', None)
self.config = config
return config return config
def list_build_sources(self, auth_token): def list_build_sources(self):
gh_client = self._get_client(auth_token) gh_client = self._get_client()
usr = gh_client.get_user() usr = gh_client.get_user()
personal = { personal = {
@ -380,8 +442,9 @@ class GithubBuildTrigger(BuildTrigger):
return len(m.group(0)) == len(match_string) return len(m.group(0)) == len(match_string)
def list_build_subdirs(self, auth_token, config): def list_build_subdirs(self):
gh_client = self._get_client(auth_token) config = self.config
gh_client = self._get_client()
source = config['build_source'] source = config['build_source']
try: try:
@ -411,11 +474,13 @@ class GithubBuildTrigger(BuildTrigger):
raise RepositoryReadException(message) raise RepositoryReadException(message)
def dockerfile_url(self, auth_token, config): def dockerfile_url(self):
config = self.config
source = config['build_source'] source = config['build_source']
subdirectory = config.get('subdir', '') subdirectory = config.get('subdir', '')
path = subdirectory + '/Dockerfile' if subdirectory else 'Dockerfile' path = subdirectory + '/Dockerfile' if subdirectory else 'Dockerfile'
gh_client = self._get_client(auth_token) gh_client = self._get_client()
try: try:
repo = gh_client.get_repo(source) repo = gh_client.get_repo(source)
@ -425,8 +490,9 @@ class GithubBuildTrigger(BuildTrigger):
logger.exception('Could not load repository for Dockerfile.') logger.exception('Could not load repository for Dockerfile.')
return None return None
def load_dockerfile_contents(self, auth_token, config): def load_dockerfile_contents(self):
gh_client = self._get_client(auth_token) config = self.config
gh_client = self._get_client()
source = config['build_source'] source = config['build_source']
subdirectory = config.get('subdir', '') subdirectory = config.get('subdir', '')
@ -556,7 +622,7 @@ class GithubBuildTrigger(BuildTrigger):
def get_display_name(sha): def get_display_name(sha):
return sha[0:7] return sha[0:7]
def handle_trigger_request(self, request, trigger): def handle_trigger_request(self, request):
payload = request.get_json() payload = request.get_json()
if not payload or payload.get('head_commit') is None: if not payload or payload.get('head_commit') is None:
raise SkipRequestException() raise SkipRequestException()
@ -570,7 +636,7 @@ class GithubBuildTrigger(BuildTrigger):
commit_message = payload['head_commit'].get('message', '') commit_message = payload['head_commit'].get('message', '')
git_url = payload['repository']['git_url'] git_url = payload['repository']['git_url']
config = get_trigger_config(trigger) config = self.config
if 'branchtag_regex' in config: if 'branchtag_regex' in config:
try: try:
regex = re.compile(config['branchtag_regex']) regex = re.compile(config['branchtag_regex'])
@ -585,7 +651,7 @@ class GithubBuildTrigger(BuildTrigger):
short_sha = GithubBuildTrigger.get_display_name(commit_sha) short_sha = GithubBuildTrigger.get_display_name(commit_sha)
gh_client = self._get_client(trigger.auth_token) gh_client = self._get_client()
repo_full_name = '%s/%s' % (payload['repository']['owner']['name'], repo_full_name = '%s/%s' % (payload['repository']['owner']['name'],
payload['repository']['name']) payload['repository']['name'])
@ -593,16 +659,16 @@ class GithubBuildTrigger(BuildTrigger):
logger.debug('Github repo: %s', repo) logger.debug('Github repo: %s', repo)
return GithubBuildTrigger._prepare_build(trigger, config, repo, commit_sha, return GithubBuildTrigger._prepare_build(self.trigger, config, repo, commit_sha, short_sha,
short_sha, ref, git_url) ref, git_url)
def manual_start(self, trigger, run_parameters=None): def manual_start(self, run_parameters=None):
config = get_trigger_config(trigger) config = self.config
try: try:
source = config['build_source'] source = config['build_source']
run_parameters = run_parameters or {} run_parameters = run_parameters or {}
gh_client = self._get_client(trigger.auth_token) gh_client = self._get_client()
repo = gh_client.get_repo(source) repo = gh_client.get_repo(source)
branch_name = run_parameters.get('branch_name') or repo.default_branch branch_name = run_parameters.get('branch_name') or repo.default_branch
branch = repo.get_branch(branch_name) branch = repo.get_branch(branch_name)
@ -611,27 +677,28 @@ class GithubBuildTrigger(BuildTrigger):
ref = 'refs/heads/%s' % (branch_name) ref = 'refs/heads/%s' % (branch_name)
git_url = repo.git_url git_url = repo.git_url
return self._prepare_build(trigger, config, repo, branch_sha, short_sha, ref, git_url) return self._prepare_build(self.trigger, config, repo, branch_sha, short_sha, ref, git_url)
except GithubException as ghe: except GithubException as ghe:
raise TriggerStartException(ghe.data['message']) raise TriggerStartException(ghe.data['message'])
def list_field_values(self, auth_token, config, field_name): def list_field_values(self, field_name):
if field_name == 'refs': if field_name == 'refs':
branches = self.list_field_values(auth_token, config, 'branch_name') branches = self.list_field_values('branch_name')
tags = self.list_field_values(auth_token, config, 'tag_name') tags = self.list_field_values('tag_name')
return ([{'kind': 'branch', 'name': b} for b in branches] + return ([{'kind': 'branch', 'name': b} for b in branches] +
[{'kind': 'tag', 'name': tag} for tag in tags]) [{'kind': 'tag', 'name': tag} for tag in tags])
config = self.config
if field_name == 'tag_name': if field_name == 'tag_name':
gh_client = self._get_client(auth_token) gh_client = self._get_client()
source = config['build_source'] source = config['build_source']
repo = gh_client.get_repo(source) repo = gh_client.get_repo(source)
return [tag.name for tag in repo.get_tags()] return [tag.name for tag in repo.get_tags()]
if field_name == 'branch_name': if field_name == 'branch_name':
gh_client = self._get_client(auth_token) gh_client = self._get_client()
source = config['build_source'] source = config['build_source']
repo = gh_client.get_repo(source) repo = gh_client.get_repo(source)
branches = [branch.name for branch in repo.get_branches()] branches = [branch.name for branch in repo.get_branches()]
@ -647,7 +714,7 @@ class GithubBuildTrigger(BuildTrigger):
return None return None
class CustomBuildTrigger(BuildTrigger): class CustomBuildTrigger(BuildTriggerHandler):
payload_schema = { payload_schema = {
'type': 'object', 'type': 'object',
'properties': { 'properties': {
@ -730,8 +797,8 @@ class CustomBuildTrigger(BuildTrigger):
def service_name(cls): def service_name(cls):
return 'custom-git' return 'custom-git'
def is_active(self, config): def is_active(self):
return config.has_key('credentials') return self.config.has_key('credentials')
def _metadata_from_payload(self, payload): def _metadata_from_payload(self, payload):
try: try:
@ -741,7 +808,7 @@ class CustomBuildTrigger(BuildTrigger):
raise InvalidPayloadException() raise InvalidPayloadException()
return metadata return metadata
def handle_trigger_request(self, request, trigger): def handle_trigger_request(self, request):
payload = request.get_json() payload = request.get_json()
if not payload: if not payload:
raise SkipRequestException() raise SkipRequestException()
@ -750,7 +817,7 @@ class CustomBuildTrigger(BuildTrigger):
metadata = self._metadata_from_payload(payload) metadata = self._metadata_from_payload(payload)
# The build source is the canonical git URL used to clone. # The build source is the canonical git URL used to clone.
config = get_trigger_config(trigger) config = self.config
metadata['git_url'] = config['build_source'] metadata['git_url'] = config['build_source']
branch = metadata['ref'].split('/')[-1] branch = metadata['ref'].split('/')[-1]
@ -759,9 +826,10 @@ class CustomBuildTrigger(BuildTrigger):
build_name = metadata['commit_sha'][:6] build_name = metadata['commit_sha'][:6]
dockerfile_id = None dockerfile_id = None
return dockerfile_id, tags, build_name, trigger.config['subdir'], metadata return dockerfile_id, tags, build_name, config['subdir'], metadata
def activate(self, trigger_uuid, standard_webhook_url, auth_token, config): def activate(self, standard_webhook_url):
config = self.config
public_key, private_key = generate_ssh_keypair() public_key, private_key = generate_ssh_keypair()
config['credentials'] = [ config['credentials'] = [
{ {
@ -773,18 +841,21 @@ class CustomBuildTrigger(BuildTrigger):
'value': standard_webhook_url, 'value': standard_webhook_url,
}, },
] ]
self.config = config
return config, {'private_key': private_key} return config, {'private_key': private_key}
def deactivate(self, auth_token, config): def deactivate(self):
config = self.config
config.pop('credentials', None) config.pop('credentials', None)
self.config = config
return config return config
def manual_start(self, trigger, run_parameters=None): def manual_start(self, run_parameters=None):
# commit_sha is the only required parameter # commit_sha is the only required parameter
if 'commit_sha' not in run_parameters: if 'commit_sha' not in run_parameters:
raise TriggerStartException('missing required parameter') raise TriggerStartException('missing required parameter')
config = get_trigger_config(trigger) config = self.config
dockerfile_id = None dockerfile_id = None
tags = {run_parameters['commit_sha']} tags = {run_parameters['commit_sha']}
build_name = run_parameters['commit_sha'] build_name = run_parameters['commit_sha']

View file

@ -20,7 +20,8 @@ from util.cache import no_cache
from endpoints.common import common_login, render_page_template, route_show_if, param_required from endpoints.common import common_login, render_page_template, route_show_if, param_required
from endpoints.csrf import csrf_protect, generate_csrf_token, verify_csrf from endpoints.csrf import csrf_protect, generate_csrf_token, verify_csrf
from endpoints.registry import set_cache_headers from endpoints.registry import set_cache_headers
from endpoints.trigger import CustomBuildTrigger, BitbucketBuildTrigger, TriggerProviderException from endpoints.trigger import (CustomBuildTrigger, BitbucketBuildTrigger, TriggerProviderException,
BuildTriggerHandler)
from util.names import parse_repository_name, parse_repository_name_and_tag from util.names import parse_repository_name, parse_repository_name_and_tag
from util.useremails import send_email_changed from util.useremails import send_email_changed
from util.systemlogs import build_logs_archive from util.systemlogs import build_logs_archive
@ -513,7 +514,7 @@ def attach_bitbucket_trigger(namespace, repository_name):
current_user.db_user()) current_user.db_user())
try: try:
oauth_info = BitbucketBuildTrigger.get_oauth_url(trigger.uuid) oauth_info = BuildTriggerHandler.get_handler(trigger).get_oauth_url()
config = { config = {
'access_token': oauth_info['access_token'] 'access_token': oauth_info['access_token']

View file

@ -9,7 +9,8 @@ from auth.permissions import ModifyRepositoryPermission
from util.invoice import renderInvoiceToHtml from util.invoice import renderInvoiceToHtml
from util.useremails import send_invoice_email, send_subscription_change, send_payment_failed from util.useremails import send_invoice_email, send_subscription_change, send_payment_failed
from util.http import abort from util.http import abort
from endpoints.trigger import BuildTrigger, ValidationRequestException, SkipRequestException, InvalidPayloadException from endpoints.trigger import (BuildTriggerHandler, ValidationRequestException,
SkipRequestException, InvalidPayloadException)
from endpoints.common import start_build from endpoints.common import start_build
@ -82,11 +83,11 @@ def build_trigger_webhook(trigger_uuid, **kwargs):
repository = trigger.repository.name repository = trigger.repository.name
permission = ModifyRepositoryPermission(namespace, repository) permission = ModifyRepositoryPermission(namespace, repository)
if permission.can(): if permission.can():
handler = BuildTrigger.get_trigger_for_service(trigger.service.name) handler = BuildTriggerHandler.get_handler(trigger)
logger.debug('Passing webhook request to handler %s', handler) logger.debug('Passing webhook request to handler %s', handler)
try: try:
specs = handler.handle_trigger_request(request, trigger) specs = handler.handle_trigger_request(request)
dockerfile_id, tags, name, subdir, metadata = specs dockerfile_id, tags, name, subdir, metadata = specs
except ValidationRequestException: except ValidationRequestException:
# This was just a validation request, we don't need to build anything # This was just a validation request, we don't need to build anything

View file

@ -0,0 +1,88 @@
.trigger-setup-githost-element .ref-reference {
color: #ccc;
}
.trigger-setup-githost-element .ref-reference span {
cursor: pointer;
text-decoration: line-through;
}
.trigger-setup-githost-element .ref-reference:hover {
color: #3276b1;
}
.trigger-setup-githost-element .ref-reference:hover span {
text-decoration: none;
}
.trigger-setup-githost-element .ref-reference.match {
color: black;
}
.trigger-setup-githost-element .ref-reference.match span {
text-decoration: none;
cursor: default;
}
.trigger-setup-githost-element .ref-filter {
white-space: nowrap;
}
.trigger-setup-githost-element .ref-filter span {
display: inline-block;
}
.trigger-setup-githost-element .selected-info {
margin-bottom: 20px;
}
.trigger-setup-githost-element .org-icon {
width: 20px;
margin-right: 8px;
vertical-align: middle;
}
.trigger-setup-githost-element li.repo-listing i {
margin-right: 10px;
margin-left: 6px;
}
.trigger-setup-githost-element li.org-header {
padding-left: 6px;
}
.trigger-setup-githost-element .matching-refs {
margin: 0px;
padding: 0px;
margin-left: 10px;
display: inline-block;
}
.trigger-setup-githost-element .ref-matches {
padding-left: 70px;
position: relative;
margin-bottom: 10px;
}
.trigger-setup-githost-element .ref-matches .kind {
font-weight: bold;
position: absolute;
top: 0px;
left: 0px;
}
.trigger-setup-githost-element .matching-refs.tags li:before {
content: "\f02b";
font-family: FontAwesome;
}
.trigger-setup-githost-element .matching-refs.branches li:before {
content: "\f126";
font-family: FontAwesome;
}
.trigger-setup-githost-element .matching-refs li {
list-style: none;
display: inline-block;
margin-left: 10px;
}

View file

@ -3768,95 +3768,6 @@ pre.command:before {
border-bottom-left-radius: 0px; border-bottom-left-radius: 0px;
} }
.trigger-setup-github-element .ref-reference {
color: #ccc;
}
.trigger-setup-github-element .ref-reference span {
cursor: pointer;
text-decoration: line-through;
}
.trigger-setup-github-element .ref-reference:hover {
color: #3276b1;
}
.trigger-setup-github-element .ref-reference:hover span {
text-decoration: none;
}
.trigger-setup-github-element .ref-reference.match {
color: black;
}
.trigger-setup-github-element .ref-reference.match span {
text-decoration: none;
cursor: default;
}
.trigger-setup-github-element .ref-filter {
white-space: nowrap;
}
.trigger-setup-github-element .ref-filter span {
display: inline-block;
}
.trigger-setup-github-element .selected-info {
margin-bottom: 20px;
}
.trigger-setup-github-element .github-org-icon {
width: 20px;
margin-right: 8px;
vertical-align: middle;
}
.trigger-setup-github-element li.github-repo-listing i {
margin-right: 10px;
margin-left: 6px;
}
.trigger-setup-github-element li.github-org-header {
padding-left: 6px;
}
.trigger-setup-github-element .matching-refs {
margin: 0px;
padding: 0px;
margin-left: 10px;
display: inline-block;
}
.trigger-setup-github-element .ref-matches {
padding-left: 70px;
position: relative;
margin-bottom: 10px;
}
.trigger-setup-github-element .ref-matches .kind {
font-weight: bold;
position: absolute;
top: 0px;
left: 0px;
}
.trigger-setup-github-element .matching-refs.tags li:before {
content: "\f02b";
font-family: FontAwesome;
}
.trigger-setup-github-element .matching-refs.branches li:before {
content: "\f126";
font-family: FontAwesome;
}
.trigger-setup-github-element .matching-refs li {
list-style: none;
display: inline-block;
margin-left: 10px;
}
.setup-trigger-directive-element .dockerfile-found-content { .setup-trigger-directive-element .dockerfile-found-content {
margin-left: 32px; margin-left: 32px;
} }

View file

@ -15,7 +15,14 @@
<!-- Trigger-specific setup --> <!-- Trigger-specific setup -->
<div class="trigger-description-element trigger-option-section" ng-switch on="trigger.service"> <div class="trigger-description-element trigger-option-section" ng-switch on="trigger.service">
<div ng-switch-when="github"> <div ng-switch-when="github">
<div class="trigger-setup-github" repository="repository" trigger="trigger" <div class="trigger-setup-githost" repository="repository" trigger="trigger"
kind="github"
next-step-counter="nextStepCounter" current-step-valid="state.stepValid"
analyze="checkAnalyze(isValid)"></div>
</div>
<div ng-switch-when="bitbucket">
<div class="trigger-setup-githost" repository="repository" trigger="trigger"
kind="bitbucket"
next-step-counter="nextStepCounter" current-step-valid="state.stepValid" next-step-counter="nextStepCounter" current-step-valid="state.stepValid"
analyze="checkAnalyze(isValid)"></div> analyze="checkAnalyze(isValid)"></div>
</div> </div>

View file

@ -1,4 +1,4 @@
<div class="trigger-setup-github-element"> <div class="trigger-setup-githost-element">
<!-- Current selected info --> <!-- Current selected info -->
<div class="selected-info" ng-show="nextStepCounter > 0"> <div class="selected-info" ng-show="nextStepCounter > 0">
<table style="width: 100%;"> <table style="width: 100%;">
@ -8,9 +8,17 @@
</td> </td>
<td> <td>
<div class="current-repo"> <div class="current-repo">
<img class="dropdown-select-icon github-org-icon" <img class="dropdown-select-icon org-icon"
ng-src="{{ state.currentRepo.avatar_url ? state.currentRepo.avatar_url : '/static/img/empty.png' }}"> ng-src="{{ state.currentRepo.avatar_url ? state.currentRepo.avatar_url : '/static/img/empty.png' }}">
<a ng-href="https://github.com/{{ state.currentRepo.repo }}" target="_blank">{{ state.currentRepo.repo }}</a>
<!-- Kind Switch -->
<a ng-href="https://github.com/{{ state.currentRepo.repo }}" target="_blank" ng-if="kind == 'github'">
{{ state.currentRepo.repo }}
</a>
<a ng-href="https://bitbucket.org/{{ state.currentRepo.repo }}" target="_blank" ng-if="kind == 'bitbucket'">
{{ state.currentRepo.repo }}
</a>
<!-- /Kind Switch -->
</div> </div>
</td> </td>
</tr> </tr>
@ -47,21 +55,23 @@
<!-- Repository select --> <!-- Repository select -->
<div class="step-view-step" complete-condition="state.currentRepo" load-callback="loadRepositories(callback)" <div class="step-view-step" complete-condition="state.currentRepo" load-callback="loadRepositories(callback)"
load-message="Loading Repositories"> load-message="Loading Repositories">
<div style="margin-bottom: 12px">Please choose the GitHub repository that will trigger the build:</div> <div style="margin-bottom: 12px">Please choose the repository that will trigger the build:</div>
<div class="dropdown-select" placeholder="'Enter or select a repository'" selected-item="state.currentRepo" <div class="dropdown-select" placeholder="'Enter or select a repository'" selected-item="state.currentRepo"
lookahead-items="repoLookahead" allow-custom-input="true"> lookahead-items="repoLookahead" allow-custom-input="true">
<!-- Icons --> <!-- Icons -->
<i class="dropdown-select-icon none-icon fa fa-github fa-lg"></i> <i class="dropdown-select-icon none-icon fa fa-lg" ng-class="'fa-' + kind"></i>
<img class="dropdown-select-icon github-org-icon" <img class="dropdown-select-icon org-icon"
ng-src="{{ state.currentRepo.avatar_url ? state.currentRepo.avatar_url : '/static/image/empty.png' }}"> ng-src="{{ state.currentRepo.avatar_url ? state.currentRepo.avatar_url : '/static/image/empty.png' }}">
<!-- Dropdown menu --> <!-- Dropdown menu -->
<ul class="dropdown-select-menu scrollable-menu" role="menu"> <ul class="dropdown-select-menu scrollable-menu" role="menu">
<li ng-repeat-start="org in orgs" role="presentation" class="dropdown-header github-org-header"> <li ng-repeat-start="org in orgs" role="presentation" class="dropdown-header org-header">
<img ng-src="{{ org.info.avatar_url }}" class="github-org-icon">{{ org.info.name }} <img ng-src="{{ org.info.avatar_url }}" class="org-icon">{{ org.info.name }}
</li> </li>
<li ng-repeat="repo in org.repos" class="github-repo-listing"> <li ng-repeat="repo in org.repos" class="repo-listing">
<a href="javascript:void(0)" ng-click="selectRepo(repo, org)"><i class="fa fa-github fa-lg"></i> {{ repo }}</a> <a href="javascript:void(0)" ng-click="selectRepo(repo, org)">
<i class="fa fa-lg" ng-class="'fa-' + kind"></i> {{ repo }}
</a>
</li> </li>
<li role="presentation" class="divider" ng-repeat-end ng-show="$index < orgs.length - 1"></li> <li role="presentation" class="divider" ng-repeat-end ng-show="$index < orgs.length - 1"></li>
</ul> </ul>

View file

@ -1,16 +1,17 @@
/** /**
* An element which displays github-specific setup information for its build triggers. * An element which displays hosted Git (GitHub, Bitbucket)-specific setup information for its build triggers.
*/ */
angular.module('quay').directive('triggerSetupGithub', function () { angular.module('quay').directive('triggerSetupGithost', function () {
var directiveDefinitionObject = { var directiveDefinitionObject = {
priority: 0, priority: 0,
templateUrl: '/static/directives/trigger-setup-github.html', templateUrl: '/static/directives/trigger-setup-githost.html',
replace: false, replace: false,
transclude: false, transclude: false,
restrict: 'C', restrict: 'C',
scope: { scope: {
'repository': '=repository', 'repository': '=repository',
'trigger': '=trigger', 'trigger': '=trigger',
'kind': '@kind',
'nextStepCounter': '=nextStepCounter', 'nextStepCounter': '=nextStepCounter',
'currentStepValid': '=currentStepValid', 'currentStepValid': '=currentStepValid',