Work in progress: bitbucket support

This commit is contained in:
Joseph Schorr 2015-04-24 15:13:08 -04:00
parent d180524b23
commit c480fb2105
12 changed files with 321 additions and 86 deletions

View file

@ -0,0 +1,43 @@
import logging
from flask import request, redirect, url_for, Blueprint
from flask.ext.login import current_user
from endpoints.trigger import BitbucketBuildTrigger
from endpoints.common import route_show_if
from app import app
from data import model
from util.names import parse_repository_name
from util.http import abort
from auth.auth import require_session_login
import features
logger = logging.getLogger(__name__)
client = app.config['HTTPCLIENT']
bitbuckettrigger = Blueprint('bitbuckettrigger', __name__)
@bitbuckettrigger.route('/bitbucket/callback/trigger/<trigger_uuid>', methods=['GET'])
@route_show_if(features.BITBUCKET_BUILD)
@require_session_login
def attach_bitbucket_build_trigger(trigger_uuid):
trigger = model.get_build_trigger(trigger_uuid)
if not trigger or trigger.service.name != BitbucketBuildTrigger.service_name():
abort(404)
if trigger.connected_user != current_user.db_user():
abort(404)
verifier = request.args.get('oauth_verifier')
result = BitbucketBuildTrigger.exchange_verifier(trigger, verifier)
print result
return 'hello'
repo_path = '%s/%s' % (namespace, repository)
full_url = '%s%s%s' % (url_for('web.repository', path=repo_path), '?tab=builds&newtrigger=',
trigger.uuid)
logger.debug('Redirecting to full url: %s', full_url)
return redirect(full_url)

View file

@ -0,0 +1,51 @@
import logging
from flask import request, redirect, url_for, Blueprint
from flask.ext.login import current_user
from endpoints.common import route_show_if
from app import app, github_trigger
from data import model
from util.names import parse_repository_name
from util.http import abort
from auth.permissions import AdministerRepositoryPermission
from auth.auth import require_session_login
import features
logger = logging.getLogger(__name__)
client = app.config['HTTPCLIENT']
githubtrigger = Blueprint('callback', __name__)
@githubtrigger.route('/github/callback/trigger/<path:repository>', methods=['GET'])
@githubtrigger.route('/github/callback/trigger/<path:repository>/__new', methods=['GET'])
@route_show_if(features.GITHUB_BUILD)
@require_session_login
@parse_repository_name
def attach_github_build_trigger(namespace, repository):
permission = AdministerRepositoryPermission(namespace, repository)
if permission.can():
code = request.args.get('code')
token = github_trigger.exchange_code_for_token(app.config, client, code)
repo = model.get_repository(namespace, repository)
if not repo:
msg = 'Invalid repository: %s/%s' % (namespace, repository)
abort(404, message=msg)
trigger = model.create_build_trigger(repo, 'github', token, current_user.db_user())
# TODO(jschorr): Remove once the new layout is in place.
admin_path = '%s/%s/%s' % (namespace, repository, 'admin')
full_url = '%s%s%s' % (url_for('web.repository', path=admin_path), '?tab=trigger&new_trigger=',
trigger.uuid)
if '__new' in request.url:
repo_path = '%s/%s' % (namespace, repository)
full_url = '%s%s%s' % (url_for('web.repository', path=repo_path), '?tab=builds&newtrigger=',
trigger.uuid)
logger.debug('Redirecting to full url: %s', full_url)
return redirect(full_url)
abort(403)

View file

