Split out callbacks into their own blueprint. Add build trigger DB information and connect it with some APIs. Stub out the UI to allow for generation of triggers. Split out the triggers into a plugin-ish architecture for easily adding new triggers.
This commit is contained in:
parent
fc4983ed8b
commit
b5d4919364
13 changed files with 500 additions and 170 deletions
|
@ -16,11 +16,13 @@ from endpoints.tags import tags
|
|||
from endpoints.registry import registry
|
||||
from endpoints.webhooks import webhooks
|
||||
from endpoints.realtime import realtime
|
||||
from endpoints.callbacks import callback
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
application.register_blueprint(web)
|
||||
application.register_blueprint(callback, url_prefix='/oauth2')
|
||||
application.register_blueprint(index, url_prefix='/v1')
|
||||
application.register_blueprint(tags, url_prefix='/v1')
|
||||
application.register_blueprint(registry, url_prefix='/v1')
|
||||
|
|
|
@ -110,6 +110,19 @@ class Repository(BaseModel):
|
|||
)
|
||||
|
||||
|
||||
class BuildTriggerService(BaseModel):
|
||||
name = CharField(index=True)
|
||||
|
||||
|
||||
class RepositoryBuildTrigger(BaseModel):
|
||||
uuid = CharField(default=uuid_generator)
|
||||
service = ForeignKeyField(BuildTriggerService, index=True)
|
||||
repository = ForeignKeyField(Repository, index=True)
|
||||
connected_user = ForeignKeyField(User)
|
||||
auth_token = CharField()
|
||||
config = TextField(default='{}')
|
||||
|
||||
|
||||
class Role(BaseModel):
|
||||
name = CharField(index=True)
|
||||
|
||||
|
@ -248,4 +261,5 @@ all_models = [User, Repository, Image, AccessToken, Role,
|
|||
RepositoryPermission, Visibility, RepositoryTag,
|
||||
EmailConfirmation, FederatedLogin, LoginService, QueueItem,
|
||||
RepositoryBuild, Team, TeamMember, TeamRole, Webhook,
|
||||
LogEntryKind, LogEntry, PermissionPrototype]
|
||||
LogEntryKind, LogEntry, PermissionPrototype, BuildTriggerService,
|
||||
RepositoryBuildTrigger]
|
||||
|
|
|
@ -55,6 +55,10 @@ class InvalidWebhookException(DataModelException):
|
|||
pass
|
||||
|
||||
|
||||
class InvalidBuildTriggerException(DataModelException):
|
||||
pass
|
||||
|
||||
|
||||
def create_user(username, password, email):
|
||||
if not validate_email(email):
|
||||
raise InvalidEmailAddressException('Invalid email address: %s' % email)
|
||||
|
@ -1352,27 +1356,75 @@ def delete_webhook(namespace_name, repository_name, public_id):
|
|||
webhook = get_webhook(namespace_name, repository_name, public_id)
|
||||
webhook.delete_instance()
|
||||
return webhook
|
||||
|
||||
def list_logs(user_or_organization_name, start_time, end_time, performer = None, repository = None):
|
||||
joined = LogEntry.select().join(User)
|
||||
if repository:
|
||||
joined = joined.where(LogEntry.repository == repository)
|
||||
|
||||
if performer:
|
||||
joined = joined.where(LogEntry.performer == performer)
|
||||
|
||||
return joined.where(
|
||||
User.username == user_or_organization_name,
|
||||
LogEntry.datetime >= start_time,
|
||||
LogEntry.datetime < end_time).order_by(LogEntry.datetime.desc())
|
||||
def list_logs(user_or_organization_name, start_time, end_time, performer=None,
|
||||
repository=None):
|
||||
joined = LogEntry.select().join(User)
|
||||
if repository:
|
||||
joined = joined.where(LogEntry.repository == repository)
|
||||
|
||||
def log_action(kind_name, user_or_organization_name, performer=None, repository=None,
|
||||
access_token=None, ip=None, metadata={}, timestamp=None):
|
||||
if performer:
|
||||
joined = joined.where(LogEntry.performer == performer)
|
||||
|
||||
return joined.where(
|
||||
User.username == user_or_organization_name,
|
||||
LogEntry.datetime >= start_time,
|
||||
LogEntry.datetime < end_time).order_by(LogEntry.datetime.desc())
|
||||
|
||||
|
||||
def log_action(kind_name, user_or_organization_name, performer=None,
|
||||
repository=None, access_token=None, ip=None, metadata={},
|
||||
timestamp=None):
|
||||
if not timestamp:
|
||||
timestamp = datetime.today()
|
||||
|
||||
kind = LogEntryKind.get(LogEntryKind.name == kind_name)
|
||||
account = User.get(User.username == user_or_organization_name)
|
||||
entry = LogEntry.create(kind=kind, account=account, performer=performer,
|
||||
repository=repository, access_token=access_token, ip=ip,
|
||||
metadata_json=json.dumps(metadata), datetime=timestamp)
|
||||
LogEntry.create(kind=kind, account=account, performer=performer,
|
||||
repository=repository, access_token=access_token, ip=ip,
|
||||
metadata_json=json.dumps(metadata), datetime=timestamp)
|
||||
|
||||
|
||||
def create_build_trigger(namespace_name, repository_name, service_name,
|
||||
auth_token, user):
|
||||
service = BuildTriggerService.get(name=service_name)
|
||||
repo = Repository.get(namespace=namespace_name, name=repository_name)
|
||||
|
||||
trigger = RepositoryBuildTrigger.create(repository=repo, service=service,
|
||||
auth_token=auth_token,
|
||||
connected_user=user)
|
||||
return trigger
|
||||
|
||||
|
||||
def get_build_trigger(namespace_name, repository_name, trigger_uuid):
|
||||
try:
|
||||
return (RepositoryBuildTrigger
|
||||
.select(RepositoryBuildTrigger, BuildTriggerService, Repository)
|
||||
.join(BuildTriggerService)
|
||||
.switch(RepositoryBuildTrigger)
|
||||
.join(Repository)
|
||||
.switch(RepositoryBuildTrigger)
|
||||
.join(User)
|
||||
.where(RepositoryBuildTrigger.uuid == trigger_uuid,
|
||||
Repository.namespace == namespace_name,
|
||||
Repository.name == repository_name)
|
||||
.get())
|
||||
except RepositoryBuildTrigger.DoesNotExist:
|
||||
msg = 'No build trigger with uuid: %s' % trigger_uuid
|
||||
raise InvalidBuildTriggerException(msg)
|
||||
|
||||
|
||||
def list_build_triggers(namespace_name, repository_name):
|
||||
return (RepositoryBuildTrigger
|
||||
.select(RepositoryBuildTrigger, BuildTriggerService, Repository)
|
||||
.join(BuildTriggerService)
|
||||
.switch(RepositoryBuildTrigger)
|
||||
.join(Repository)
|
||||
.where(Repository.namespace == namespace_name,
|
||||
Repository.name == repository_name))
|
||||
|
||||
|
||||
def delete_build_trigger(namespace_name, repository_name, trigger_uuid):
|
||||
trigger = get_build_trigger(namespace_name, repository_name, trigger_uuid)
|
||||
trigger.delete_instance()
|
||||
|
|
|
@ -25,7 +25,7 @@ from auth.permissions import (ReadRepositoryPermission,
|
|||
AdministerOrganizationPermission,
|
||||
OrganizationMemberPermission,
|
||||
ViewTeamPermission)
|
||||
from endpoints.common import common_login
|
||||
from endpoints.common import common_login, get_route_data
|
||||
from util.cache import cache_control
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
@ -34,7 +34,6 @@ user_files = app.config['USERFILES']
|
|||
build_logs = app.config['BUILDLOGS']
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
route_data = None
|
||||
|
||||
api = Blueprint('api', __name__)
|
||||
|
||||
|
@ -62,37 +61,6 @@ def request_error(exception=None, **kwargs):
|
|||
return make_response(jsonify(data), 400)
|
||||
|
||||
|
||||
def get_route_data():
|
||||
global route_data
|
||||
if route_data:
|
||||
return route_data
|
||||
|
||||
routes = []
|
||||
for rule in app.url_map.iter_rules():
|
||||
if rule.endpoint.startswith('api.'):
|
||||
endpoint_method = app.view_functions[rule.endpoint]
|
||||
is_internal = '__internal_call' in dir(endpoint_method)
|
||||
is_org_api = '__user_call' in dir(endpoint_method)
|
||||
methods = list(rule.methods.difference(['HEAD', 'OPTIONS']))
|
||||
|
||||
route = {
|
||||
'name': rule.endpoint[4:],
|
||||
'methods': methods,
|
||||
'path': rule.rule,
|
||||
'parameters': list(rule.arguments)
|
||||
}
|
||||
|
||||
if is_org_api:
|
||||
route['user_method'] = endpoint_method.__user_call
|
||||
|
||||
routes.append(route)
|
||||
|
||||
route_data = {
|
||||
'endpoints': routes
|
||||
}
|
||||
return route_data
|
||||
|
||||
|
||||
def log_action(kind, user_or_orgname, metadata={}, repo=None):
|
||||
performer = current_user.db_user()
|
||||
model.log_action(kind, user_or_orgname, performer=performer,
|
||||
|
@ -1335,6 +1303,62 @@ def delete_webhook(namespace, repository, public_id):
|
|||
abort(403) # Permission denied
|
||||
|
||||
|
||||
def trigger_view(trigger):
|
||||
return {
|
||||
'service': trigger.service.name,
|
||||
'config': json.loads(trigger.config),
|
||||
'id': trigger.uuid,
|
||||
'connected_user': trigger.connected_user.username,
|
||||
}
|
||||
|
||||
|
||||
@api.route('/repository/<path:repository>/trigger/<trigger_uuid>',
|
||||
methods=['GET'])
|
||||
@api_login_required
|
||||
@parse_repository_name
|
||||
def get_build_trigger(namespace, repository, trigger_uuid):
|
||||
permission = AdministerRepositoryPermission(namespace, repository)
|
||||
if permission.can():
|
||||
try:
|
||||
trigger = model.get_build_trigger(namespace, repository, trigger_uuid)
|
||||
except model.InvalidBuildTriggerException:
|
||||
abort(404)
|
||||
|
||||
return jsonify(trigger_view(trigger))
|
||||
|
||||
abort(403) # Permission denied
|
||||
|
||||
|
||||
@api.route('/repository/<path:repository>/trigger/', methods=['GET'])
|
||||
@api_login_required
|
||||
@parse_repository_name
|
||||
def list_build_triggers(namespace, repository):
|
||||
permission = AdministerRepositoryPermission(namespace, repository)
|
||||
if permission.can():
|
||||
triggers = model.list_build_triggers(namespace, repository)
|
||||
return jsonify({
|
||||
'triggers': [trigger_view(trigger) for trigger in triggers]
|
||||
})
|
||||
|
||||
abort(403) # Permission denied
|
||||
|
||||
|
||||
@api.route('/repository/<path:repository>/trigger/<trigger_uuid>',
|
||||
methods=['DELETE'])
|
||||
@api_login_required
|
||||
@parse_repository_name
|
||||
def delete_build_trigger(namespace, repository, trigger_uuid):
|
||||
permission = AdministerRepositoryPermission(namespace, repository)
|
||||
if permission.can():
|
||||
model.delete_build_trigger(namespace, repository, trigger_uuid)
|
||||
log_action('delete_repo_trigger', namespace,
|
||||
{'repo': repository, 'trigger_id': trigger_uuid},
|
||||
repo=model.get_repository(namespace, repository))
|
||||
return make_response('No Content', 204)
|
||||
|
||||
abort(403) # Permission denied
|
||||
|
||||
|
||||
@api.route('/filedrop/', methods=['POST'])
|
||||
@api_login_required
|
||||
@internal_api_call
|
||||
|
|
119
endpoints/callbacks.py
Normal file
119
endpoints/callbacks.py
Normal file
|
@ -0,0 +1,119 @@
|
|||
import requests
|
||||
import logging
|
||||
|
||||
from flask import request, redirect, url_for, Blueprint
|
||||
from flask.ext.login import login_required, current_user
|
||||
|
||||
from endpoints.common import render_page_template, common_login
|
||||
from app import app, mixpanel
|
||||
from data import model
|
||||
from util.names import parse_repository_name
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
callback = Blueprint('callback', __name__)
|
||||
|
||||
|
||||
def exchange_github_code_for_token(code):
|
||||
code = request.args.get('code')
|
||||
payload = {
|
||||
'client_id': app.config['GITHUB_CLIENT_ID'],
|
||||
'client_secret': app.config['GITHUB_CLIENT_SECRET'],
|
||||
'code': code,
|
||||
}
|
||||
headers = {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
|
||||
get_access_token = requests.post(app.config['GITHUB_TOKEN_URL'],
|
||||
params=payload, headers=headers)
|
||||
|
||||
token = get_access_token.json()['access_token']
|
||||
return token
|
||||
|
||||
|
||||
def get_github_user(token):
|
||||
token_param = {
|
||||
'access_token': token,
|
||||
}
|
||||
get_user = requests.get(app.config['GITHUB_USER_URL'], params=token_param)
|
||||
|
||||
return get_user.json()
|
||||
|
||||
|
||||
@callback.route('/github/callback', methods=['GET'])
|
||||
def github_oauth_callback():
|
||||
error = request.args.get('error', None)
|
||||
if error:
|
||||
return render_page_template('githuberror.html', error_message=error)
|
||||
|
||||
token = exchange_github_code_for_token(request.args.get('code'))
|
||||
user_data = get_github_user(token)
|
||||
|
||||
username = user_data['login']
|
||||
github_id = user_data['id']
|
||||
|
||||
v3_media_type = {
|
||||
'Accept': 'application/vnd.github.v3'
|
||||
}
|
||||
|
||||
token_param = {
|
||||
'access_token': token,
|
||||
}
|
||||
get_email = requests.get(app.config['GITHUB_USER_EMAILS'],
|
||||
params=token_param, headers=v3_media_type)
|
||||
|
||||
# We will accept any email, but we prefer the primary
|
||||
found_email = None
|
||||
for user_email in get_email.json():
|
||||
found_email = user_email['email']
|
||||
if user_email['primary']:
|
||||
break
|
||||
|
||||
to_login = model.verify_federated_login('github', github_id)
|
||||
if not to_login:
|
||||
# try to create the user
|
||||
try:
|
||||
to_login = model.create_federated_user(username, found_email, 'github',
|
||||
github_id)
|
||||
|
||||
# Success, tell mixpanel
|
||||
mixpanel.track(to_login.username, 'register', {'service': 'github'})
|
||||
|
||||
state = request.args.get('state', None)
|
||||
if state:
|
||||
logger.debug('Aliasing with state: %s' % state)
|
||||
mixpanel.alias(to_login.username, state)
|
||||
|
||||
except model.DataModelException, ex:
|
||||
return render_page_template('githuberror.html', error_message=ex.message)
|
||||
|
||||
if common_login(to_login):
|
||||
return redirect(url_for('web.index'))
|
||||
|
||||
return render_page_template('githuberror.html')
|
||||
|
||||
|
||||
@callback.route('/github/callback/attach', methods=['GET'])
|
||||
@login_required
|
||||
def github_oauth_attach():
|
||||
token = exchange_github_code_for_token(request.args.get('code'))
|
||||
user_data = get_github_user(token)
|
||||
github_id = user_data['id']
|
||||
user_obj = current_user.db_user()
|
||||
model.attach_federated_login(user_obj, 'github', github_id)
|
||||
return redirect(url_for('web.user'))
|
||||
|
||||
|
||||
@callback.route('/github/callback/trigger/<path:repository>', methods=['GET'])
|
||||
@login_required
|
||||
@parse_repository_name
|
||||
def attach_github_build_trigger(namespace, repository):
|
||||
token = exchange_github_code_for_token(request.args.get('code'))
|
||||
model.create_build_trigger(namespace, repository, 'github', token,
|
||||
current_user.db_user())
|
||||
admin_path = '%s/%s/%s' % (namespace, repository, 'admin')
|
||||
full_url = url_for('web.repository', path=admin_path) + '?tab=trigger'
|
||||
logger.debug('Redirecting to full url: %s' % full_url)
|
||||
return redirect(full_url)
|
|
@ -2,7 +2,7 @@ import logging
|
|||
import os
|
||||
import base64
|
||||
|
||||
from flask import request, abort, session, make_response
|
||||
from flask import session, make_response, render_template
|
||||
from flask.ext.login import login_user, UserMixin
|
||||
from flask.ext.principal import identity_changed
|
||||
|
||||
|
@ -14,6 +14,39 @@ from auth.permissions import QuayDeferredPermissionUser
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
route_data = None
|
||||
|
||||
def get_route_data():
|
||||
global route_data
|
||||
if route_data:
|
||||
return route_data
|
||||
|
||||
routes = []
|
||||
for rule in app.url_map.iter_rules():
|
||||
if rule.endpoint.startswith('api.'):
|
||||
endpoint_method = app.view_functions[rule.endpoint]
|
||||
is_internal = '__internal_call' in dir(endpoint_method)
|
||||
is_org_api = '__user_call' in dir(endpoint_method)
|
||||
methods = list(rule.methods.difference(['HEAD', 'OPTIONS']))
|
||||
|
||||
route = {
|
||||
'name': rule.endpoint[4:],
|
||||
'methods': methods,
|
||||
'path': rule.rule,
|
||||
'parameters': list(rule.arguments)
|
||||
}
|
||||
|
||||
if is_org_api:
|
||||
route['user_method'] = endpoint_method.__user_call
|
||||
|
||||
routes.append(route)
|
||||
|
||||
route_data = {
|
||||
'endpoints': routes
|
||||
}
|
||||
return route_data
|
||||
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(username):
|
||||
logger.debug('Loading user: %s' % username)
|
||||
|
@ -68,3 +101,11 @@ def generate_csrf_token():
|
|||
return session['_csrf_token']
|
||||
|
||||
app.jinja_env.globals['csrf_token'] = generate_csrf_token
|
||||
|
||||
|
||||
def render_page_template(name, **kwargs):
|
||||
|
||||
resp = make_response(render_template(name, route_data=get_route_data(),
|
||||
**kwargs))
|
||||
resp.headers['X-FRAME-OPTIONS'] = 'DENY'
|
||||
return resp
|
111
endpoints/trigger.py
Normal file
111
endpoints/trigger.py
Normal file
|
@ -0,0 +1,111 @@
|
|||
import json
|
||||
import requests
|
||||
import logging
|
||||
|
||||
from github import Github
|
||||
|
||||
from app import app
|
||||
|
||||
|
||||
user_files = app.config['USERFILES']
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
ZIPBALL = 'application/zip'
|
||||
|
||||
|
||||
class BuildArchiveException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidServiceException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class BuildTrigger(object):
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def list_repositories(self, auth_token):
|
||||
"""
|
||||
Take the auth information for the specific trigger type and load the
|
||||
list of repositories.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def incoming_webhook(self, request, auth_token, config):
|
||||
"""
|
||||
Transform the incoming request data into a set of actions.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def service_name(cls):
|
||||
"""
|
||||
Particular service implemented by subclasses.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def get_trigger_for_service(cls, service):
|
||||
for subc in cls.__subclasses__():
|
||||
if subc.service_name() == service:
|
||||
return subc()
|
||||
|
||||
raise InvalidServiceException('Unable to find service: %s' % service)
|
||||
|
||||
|
||||
class GithubBuildTrigger(BuildTrigger):
|
||||
@staticmethod
|
||||
def _get_client(auth_token):
|
||||
return Github(auth_token, client_id=app.config['GITHUB_CLIENT_ID'],
|
||||
client_secret=app.config['GITHUB_CLIENT_SECRET'])
|
||||
|
||||
@classmethod
|
||||
def service_name(cls):
|
||||
return 'github'
|
||||
|
||||
def list_repositories(self, auth_token):
|
||||
gh_client = self._get_client(auth_token)
|
||||
usr = gh_client.get_user()
|
||||
|
||||
repo_list = [repo.full_name for repo in usr.get_repos()]
|
||||
for org in usr.get_orgs():
|
||||
repo_list.extend((repo.full_name for repo in org.get_repos()))
|
||||
|
||||
return repo_list
|
||||
|
||||
def incoming_webhook(self, request, auth_token, config):
|
||||
payload = request.get_json()
|
||||
logger.debug('Payload %s', payload)
|
||||
ref = payload['ref']
|
||||
commit_id = payload['head_commit']['id'][0:7]
|
||||
|
||||
gh_client = self._get_client(auth_token)
|
||||
|
||||
repo_full_name = '%s/%s' % (payload['repository']['owner']['name'],
|
||||
payload['repository']['name'])
|
||||
repo = gh_client.get_repo(repo_full_name)
|
||||
|
||||
logger.debug('Github repo: %s', repo)
|
||||
|
||||
# Prepare the download and upload URLs
|
||||
branch_name = ref.split('/')[-1]
|
||||
archive_link = repo.get_archive_link('zipball', branch_name)
|
||||
download_archive = requests.get(archive_link, stream=True)
|
||||
|
||||
upload_url, dockerfile_id = user_files.prepare_for_drop(ZIPBALL)
|
||||
up_headers = {'Content-Type': ZIPBALL}
|
||||
upload_archive = requests.put(upload_url, headers=up_headers,
|
||||
data=download_archive.raw)
|
||||
|
||||
if upload_archive.status_code / 100 != 2:
|
||||
logger.debug('Failed to upload archive to s3')
|
||||
raise BuildArchiveException('Unable to copy archie to s3 for ref: %s' %
|
||||
ref)
|
||||
|
||||
logger.debug('Successfully prepared job')
|
||||
|
||||
return dockerfile_id, branch_name, commit_id
|
116
endpoints/web.py
116
endpoints/web.py
|
@ -2,19 +2,18 @@ import logging
|
|||
import requests
|
||||
import stripe
|
||||
|
||||
from flask import (abort, redirect, request, url_for, render_template,
|
||||
make_response, Response, Blueprint)
|
||||
from flask.ext.login import login_required, current_user
|
||||
from flask import (abort, redirect, request, url_for, make_response, Response,
|
||||
Blueprint)
|
||||
from flask.ext.login import current_user
|
||||
from urlparse import urlparse
|
||||
|
||||
from data import model
|
||||
from app import app, mixpanel
|
||||
from app import app
|
||||
from auth.permissions import AdministerOrganizationPermission
|
||||
from util.invoice import renderInvoiceToPdf
|
||||
from util.seo import render_snapshot
|
||||
from util.cache import no_cache
|
||||
from endpoints.api import get_route_data
|
||||
from endpoints.common import common_login
|
||||
from endpoints.common import common_login, render_page_template
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -22,16 +21,7 @@ logger = logging.getLogger(__name__)
|
|||
web = Blueprint('web', __name__)
|
||||
|
||||
|
||||
def render_page_template(name, **kwargs):
|
||||
|
||||
resp = make_response(render_template(name, route_data=get_route_data(),
|
||||
**kwargs))
|
||||
resp.headers['X-FRAME-OPTIONS'] = 'DENY'
|
||||
return resp
|
||||
|
||||
|
||||
@web.route('/', methods=['GET'], defaults={'path': ''})
|
||||
@web.route('/repository/<path:path>', methods=['GET'])
|
||||
@web.route('/organization/<path:path>', methods=['GET'])
|
||||
@no_cache
|
||||
def index(path):
|
||||
|
@ -106,9 +96,10 @@ def new():
|
|||
return index('')
|
||||
|
||||
|
||||
@web.route('/repository/')
|
||||
@web.route('/repository/', defaults={'path': ''})
|
||||
@web.route('/repository/<path:path>', methods=['GET'])
|
||||
@no_cache
|
||||
def repository():
|
||||
def repository(path):
|
||||
return index('')
|
||||
|
||||
|
||||
|
@ -179,97 +170,6 @@ def receipt():
|
|||
abort(404)
|
||||
|
||||
|
||||
def exchange_github_code_for_token(code):
|
||||
code = request.args.get('code')
|
||||
payload = {
|
||||
'client_id': app.config['GITHUB_CLIENT_ID'],
|
||||
'client_secret': app.config['GITHUB_CLIENT_SECRET'],
|
||||
'code': code,
|
||||
}
|
||||
headers = {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
|
||||
get_access_token = requests.post(app.config['GITHUB_TOKEN_URL'],
|
||||
params=payload, headers=headers)
|
||||
|
||||
token = get_access_token.json()['access_token']
|
||||
return token
|
||||
|
||||
|
||||
def get_github_user(token):
|
||||
token_param = {
|
||||
'access_token': token,
|
||||
}
|
||||
get_user = requests.get(app.config['GITHUB_USER_URL'], params=token_param)
|
||||
|
||||
return get_user.json()
|
||||
|
||||
|
||||
@web.route('/oauth2/github/callback', methods=['GET'])
|
||||
def github_oauth_callback():
|
||||
error = request.args.get('error', None)
|
||||
if error:
|
||||
return render_page_template('githuberror.html', error_message=error)
|
||||
|
||||
token = exchange_github_code_for_token(request.args.get('code'))
|
||||
user_data = get_github_user(token)
|
||||
|
||||
username = user_data['login']
|
||||
github_id = user_data['id']
|
||||
|
||||
v3_media_type = {
|
||||
'Accept': 'application/vnd.github.v3'
|
||||
}
|
||||
|
||||
token_param = {
|
||||
'access_token': token,
|
||||
}
|
||||
get_email = requests.get(app.config['GITHUB_USER_EMAILS'],
|
||||
params=token_param, headers=v3_media_type)
|
||||
|
||||
# We will accept any email, but we prefer the primary
|
||||
found_email = None
|
||||
for user_email in get_email.json():
|
||||
found_email = user_email['email']
|
||||
if user_email['primary']:
|
||||
break
|
||||
|
||||
to_login = model.verify_federated_login('github', github_id)
|
||||
if not to_login:
|
||||
# try to create the user
|
||||
try:
|
||||
to_login = model.create_federated_user(username, found_email, 'github',
|
||||
github_id)
|
||||
|
||||
# Success, tell mixpanel
|
||||
mixpanel.track(to_login.username, 'register', {'service': 'github'})
|
||||
|
||||
state = request.args.get('state', None)
|
||||
if state:
|
||||
logger.debug('Aliasing with state: %s' % state)
|
||||
mixpanel.alias(to_login.username, state)
|
||||
|
||||
except model.DataModelException, ex:
|
||||
return render_page_template('githuberror.html', error_message=ex.message)
|
||||
|
||||
if common_login(to_login):
|
||||
return redirect(url_for('web.index'))
|
||||
|
||||
return render_page_template('githuberror.html')
|
||||
|
||||
|
||||
@web.route('/oauth2/github/callback/attach', methods=['GET'])
|
||||
@login_required
|
||||
def github_oauth_attach():
|
||||
token = exchange_github_code_for_token(request.args.get('code'))
|
||||
user_data = get_github_user(token)
|
||||
github_id = user_data['id']
|
||||
user_obj = current_user.db_user()
|
||||
model.attach_federated_login(user_obj, 'github', github_id)
|
||||
return redirect(url_for('web.user'))
|
||||
|
||||
|
||||
@web.route('/confirm', methods=['GET'])
|
||||
def confirm_email():
|
||||
code = request.values['code']
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import logging
|
||||
import stripe
|
||||
import urlparse
|
||||
import json
|
||||
|
||||
from flask import request, make_response, Blueprint
|
||||
|
||||
|
@ -12,11 +11,14 @@ from util.invoice import renderInvoiceToHtml
|
|||
from util.email import send_invoice_email
|
||||
from util.names import parse_repository_name
|
||||
from util.http import abort
|
||||
from endpoints.trigger import BuildTrigger
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
webhooks = Blueprint('webhooks', __name__)
|
||||
|
||||
|
||||
@webhooks.route('/stripe', methods=['POST'])
|
||||
def stripe_webhook():
|
||||
request_data = request.get_json()
|
||||
|
@ -41,22 +43,37 @@ def stripe_webhook():
|
|||
|
||||
return make_response('Okay')
|
||||
|
||||
@webhooks.route('/github/push/repository/<path:repository>', methods=['POST'])
|
||||
|
||||
@webhooks.route('/push/<path:repository>/trigger/<trigger_uuid>',
|
||||
methods=['POST'])
|
||||
@process_auth
|
||||
@parse_repository_name
|
||||
def github_push_webhook(namespace, repository):
|
||||
# data = request.get_json()
|
||||
def github_push_webhook(namespace, repository, trigger_uuid):
|
||||
logger.debug('Webhook received for %s/%s with uuid %s', namespace,
|
||||
repository, trigger_uuid)
|
||||
permission = ModifyRepositoryPermission(namespace, repository)
|
||||
if permission.can():
|
||||
payload = json.loads(request.form['payload'])
|
||||
ref = payload['ref']
|
||||
try:
|
||||
trigger = model.get_build_trigger(namespace, repository, trigger_uuid)
|
||||
except model.InvalidBuildTriggerException:
|
||||
abort(404)
|
||||
|
||||
repo = model.get_repository(namespace, repository)
|
||||
token = model.create_access_token(repo, 'write')
|
||||
handler = BuildTrigger.get_trigger_for_service(trigger.service.name)
|
||||
|
||||
logger.debug('Passing webhook request to handler %s', handler)
|
||||
df_id, tag, name = handler.incoming_webhook(request, trigger.auth_token,
|
||||
trigger.config)
|
||||
|
||||
host = urlparse.urlparse(request.url).netloc
|
||||
tag = '%s/%s/%s:latest' % (host, repo.namespace, repo.name)
|
||||
full_tag = '%s/%s/%s:%s' % (host, trigger.repository.namespace,
|
||||
trigger.repository.name, tag)
|
||||
|
||||
model.create_repository_build(repo, token, build_spec, tag)
|
||||
token = model.create_access_token(trigger.repository, 'write')
|
||||
logger.debug('Creating build %s with full_tag %s and dockerfile_id %s',
|
||||
name, full_tag, df_id)
|
||||
model.create_repository_build(trigger.repository, token, df_id, full_tag,
|
||||
name)
|
||||
|
||||
return make_response('Okay')
|
||||
|
||||
abort(403)
|
|
@ -164,6 +164,8 @@ def initialize_database():
|
|||
Visibility.create(name='private')
|
||||
LoginService.create(name='github')
|
||||
LoginService.create(name='quayrobot')
|
||||
|
||||
BuildTriggerService.create(name='github')
|
||||
|
||||
LogEntryKind.create(name='account_change_plan')
|
||||
LogEntryKind.create(name='account_change_cc')
|
||||
|
@ -200,6 +202,8 @@ def initialize_database():
|
|||
LogEntryKind.create(name='modify_prototype_permission')
|
||||
LogEntryKind.create(name='delete_prototype_permission')
|
||||
|
||||
LogEntryKind.create(name='delete_repo_trigger')
|
||||
|
||||
|
||||
def wipe_database():
|
||||
logger.debug('Wiping all data from the DB.')
|
||||
|
|
|
@ -1014,7 +1014,7 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
|
|||
fetchRepository();
|
||||
}
|
||||
|
||||
function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope) {
|
||||
function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams, $rootScope) {
|
||||
var namespace = $routeParams.namespace;
|
||||
var name = $routeParams.name;
|
||||
|
||||
|
@ -1024,6 +1024,9 @@ function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
|
|||
|
||||
$scope.permissionCache = {};
|
||||
|
||||
$scope.githubRedirectUri = KeyService.githubRedirectUri;
|
||||
$scope.githubClientId = KeyService.githubClientId;
|
||||
|
||||
$scope.buildEntityForPermission = function(name, permission, kind) {
|
||||
var key = name + ':' + kind;
|
||||
if ($scope.permissionCache[key]) {
|
||||
|
@ -1244,6 +1247,29 @@ function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
|
|||
});
|
||||
};
|
||||
|
||||
$scope.loadTriggers = function() {
|
||||
var params = {
|
||||
'repository': namespace + '/' + name
|
||||
};
|
||||
|
||||
$scope.newWebhook = {};
|
||||
$scope.triggersResource = ApiService.listBuildTriggersAsResource(params).get(function(resp) {
|
||||
$scope.triggers = resp.triggers;
|
||||
return $scope.triggers;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.deletetrigger = function(trigger) {
|
||||
var params = {
|
||||
'repository': namespace + '/' + name,
|
||||
'trigger_uuid': trigger.id
|
||||
};
|
||||
|
||||
ApiService.deleteBuildTrigger(null, params).then(function(resp) {
|
||||
$scope.triggers.splice($scope.triggers.indexOf(trigger), 1);
|
||||
});
|
||||
};
|
||||
|
||||
var fetchTokens = function() {
|
||||
var params = {
|
||||
'repository': namespace + '/' + name
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
<div class="col-md-2">
|
||||
<ul class="nav nav-pills nav-stacked">
|
||||
<li class="active"><a href="javascript:void(0)" data-toggle="tab" data-target="#permissions">Permissions</a></li>
|
||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#trigger" ng-click="loadTriggers()">Build Triggers</a></li>
|
||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#webhook" ng-click="loadWebhooks()">Webhooks</a></li>
|
||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#publicprivate">Public/Private</a></li>
|
||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#delete">Delete</a></li>
|
||||
|
@ -206,6 +207,25 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Triggers tab -->
|
||||
<div id="trigger" class="tab-pane">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">Build Triggers
|
||||
<i class="info-icon fa fa-info-circle" data-placement="left" data-content="SCM push callbacks which can cause the repository to be built and updated."></i>
|
||||
</div>
|
||||
|
||||
<div class="panel-body">
|
||||
<div class="resource-view" resource="triggersResource" error-message="'Could not load triggers'">
|
||||
<a class="btn btn-primary" href="https://github.com/login/oauth/authorize?client_id={{ githubClientId }}&scope=repo&redirect_uri={{ githubRedirectUri }}/trigger/{{ repo.namespace }}/{{ repo.name }}">Link to Github</a>
|
||||
</div>
|
||||
|
||||
<div class="right-info">
|
||||
Quay will do something.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Public/private tab -->
|
||||
<div id="publicprivate" class="tab-pane">
|
||||
<!-- Public/Private -->
|
||||
|
|
Binary file not shown.
Reference in a new issue