Port over webhooks, search, and builds.
This commit is contained in:
parent
85eb585a85
commit
e475e9809d
7 changed files with 342 additions and 3 deletions
|
@ -176,3 +176,6 @@ import endpoints.api.legacy
|
||||||
import endpoints.api.repository
|
import endpoints.api.repository
|
||||||
import endpoints.api.discovery
|
import endpoints.api.discovery
|
||||||
import endpoints.api.user
|
import endpoints.api.user
|
||||||
|
import endpoints.api.search
|
||||||
|
import endpoints.api.build
|
||||||
|
import endpoints.api.webhook
|
||||||
|
|
150
endpoints/api/build.py
Normal file
150
endpoints/api/build.py
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
import logging
|
||||||
|
import json
|
||||||
|
|
||||||
|
from flask import request, url_for
|
||||||
|
from flask.ext.restful import abort
|
||||||
|
|
||||||
|
from app import app
|
||||||
|
from endpoints.api import (RepositoryParamResource, parse_args, query_param, nickname, resource,
|
||||||
|
require_repo_read, require_repo_write, validate_json_request)
|
||||||
|
from endpoints.common import start_build
|
||||||
|
from data import model
|
||||||
|
from auth.permissions import ModifyRepositoryPermission
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
user_files = app.config['USERFILES']
|
||||||
|
build_logs = app.config['BUILDLOGS']
|
||||||
|
|
||||||
|
|
||||||
|
def build_status_view(build_obj, can_write=False):
|
||||||
|
status = build_logs.get_status(build_obj.uuid)
|
||||||
|
logger.debug('Can write: %s job_config: %s', can_write, build_obj.job_config)
|
||||||
|
build_obj.job_config = None
|
||||||
|
resp = {
|
||||||
|
'id': build_obj.uuid,
|
||||||
|
'phase': build_obj.phase,
|
||||||
|
'started': build_obj.started,
|
||||||
|
'display_name': build_obj.display_name,
|
||||||
|
'status': status,
|
||||||
|
'job_config': json.loads(build_obj.job_config) if can_write else None,
|
||||||
|
'is_writer': can_write,
|
||||||
|
'trigger': trigger_view(build_obj.trigger),
|
||||||
|
'resource_key': build_obj.resource_key,
|
||||||
|
}
|
||||||
|
if can_write:
|
||||||
|
resp['archive_url'] = user_files.get_file_url(build.resource_key)
|
||||||
|
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
@resource('/v1/repository/<path:repository>/build/')
|
||||||
|
class RepositoryBuildList(RepositoryParamResource):
|
||||||
|
""" Resource related to creating and listing repository builds. """
|
||||||
|
schemas = {
|
||||||
|
'RepositoryBuildRequest': {
|
||||||
|
'id': 'RepositoryBuildRequest',
|
||||||
|
'type': 'object',
|
||||||
|
'description': 'Description of a new repository build.',
|
||||||
|
'required': True,
|
||||||
|
'properties': {
|
||||||
|
'file_id': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'The file id that was generated when the build spec was uploaded',
|
||||||
|
'required': True,
|
||||||
|
},
|
||||||
|
'subdirectory': {
|
||||||
|
'type': 'string',
|
||||||
|
'description': 'Subdirectory in which the Dockerfile can be found',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
@parse_args
|
||||||
|
@query_param('limit', 'The maximum number of builds to return', type=int, default=5)
|
||||||
|
@require_repo_read
|
||||||
|
@nickname('getRepoBuilds')
|
||||||
|
def get(self, args, namespace, repository):
|
||||||
|
""" Get the list of repository builds. """
|
||||||
|
limit = args['limit']
|
||||||
|
builds = list(model.list_repository_builds(namespace, repository, limit))
|
||||||
|
|
||||||
|
can_write = ModifyRepositoryPermission(namespace, repository).can()
|
||||||
|
return {
|
||||||
|
'builds': [build_status_view(build, can_write) for build in builds]
|
||||||
|
}
|
||||||
|
|
||||||
|
@require_repo_write
|
||||||
|
@nickname('requestRepoBuild')
|
||||||
|
@validate_json_request('RepositoryBuildRequest')
|
||||||
|
def post(self, namespace, repository):
|
||||||
|
""" Request that a repository be built and pushed from the specified input. """
|
||||||
|
logger.debug('User requested repository initialization.')
|
||||||
|
request_json = request.get_json()
|
||||||
|
|
||||||
|
dockerfile_id = request_json['file_id']
|
||||||
|
subdir = request_json['subdirectory'] if 'subdirectory' in request_json else ''
|
||||||
|
|
||||||
|
# Check if the dockerfile resource has already been used. If so, then it
|
||||||
|
# can only be reused if the user has access to the repository for which it
|
||||||
|
# was used.
|
||||||
|
associated_repository = model.get_repository_for_resource(dockerfile_id)
|
||||||
|
if associated_repository:
|
||||||
|
if not ModifyRepositoryPermission(associated_repository.namespace,
|
||||||
|
associated_repository.name):
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
# Start the build.
|
||||||
|
repo = model.get_repository(namespace, repository)
|
||||||
|
display_name = user_files.get_file_checksum(dockerfile_id)
|
||||||
|
|
||||||
|
build_request = start_build(repo, dockerfile_id, ['latest'], display_name,
|
||||||
|
subdir, True)
|
||||||
|
|
||||||
|
resp = build_status_view(build_request, True)
|
||||||
|
repo_string = '%s/%s' % (namespace, repository)
|
||||||
|
headers = {
|
||||||
|
'Location': url_for('api_bp.get_repo_build_status', repository=repo_string,
|
||||||
|
build_uuid=build_request.uuid),
|
||||||
|
}
|
||||||
|
return resp, 201, headers
|
||||||
|
|
||||||
|
|
||||||
|
@resource('/v1/repository/<path:repository>/build/<build_uuid>/status')
|
||||||
|
class RepositoryBuildStatus(RepositoryParamResource):
|
||||||
|
""" Resource for dealing with repository build status. """
|
||||||
|
@require_repo_read
|
||||||
|
@nickname('getRepoBuildStatus')
|
||||||
|
def get(self, namespace, repository, build_uuid):
|
||||||
|
""" Return the status for the builds specified by the build uuids. """
|
||||||
|
build = model.get_repository_build(namespace, repository, build_uuid)
|
||||||
|
if not build:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
can_write = ModifyRepositoryPermission(namespace, repository).can()
|
||||||
|
return build_status_view(build, can_write)
|
||||||
|
|
||||||
|
|
||||||
|
@resource('/repository/<path:repository>/build/<build_uuid>/logs')
|
||||||
|
class RepositoryBuildLogs(RepositoryParamResource):
|
||||||
|
""" Resource for loading repository build logs. """
|
||||||
|
@require_repo_write
|
||||||
|
@nickname('getRepoBuildLogs')
|
||||||
|
def get(self, namespace, repository, build_uuid):
|
||||||
|
""" Return the build logs for the build specified by the build uuid. """
|
||||||
|
response_obj = {}
|
||||||
|
|
||||||
|
build = model.get_repository_build(namespace, repository, build_uuid)
|
||||||
|
|
||||||
|
start = int(request.args.get('start', 0))
|
||||||
|
|
||||||
|
count, logs = build_logs.get_log_entries(build.uuid, start)
|
||||||
|
|
||||||
|
response_obj.update({
|
||||||
|
'start': start,
|
||||||
|
'total': count,
|
||||||
|
'logs': [log for log in logs],
|
||||||
|
})
|
||||||
|
|
||||||
|
return response_obj
|
|
@ -339,6 +339,7 @@ def request_recovery_email():
|
||||||
return make_response('Created', 201)
|
return make_response('Created', 201)
|
||||||
|
|
||||||
|
|
||||||
|
# Ported
|
||||||
@api_bp.route('/entities/<prefix>', methods=['GET'])
|
@api_bp.route('/entities/<prefix>', methods=['GET'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
def get_matching_entities(prefix):
|
def get_matching_entities(prefix):
|
||||||
|
@ -919,6 +920,7 @@ def create_repo():
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
|
|
||||||
|
# Ported
|
||||||
@api_bp.route('/find/repository', methods=['GET'])
|
@api_bp.route('/find/repository', methods=['GET'])
|
||||||
def find_repos():
|
def find_repos():
|
||||||
prefix = request.args.get('query', '')
|
prefix = request.args.get('query', '')
|
||||||
|
@ -1158,6 +1160,7 @@ def build_status_view(build_obj, can_write=False):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Ported
|
||||||
@api_bp.route('/repository/<path:repository>/build/', methods=['GET'])
|
@api_bp.route('/repository/<path:repository>/build/', methods=['GET'])
|
||||||
@parse_repository_name
|
@parse_repository_name
|
||||||
def get_repo_builds(namespace, repository):
|
def get_repo_builds(namespace, repository):
|
||||||
|
@ -1175,6 +1178,7 @@ def get_repo_builds(namespace, repository):
|
||||||
abort(403) # Permission denied
|
abort(403) # Permission denied
|
||||||
|
|
||||||
|
|
||||||
|
# Ported
|
||||||
@api_bp.route('/repository/<path:repository>/build/<build_uuid>/status',
|
@api_bp.route('/repository/<path:repository>/build/<build_uuid>/status',
|
||||||
methods=['GET'])
|
methods=['GET'])
|
||||||
@parse_repository_name
|
@parse_repository_name
|
||||||
|
@ -1192,6 +1196,7 @@ def get_repo_build_status(namespace, repository, build_uuid):
|
||||||
abort(403) # Permission denied
|
abort(403) # Permission denied
|
||||||
|
|
||||||
|
|
||||||
|
# Ported and merged with status
|
||||||
@api_bp.route('/repository/<path:repository>/build/<build_uuid>/archiveurl',
|
@api_bp.route('/repository/<path:repository>/build/<build_uuid>/archiveurl',
|
||||||
methods=['GET'])
|
methods=['GET'])
|
||||||
@parse_repository_name
|
@parse_repository_name
|
||||||
|
@ -1210,6 +1215,7 @@ def get_repo_build_archive_url(namespace, repository, build_uuid):
|
||||||
abort(403) # Permission denied
|
abort(403) # Permission denied
|
||||||
|
|
||||||
|
|
||||||
|
# Ported
|
||||||
@api_bp.route('/repository/<path:repository>/build/<build_uuid>/logs',
|
@api_bp.route('/repository/<path:repository>/build/<build_uuid>/logs',
|
||||||
methods=['GET'])
|
methods=['GET'])
|
||||||
@parse_repository_name
|
@parse_repository_name
|
||||||
|
@ -1235,6 +1241,7 @@ def get_repo_build_logs(namespace, repository, build_uuid):
|
||||||
abort(403) # Permission denied
|
abort(403) # Permission denied
|
||||||
|
|
||||||
|
|
||||||
|
# Ported
|
||||||
@api_bp.route('/repository/<path:repository>/build/', methods=['POST'])
|
@api_bp.route('/repository/<path:repository>/build/', methods=['POST'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
@parse_repository_name
|
@parse_repository_name
|
||||||
|
@ -1281,6 +1288,7 @@ def webhook_view(webhook):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Ported
|
||||||
@api_bp.route('/repository/<path:repository>/webhook/', methods=['POST'])
|
@api_bp.route('/repository/<path:repository>/webhook/', methods=['POST'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
@parse_repository_name
|
@parse_repository_name
|
||||||
|
@ -1302,6 +1310,7 @@ def create_webhook(namespace, repository):
|
||||||
abort(403) # Permissions denied
|
abort(403) # Permissions denied
|
||||||
|
|
||||||
|
|
||||||
|
# Ported
|
||||||
@api_bp.route('/repository/<path:repository>/webhook/<public_id>',
|
@api_bp.route('/repository/<path:repository>/webhook/<public_id>',
|
||||||
methods=['GET'])
|
methods=['GET'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
|
@ -1319,6 +1328,7 @@ def get_webhook(namespace, repository, public_id):
|
||||||
abort(403) # Permission denied
|
abort(403) # Permission denied
|
||||||
|
|
||||||
|
|
||||||
|
# Ported
|
||||||
@api_bp.route('/repository/<path:repository>/webhook/', methods=['GET'])
|
@api_bp.route('/repository/<path:repository>/webhook/', methods=['GET'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
@parse_repository_name
|
@parse_repository_name
|
||||||
|
@ -1333,6 +1343,7 @@ def list_webhooks(namespace, repository):
|
||||||
abort(403) # Permission denied
|
abort(403) # Permission denied
|
||||||
|
|
||||||
|
|
||||||
|
# Ported
|
||||||
@api_bp.route('/repository/<path:repository>/webhook/<public_id>',
|
@api_bp.route('/repository/<path:repository>/webhook/<public_id>',
|
||||||
methods=['DELETE'])
|
methods=['DELETE'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
|
|
|
@ -53,8 +53,8 @@ class RepositoryList(ApiResource):
|
||||||
'type': 'string',
|
'type': 'string',
|
||||||
'description': 'Markdown encoded description for the repository',
|
'description': 'Markdown encoded description for the repository',
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@require_scope(scopes.CREATE_REPO)
|
@require_scope(scopes.CREATE_REPO)
|
||||||
|
|
105
endpoints/api/search.py
Normal file
105
endpoints/api/search.py
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
from endpoints.api import ApiResource, parse_args, query_param, truthy_bool, nickname, resource
|
||||||
|
from data import model
|
||||||
|
from auth.permissions import OrganizationMemberPermission, ViewTeamPermission
|
||||||
|
from auth.auth_context import get_authenticated_user
|
||||||
|
|
||||||
|
|
||||||
|
@resource('/v1/entities/<prefix>')
|
||||||
|
class EntitySearch(ApiResource):
|
||||||
|
""" Resource for searching entities. """
|
||||||
|
@parse_args
|
||||||
|
@query_param('namespace', 'Namespace to use when querying for org entities.', type=str,
|
||||||
|
default='')
|
||||||
|
@query_param('includeTeams', 'Whether to include team names.', type=truthy_bool, default=False)
|
||||||
|
@nickname('getMatchingEntities')
|
||||||
|
def get(self, args, prefix):
|
||||||
|
""" Get a list of entities that match the specified prefix. """
|
||||||
|
teams = []
|
||||||
|
|
||||||
|
namespace_name = args['namespace']
|
||||||
|
robot_namespace = None
|
||||||
|
organization = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
organization = model.get_organization(namespace_name)
|
||||||
|
|
||||||
|
# namespace name was an org
|
||||||
|
permission = OrganizationMemberPermission(namespace_name)
|
||||||
|
if permission.can():
|
||||||
|
robot_namespace = namespace_name
|
||||||
|
|
||||||
|
if args['includeTeams']:
|
||||||
|
teams = model.get_matching_teams(prefix, organization)
|
||||||
|
|
||||||
|
except model.InvalidOrganizationException:
|
||||||
|
# namespace name was a user
|
||||||
|
if get_authenticated_user().username == namespace_name:
|
||||||
|
robot_namespace = namespace_name
|
||||||
|
|
||||||
|
users = model.get_matching_users(prefix, robot_namespace, organization)
|
||||||
|
|
||||||
|
def entity_team_view(team):
|
||||||
|
result = {
|
||||||
|
'name': team.name,
|
||||||
|
'kind': 'team',
|
||||||
|
'is_org_member': True
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
|
||||||
|
def user_view(user):
|
||||||
|
user_json = {
|
||||||
|
'name': user.username,
|
||||||
|
'kind': 'user',
|
||||||
|
'is_robot': user.is_robot,
|
||||||
|
}
|
||||||
|
|
||||||
|
if organization is not None:
|
||||||
|
user_json['is_org_member'] = user.is_robot or user.is_org_member
|
||||||
|
|
||||||
|
return user_json
|
||||||
|
|
||||||
|
team_data = [entity_team_view(team) for team in teams]
|
||||||
|
user_data = [user_view(user) for user in users]
|
||||||
|
|
||||||
|
return {
|
||||||
|
'results': team_data + user_data
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def team_view(orgname, team):
|
||||||
|
view_permission = ViewTeamPermission(orgname, team.name)
|
||||||
|
role = model.get_team_org_role(team).name
|
||||||
|
return {
|
||||||
|
'id': team.id,
|
||||||
|
'name': team.name,
|
||||||
|
'description': team.description,
|
||||||
|
'can_view': view_permission.can(),
|
||||||
|
'role': role
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@resource('/v1/find/repository')
|
||||||
|
class FindRepositories(ApiResource):
|
||||||
|
""" Resource for finding repositories. """
|
||||||
|
@parse_args
|
||||||
|
@query_param('query', 'The prefix to use when querying for repositories.', type=str, default='')
|
||||||
|
@nickname('findRepos')
|
||||||
|
def get(self, args):
|
||||||
|
""" Get a list of repositories that match the specified prefix query. """
|
||||||
|
prefix = args['query']
|
||||||
|
|
||||||
|
def repo_view(repo):
|
||||||
|
return {
|
||||||
|
'namespace': repo.namespace,
|
||||||
|
'name': repo.name,
|
||||||
|
'description': repo.description
|
||||||
|
}
|
||||||
|
|
||||||
|
username = None
|
||||||
|
if get_authenticated_user() is not None:
|
||||||
|
username = get_authenticated_user().username
|
||||||
|
|
||||||
|
matching = model.get_matching_repositories(prefix, username)
|
||||||
|
return {
|
||||||
|
'repositories': [repo_view(repo) for repo in matching]
|
||||||
|
}
|
|
@ -156,6 +156,7 @@ class User(ApiResource):
|
||||||
@nickname('createNewUser')
|
@nickname('createNewUser')
|
||||||
@validate_json_request('NewUser')
|
@validate_json_request('NewUser')
|
||||||
def post(self):
|
def post(self):
|
||||||
|
""" Create a new user. """
|
||||||
user_data = request.get_json()
|
user_data = request.get_json()
|
||||||
|
|
||||||
existing_user = model.get_user(user_data['username'])
|
existing_user = model.get_user(user_data['username'])
|
||||||
|
|
69
endpoints/api/webhook.py
Normal file
69
endpoints/api/webhook.py
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
from flask import request, url_for
|
||||||
|
from flask.ext.restful import abort
|
||||||
|
|
||||||
|
from endpoints.api import (RepositoryParamResource, nickname, resource, require_repo_admin,
|
||||||
|
log_action)
|
||||||
|
from data import model
|
||||||
|
|
||||||
|
|
||||||
|
def webhook_view(webhook):
|
||||||
|
return {
|
||||||
|
'public_id': webhook.public_id,
|
||||||
|
'parameters': json.loads(webhook.parameters),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@resource('/v1/repository/<path:repository>/webhook/')
|
||||||
|
class WebhookList(RepositoryParamResource):
|
||||||
|
""" Resource for dealing with listing and creating webhooks. """
|
||||||
|
|
||||||
|
@require_repo_admin
|
||||||
|
@nickname('createWebhook')
|
||||||
|
def post(self, namespace, repository):
|
||||||
|
""" Create a new webhook for the specified repository. """
|
||||||
|
repo = model.get_repository(namespace, repository)
|
||||||
|
webhook = model.create_webhook(repo, request.get_json())
|
||||||
|
resp = webhook_view(webhook)
|
||||||
|
repo_string = '%s/%s' % (namespace, repository)
|
||||||
|
headers = {
|
||||||
|
'Location': url_for('api_bp.get_webhook', repository=repo_string,
|
||||||
|
public_id=webhook.public_id),
|
||||||
|
}
|
||||||
|
log_action('add_repo_webhook', namespace,
|
||||||
|
{'repo': repository, 'webhook_id': webhook.public_id},
|
||||||
|
repo=repo)
|
||||||
|
return resp, 201, headers
|
||||||
|
|
||||||
|
@require_repo_admin
|
||||||
|
@nickname('listWebhooks')
|
||||||
|
def get(self, namespace, repository):
|
||||||
|
""" List the webhooks for the specified repository. """
|
||||||
|
webhooks = model.list_webhooks(namespace, repository)
|
||||||
|
return {
|
||||||
|
'webhooks': [webhook_view(webhook) for webhook in webhooks]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@resource('/v1/repository/<path:repository>/webhook/<public_id>')
|
||||||
|
class Webhook(RepositoryParamResource):
|
||||||
|
""" Resource for dealing with specific webhooks. """
|
||||||
|
@require_repo_admin
|
||||||
|
@nickname('getWebhook')
|
||||||
|
def get(self, namespace, repository, public_id):
|
||||||
|
""" Get information for the specified webhook. """
|
||||||
|
try:
|
||||||
|
webhook = model.get_webhook(namespace, repository, public_id)
|
||||||
|
except model.InvalidWebhookException:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
return webhook_view(webhook)
|
||||||
|
|
||||||
|
@require_repo_admin
|
||||||
|
@nickname('deleteWebhook')
|
||||||
|
def delete(self, namespace, repository, public_id):
|
||||||
|
""" Delete the specified webhook. """
|
||||||
|
model.delete_webhook(namespace, repository, public_id)
|
||||||
|
log_action('delete_repo_webhook', namespace,
|
||||||
|
{'repo': repository, 'webhook_id': public_id},
|
||||||
|
repo=model.get_repository(namespace, repository))
|
||||||
|
return make_response('No Content', 204)
|
Reference in a new issue