@ -5,23 +5,19 @@ from flask import request, redirect, url_for, Blueprint
from flask.ext.login import current_user
from endpoints.common import render_page_template, common_login, route_show_if
from app import app, analytics, get_app_url, github_login, google_login, github_trigger
from app import app, analytics, get_app_url, github_login, google_login
from data import model
from util.names import parse_repository_name
from util.validation import generate_valid_usernames
from util.http import abort
from auth.permissions import AdministerRepositoryPermission
from auth.auth import require_session_login
from peewee import IntegrityError
import features
logger = logging.getLogger(__name__)
client = app.config['HTTPCLIENT']
callback = Blueprint('callback', __name__)
oauthlogin = Blueprint('oauthlogin', __name__)
def render_ologin_error(service_name,
error_message='Could not load user data. The token may have expired.'):
@ -30,36 +26,6 @@ def render_ologin_error(service_name,
service_url=get_app_url(),
user_creation=features.USER_CREATION)
def exchange_code_for_token(code, service, form_encode=False, redirect_suffix=''):
code = request.args.get('code')
payload = {
'client_id': service.client_id(),
'client_secret': service.client_secret(),
'code': code,
'grant_type': 'authorization_code',
'redirect_uri': '%s://%s/oauth2/%s/callback%s' % (app.config['PREFERRED_URL_SCHEME'],
app.config['SERVER_HOSTNAME'],
service.service_name().lower(),
redirect_suffix)
}
headers = {
'Accept': 'application/json'
}
token_url = service.token_endpoint()
if form_encode:
get_access_token = client.post(token_url, data=payload, headers=headers)
else:
get_access_token = client.post(token_url, params=payload, headers=headers)
json_data = get_access_token.json()
if not json_data:
return ''
token = json_data.get('access_token', '')
return token
def get_user(service, token):
token_param = {
@ -129,14 +95,15 @@ def get_google_username(user_data):
return username
@callback.route('/google/callback', methods=['GET'])
@oauthlogin.route('/google/callback', methods=['GET'])
@route_show_if(features.GOOGLE_LOGIN)
def google_oauth_callback():
error = request.args.get('error', None)
if error:
return render_ologin_error('Google', error)
token = exchange_code_for_token(request.args.get('code'), google_login, form_encode=True)
code = request.args.get('code')
token = google_login.exchange_code_for_token(app.config, client, code, form_encode=True)
user_data = get_user(google_login, token)
if not user_data or not user_data.get('id', None) or not user_data.get('email', None):
return render_ologin_error('Google')
@ -150,7 +117,7 @@ def google_oauth_callback():
metadata=metadata)
@callback.route('/github/callback', methods=['GET'])
@oauthlogin.route('/github/callback', methods=['GET'])
@route_show_if(features.GITHUB_LOGIN)
def github_oauth_callback():
error = request.args.get('error', None)
@ -158,7 +125,8 @@ def github_oauth_callback():
return render_ologin_error('GitHub', error)
# Exchange the OAuth code.
token = exchange_code_for_token(request.args.get('code'), github_login)
code = request.args.get('code')
token = google_login.exchange_code_for_token(app.config, client, code)
# Retrieve the user's information.
user_data = get_user(github_login, token)
@ -211,12 +179,13 @@ def github_oauth_callback():
return conduct_oauth_login(github_login, github_id, username, found_email, metadata=metadata)
@callback.route('/google/callback/attach', methods=['GET'])
@oauthlogin.route('/google/callback/attach', methods=['GET'])
@route_show_if(features.GOOGLE_LOGIN)
@require_session_login
def google_oauth_attach():
token = exchange_code_for_token(request.args.get('code'), google_login,
redirect_suffix='/attach', form_encode=True)
code = request.args.get('code')
token = google_login.exchange_code_for_token(app.config, client, code,
redirect_suffix='/attach', form_encode=True)
user_data = get_user(google_login, token)
if not user_data or not user_data.get('id', None):
@ -240,11 +209,12 @@ def google_oauth_attach():
return redirect(url_for('web.user'))
@callback.route('/github/callback/attach', methods=['GET'])
@oauthlogin.route('/github/callback/attach', methods=['GET'])
@route_show_if(features.GITHUB_LOGIN)
@require_session_login
def github_oauth_attach():
token = exchange_code_for_token(request.args.get('code'), github_login)
code = request.args.get('code')
token = google_login.exchange_code_for_token(app.config, client, code)
user_data = get_user(github_login, token)
if not user_data:
return render_ologin_error('GitHub')
@ -265,37 +235,4 @@ def github_oauth_attach():
return render_ologin_error('GitHub', err)
return redirect(url_for('web.user'))
@callback.route('/github/callback/trigger/<path:repository>', methods=['GET'])
@callback.route('/github/callback/trigger/<path:repository>/__new', methods=['GET'])
@route_show_if(features.GITHUB_BUILD)
@require_session_login
@parse_repository_name
def attach_github_build_trigger(namespace, repository):
permission = AdministerRepositoryPermission(namespace, repository)
if permission.can():
token = exchange_code_for_token(request.args.get('code'), github_trigger)
repo = model.get_repository(namespace, repository)
if not repo:
msg = 'Invalid repository: %s/%s' % (namespace, repository)
abort(404, message=msg)
trigger = model.create_build_trigger(repo, 'github', token, current_user.db_user())
# TODO(jschorr): Remove once the new layout is in place.
admin_path = '%s/%s/%s' % (namespace, repository, 'admin')
full_url = '%s%s%s' % (url_for('web.repository', path=admin_path), '?tab=trigger&new_trigger=',
trigger.uuid)
if '__new' in request.url:
repo_path = '%s/%s' % (namespace, repository)
full_url = '%s%s%s' % (url_for('web.repository', path=repo_path), '?tab=builds&newtrigger=',
trigger.uuid)
logger.debug('Redirecting to full url: %s', full_url)
return redirect(full_url)
abort(403)
return redirect(url_for('web.user'))

View file

@ -7,10 +7,11 @@ import re
import json
from github import Github, UnknownObjectException, GithubException
from bitbucket.bitbucket import Bitbucket
from tempfile import SpooledTemporaryFile
from jsonschema import validate
from app import app, userfiles as user_files, github_trigger
from app import app, userfiles as user_files, github_trigger, get_app_url
from util.tarfileappender import TarfileAppender
from util.ssh import generate_ssh_keypair
@ -58,6 +59,9 @@ class EmptyRepositoryException(Exception):
class RepositoryReadException(Exception):
pass
class TriggerProviderException(Exception):
pass
class BuildTrigger(object):
def __init__(self):
@ -158,6 +162,87 @@ def get_trigger_config(trigger):
return {}
class BitbucketBuildTrigger(BuildTrigger):
"""
BuildTrigger for Bitbucket.
"""
@classmethod
def service_name(cls):
return 'bitbucket'
@staticmethod
def _get_authorized_client(trigger_uuid):
key = app.config.get('BITBUCKET_TRIGGER_CONFIG', {}).get('CONSUMER_KEY', '')
secret = app.config.get('BITBUCKET_TRIGGER_CONFIG', {}).get('CONSUMER_SECRET', '')
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)
if not result:
raise TriggerProviderException(err_message)
return bitbucket_client
@staticmethod
def get_oauth_url(trigger_uuid):
bitbucket_client = BitbucketBuildTrigger._get_authorized_client(trigger_uuid)
url = bitbucket_client.url('AUTHENTICATE', token=bitbucket_client.access_token)
return {
'access_token': bitbucket_client.access_token,
'access_token_secret': bitbucket_client.access_token_secret,
'url': url
}
@staticmethod
def exchange_verifier(trigger, verifier):
trigger_config = get_trigger_config(trigger.config)
bitbucket_client = BitbucketBuildTrigger._get_authorized_client(trigger.uuid)
print trigger.config
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:
# return None
#return (bitbucket_client.access_token, bitbucket_client.access_token_secret)
def is_active(self, config):
return False
def activate(self, trigger_uuid, standard_webhook_url, auth_token, config):
return {}
def deactivate(self, auth_token, config):
return config
def list_build_sources(self, auth_token):
return []
def list_build_subdirs(self, auth_token, config):
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.

View file

@ -20,7 +20,7 @@ from util.cache import no_cache
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.registry import set_cache_headers
from endpoints.trigger import CustomBuildTrigger
from endpoints.trigger import CustomBuildTrigger, BitbucketBuildTrigger, TriggerProviderException
from util.names import parse_repository_name, parse_repository_name_and_tag
from util.useremails import send_email_changed
from util.systemlogs import build_logs_archive
@ -495,6 +495,41 @@ def download_logs_archive():
abort(403)
@web.route('/bitbucket/setup/<path:repository>', methods=['GET'])
@require_session_login
@parse_repository_name
@route_show_if(features.BITBUCKET_BUILD)
def attach_bitbucket_trigger(namespace, repository_name):
permission = AdministerRepositoryPermission(namespace, repository_name)
if permission.can():
repo = model.get_repository(namespace, repository_name)
if not repo:
msg = 'Invalid repository: %s/%s' % (namespace, repository_name)
abort(404, message=msg)
trigger = model.create_build_trigger(repo, BitbucketBuildTrigger.service_name(),
None,
current_user.db_user())
try:
oauth_info = BitbucketBuildTrigger.get_oauth_url(trigger.uuid)
config = {
'access_token': oauth_info['access_token']
}
access_token_secret = oauth_info['access_token_secret']
model.update_build_trigger(trigger, config, auth_token=access_token_secret)
return redirect(oauth_info['url'])
except TriggerProviderException:
trigger.delete_instance()
abort(400, message='Could not retrieve OAuth URL from Bitbucket')
abort(403)
@web.route('/customtrigger/setup/<path:repository>', methods=['GET'])
@require_session_login
@parse_repository_name