Merge remote-tracking branch 'origin/better-error'

This commit is contained in:
yackob03 2014-01-30 13:44:33 -05:00
commit 96a97f667c
6 changed files with 233 additions and 120 deletions

View file

@ -55,6 +55,38 @@ def csrf_protect():
req_user) req_user)
@api.errorhandler(404)
def endpoint_not_found(e):
return jsonify({
'error_code': 404,
'message': 'Resource not found'
})
@api.errorhandler(403)
def endpoint_forbidden(e):
return jsonify({
'error_code': 403,
'message': 'Permission Denied'
})
@api.errorhandler(400)
def endpoint_invalid_request(e):
return jsonify({
'error_code': 400,
'message': 'Invalid Request'
})
def request_error(exception=None, **kwargs):
data = kwargs.copy()
if exception:
data['message'] = exception.message
return make_response(jsonify(data), 400)
def get_route_data(): def get_route_data():
global route_data global route_data
if route_data: if route_data:
@ -139,7 +171,7 @@ def discovery():
@api.route('/') @api.route('/')
@internal_api_call @internal_api_call
def welcome(): def welcome():
return make_response('welcome', 200) return jsonify({'version': '0.5'})
@api.route('/plans/') @api.route('/plans/')
@ -229,20 +261,14 @@ def convert_user_to_organization():
# Ensure that the new admin user is the not user being converted. # Ensure that the new admin user is the not user being converted.
admin_username = convert_data['adminUser'] admin_username = convert_data['adminUser']
if admin_username == user.username: if admin_username == user.username:
error_resp = jsonify({ return request_error(reason='invaliduser',
'reason': 'invaliduser' message='The admin user is not valid')
})
error_resp.status_code = 400
return error_resp
# Ensure that the sign in credentials work. # Ensure that the sign in credentials work.
admin_password = convert_data['adminPassword'] admin_password = convert_data['adminPassword']
if not model.verify_user(admin_username, admin_password): if not model.verify_user(admin_username, admin_password):
error_resp = jsonify({ return request_error(reason='invaliduser',
'reason': 'invaliduser' message='The admin user credentials are not valid')
})
error_resp.status_code = 400
return error_resp
# Subscribe the organization to the new plan. # Subscribe the organization to the new plan.
plan = convert_data['plan'] plan = convert_data['plan']
@ -278,22 +304,15 @@ def change_user_details():
new_email = user_data['email'] new_email = user_data['email']
if model.find_user_by_email(new_email): if model.find_user_by_email(new_email):
# Email already used. # Email already used.
error_resp = jsonify({ return request_error(message='E-mail address already used')
'message': 'E-mail address already used'
})
error_resp.status_code = 400
return error_resp
logger.debug('Sending email to change email address for user: %s', user.username) logger.debug('Sending email to change email address for user: %s',
user.username)
code = model.create_confirm_email_code(user, new_email=new_email) code = model.create_confirm_email_code(user, new_email=new_email)
send_change_email(user.username, user_data['email'], code.code) send_change_email(user.username, user_data['email'], code.code)
except model.InvalidPasswordException, ex: except model.InvalidPasswordException, ex:
error_resp = jsonify({ return request_error(exception=ex)
'message': ex.message,
})
error_resp.status_code = 400
return error_resp
return jsonify(user_view(user)) return jsonify(user_view(user))
@ -305,11 +324,7 @@ def create_new_user():
existing_user = model.get_user(user_data['username']) existing_user = model.get_user(user_data['username'])
if existing_user: if existing_user:
error_resp = jsonify({ return request_error(message='The username already exists')
'message': 'The username already exists'
})
error_resp.status_code = 400
return error_resp
try: try:
new_user = model.create_user(user_data['username'], user_data['password'], new_user = model.create_user(user_data['username'], user_data['password'],
@ -318,11 +333,7 @@ def create_new_user():
send_confirmation_email(new_user.username, new_user.email, code.code) send_confirmation_email(new_user.username, new_user.email, code.code)
return make_response('Created', 201) return make_response('Created', 201)
except model.DataModelException as ex: except model.DataModelException as ex:
error_resp = jsonify({ return request_error(exception=ex)
'message': ex.message,
})
error_resp.status_code = 400
return error_resp
@api.route('/signin', methods=['POST']) @api.route('/signin', methods=['POST'])
@ -343,7 +354,7 @@ def conduct_signin(username_or_email, password):
verified = model.verify_user(username_or_email, password) verified = model.verify_user(username_or_email, password)
if verified: if verified:
if common_login(verified): if common_login(verified):
return make_response('Success', 200) return jsonify({'success': True})
else: else:
needs_email_verification = True needs_email_verification = True
@ -364,7 +375,7 @@ def conduct_signin(username_or_email, password):
def logout(): def logout():
logout_user() logout_user()
identity_changed.send(app, identity=AnonymousIdentity()) identity_changed.send(app, identity=AnonymousIdentity())
return make_response('Success', 200) return jsonify({'success': True})
@api.route("/recovery", methods=['POST']) @api.route("/recovery", methods=['POST'])
@ -466,22 +477,15 @@ def create_organization():
pass pass
if existing: if existing:
error_resp = jsonify({ msg = 'A user or organization with this name already exists'
'message': 'A user or organization with this name already exists' return request_error(message=msg)
})
error_resp.status_code = 400
return error_resp
try: try:
model.create_organization(org_data['name'], org_data['email'], model.create_organization(org_data['name'], org_data['email'],
current_user.db_user()) current_user.db_user())
return make_response('Created', 201) return make_response('Created', 201)
except model.DataModelException as ex: except model.DataModelException as ex:
error_resp = jsonify({ return request_error(exception=ex)
'message': ex.message,
})
error_resp.status_code = 400
return error_resp
def org_view(o, teams): def org_view(o, teams):
@ -536,12 +540,7 @@ def change_organization_details(orgname):
if 'email' in org_data and org_data['email'] != org.email: if 'email' in org_data and org_data['email'] != org.email:
new_email = org_data['email'] new_email = org_data['email']
if model.find_user_by_email(new_email): if model.find_user_by_email(new_email):
# Email already used. return request_error(message='E-mail address already used')
error_resp = jsonify({
'message': 'E-mail address already used'
})
error_resp.status_code = 400
return error_resp
logger.debug('Changing email address for organization: %s', org.username) logger.debug('Changing email address for organization: %s', org.username)
model.update_email(org, new_email) model.update_email(org, new_email)
@ -626,7 +625,8 @@ def create_organization_prototype_permission(orgname):
details = request.get_json() details = request.get_json()
activating_username = None activating_username = None
if 'activating_user' in details and details['activating_user'] and 'name' in details['activating_user']: if ('activating_user' in details and details['activating_user'] and
'name' in details['activating_user']):
activating_username = details['activating_user']['name'] activating_username = details['activating_user']['name']
delegate = details['delegate'] delegate = details['delegate']
@ -644,10 +644,10 @@ def create_organization_prototype_permission(orgname):
if delegate_teamname else None) if delegate_teamname else None)
if activating_username and not activating_user: if activating_username and not activating_user:
abort(404) return request_error(message='Unknown activating user')
if not delegate_user and not delegate_team: if not delegate_user and not delegate_team:
abort(400) return request_error(message='Missing delagate user or team')
role_name = details['role'] role_name = details['role']
@ -905,7 +905,7 @@ def update_organization_team_member(orgname, teamname, membername):
# Find the user. # Find the user.
user = model.get_user(membername) user = model.get_user(membername)
if not user: if not user:
abort(400) return request_error(message='Unknown user')
# Add the user to the team. # Add the user to the team.
model.add_user_to_team(user, team) model.add_user_to_team(user, team)
@ -946,7 +946,7 @@ def create_repo():
existing = model.get_repository(namespace_name, repository_name) existing = model.get_repository(namespace_name, repository_name)
if existing: if existing:
return make_response('Repository already exists', 400) return request_error(message='Repository already exists')
visibility = req['visibility'] visibility = req['visibility']
@ -1019,7 +1019,7 @@ def list_repos():
if page: if page:
try: try:
page = int(page) page = int(page)
except: except Exception:
page = None page = None
username = None username = None
@ -1550,11 +1550,7 @@ def change_user_permissions(namespace, repository, username):
# This repository is not part of an organization # This repository is not part of an organization
pass pass
except model.DataModelException as ex: except model.DataModelException as ex:
error_resp = jsonify({ return request_error(exception=ex)
'message': ex.message,
})
error_resp.status_code = 400
return error_resp
log_action('change_repo_permission', namespace, log_action('change_repo_permission', namespace,
{'username': username, 'repo': repository, {'username': username, 'repo': repository,
@ -1607,11 +1603,7 @@ def delete_user_permissions(namespace, repository, username):
try: try:
model.delete_user_permission(username, namespace, repository) model.delete_user_permission(username, namespace, repository)
except model.DataModelException as ex: except model.DataModelException as ex:
error_resp = jsonify({ return request_error(exception=ex)
'message': ex.message,
})
error_resp.status_code = 400
return error_resp
log_action('delete_repo_permission', namespace, log_action('delete_repo_permission', namespace,
{'username': username, 'repo': repository}, {'username': username, 'repo': repository},
@ -1867,7 +1859,7 @@ def subscribe(user, plan, token, require_business_plan):
plan_found['price'] == 0): plan_found['price'] == 0):
logger.warning('Business attempting to subscribe to personal plan: %s', logger.warning('Business attempting to subscribe to personal plan: %s',
user.username) user.username)
abort(400) return request_error(message='No matching plan found')
private_repos = model.get_private_repo_count(user.username) private_repos = model.get_private_repo_count(user.username)
@ -2097,7 +2089,7 @@ def delete_user_robot(robot_shortname):
parent = current_user.db_user() parent = current_user.db_user()
model.delete_robot(format_robot_username(parent.username, robot_shortname)) model.delete_robot(format_robot_username(parent.username, robot_shortname))
log_action('delete_robot', parent.username, {'robot': robot_shortname}) log_action('delete_robot', parent.username, {'robot': robot_shortname})
return make_response('No Content', 204) return make_response('Deleted', 204)
@api.route('/organization/<orgname>/robots/<robot_shortname>', @api.route('/organization/<orgname>/robots/<robot_shortname>',
@ -2109,7 +2101,7 @@ def delete_org_robot(orgname, robot_shortname):
if permission.can(): if permission.can():
model.delete_robot(format_robot_username(orgname, robot_shortname)) model.delete_robot(format_robot_username(orgname, robot_shortname))
log_action('delete_robot', orgname, {'robot': robot_shortname}) log_action('delete_robot', orgname, {'robot': robot_shortname})
return make_response('No Content', 204) return make_response('Deleted', 204)
abort(403) abort(403)

View file

@ -2,7 +2,7 @@ import logging
import os import os
import base64 import base64
from flask import request, abort, session from flask import request, abort, session, make_response
from flask.ext.login import login_user, UserMixin from flask.ext.login import login_user, UserMixin
from flask.ext.principal import identity_changed from flask.ext.principal import identity_changed

View file

@ -2,12 +2,12 @@ import json
import logging import logging
import urlparse import urlparse
from flask import request, make_response, jsonify, abort, session, Blueprint from flask import request, make_response, jsonify, session, Blueprint
from functools import wraps from functools import wraps
from data import model from data import model
from data.queue import webhook_queue from data.queue import webhook_queue
from app import app, mixpanel from app import mixpanel
from auth.auth import (process_auth, get_authenticated_user, from auth.auth import (process_auth, get_authenticated_user,
get_validated_token) get_validated_token)
from util.names import parse_repository_name from util.names import parse_repository_name
@ -16,6 +16,9 @@ from auth.permissions import (ModifyRepositoryPermission, UserPermission,
ReadRepositoryPermission, ReadRepositoryPermission,
CreateRepositoryPermission) CreateRepositoryPermission)
from util.http import abort
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
index = Blueprint('index', __name__) index = Blueprint('index', __name__)
@ -52,6 +55,19 @@ def generate_headers(role='read'):
return decorator_method return decorator_method
@index.errorhandler(404)
def fallback_not_found(e):
return make_response('Not Found', 404)
@index.errorhandler(403)
def fallback_forbidden(e):
return make_response('Forbidden', 403)
@index.errorhandler(400)
def fallback_invalid_request(e):
return make_response('Invalid Request', 400)
@index.route('/users', methods=['POST']) @index.route('/users', methods=['POST'])
@index.route('/users/', methods=['POST']) @index.route('/users/', methods=['POST'])
def create_user(): def create_user():
@ -64,14 +80,14 @@ def create_user():
model.load_token_data(password) model.load_token_data(password)
return make_response('Verified', 201) return make_response('Verified', 201)
except model.InvalidTokenException: except model.InvalidTokenException:
return make_response('Invalid access token.', 400) abort(400, 'Invalid access token.', issue='invalid-access-token')
elif '+' in username: elif '+' in username:
try: try:
model.verify_robot(username, password) model.verify_robot(username, password)
return make_response('Verified', 201) return make_response('Verified', 201)
except model.InvalidRobotException: except model.InvalidRobotException:
return make_response('Invalid robot account or password.', 400) abort(400, 'Invalid robot account or password.', issue='robot-login-failure')
existing_user = model.get_user(username) existing_user = model.get_user(username)
if existing_user: if existing_user:
@ -79,7 +95,8 @@ def create_user():
if verified: if verified:
return make_response('Verified', 201) return make_response('Verified', 201)
else: else:
return make_response('Invalid password.', 400) abort(400, 'Invalid password.', issue='login-failure')
else: else:
# New user case # New user case
new_user = model.create_user(username, password, user_data['email']) new_user = model.create_user(username, password, user_data['email'])
@ -131,23 +148,30 @@ def update_user(username):
@generate_headers(role='write') @generate_headers(role='write')
def create_repository(namespace, repository): def create_repository(namespace, repository):
image_descriptions = json.loads(request.data) image_descriptions = json.loads(request.data)
repo = model.get_repository(namespace, repository) repo = model.get_repository(namespace, repository)
if not repo and get_authenticated_user() is None: if not repo and get_authenticated_user() is None:
logger.debug('Attempt to create new repository without user auth.') logger.debug('Attempt to create new repository without user auth.')
abort(401) abort(401,
message='Cannot create a repository as a guest. Please login via "docker login" first.',
issue='no-login')
elif repo: elif repo:
permission = ModifyRepositoryPermission(namespace, repository) permission = ModifyRepositoryPermission(namespace, repository)
if not permission.can(): if not permission.can():
abort(403) abort(403,
message='You do not have permission to modify repository %(namespace)s/%(repository)s',
issue='no-repo-write-permission',
namespace=namespace, repository=repository)
else: else:
permission = CreateRepositoryPermission(namespace) permission = CreateRepositoryPermission(namespace)
if not permission.can(): if not permission.can():
logger.info('Attempt to create a new repo with insufficient perms.') logger.info('Attempt to create a new repo with insufficient perms.')
abort(403) abort(403,
message='You do not have permission to create repositories in namespace "%(namespace)s"',
issue='no-create-permission',
namespace=namespace)
logger.debug('Creaing repository with owner: %s' % logger.debug('Creaing repository with owner: %s' %
get_authenticated_user().username) get_authenticated_user().username)
@ -200,7 +224,7 @@ def update_images(namespace, repository):
repo = model.get_repository(namespace, repository) repo = model.get_repository(namespace, repository)
if not repo: if not repo:
# Make sure the repo actually exists. # Make sure the repo actually exists.
abort(404) abort(404, message='Unknown repository', issue='unknown-repo')
image_with_checksums = json.loads(request.data) image_with_checksums = json.loads(request.data)
@ -248,7 +272,7 @@ def get_repository_images(namespace, repository):
# We can't rely on permissions to tell us if a repo exists anymore # We can't rely on permissions to tell us if a repo exists anymore
repo = model.get_repository(namespace, repository) repo = model.get_repository(namespace, repository)
if not repo: if not repo:
abort(404) abort(404, message='Unknown repository', issue='unknown-repo')
all_images = [] all_images = []
for image in model.get_repository_images(namespace, repository): for image in model.get_repository_images(namespace, repository):
@ -296,18 +320,18 @@ def get_repository_images(namespace, repository):
@parse_repository_name @parse_repository_name
@generate_headers(role='write') @generate_headers(role='write')
def delete_repository_images(namespace, repository): def delete_repository_images(namespace, repository):
return make_response('Not Implemented', 501) abort(501, 'Not Implemented', issue='not-implemented')
@index.route('/repositories/<path:repository>/auth', methods=['PUT']) @index.route('/repositories/<path:repository>/auth', methods=['PUT'])
@parse_repository_name @parse_repository_name
def put_repository_auth(namespace, repository): def put_repository_auth(namespace, repository):
return make_response('Not Implemented', 501) abort(501, 'Not Implemented', issue='not-implemented')
@index.route('/search', methods=['GET']) @index.route('/search', methods=['GET'])
def get_search(): def get_search():
return make_response('Not Implemented', 501) abort(501, 'Not Implemented', issue='not-implemented')
@index.route('/_ping') @index.route('/_ping')

View file

@ -1,8 +1,8 @@
import logging import logging
import json import json
from flask import (make_response, request, session, Response, abort, from flask import (make_response, request, session, Response, redirect,
redirect, Blueprint) Blueprint)
from functools import wraps from functools import wraps
from datetime import datetime from datetime import datetime
from time import time from time import time
@ -12,6 +12,7 @@ from data.queue import image_diff_queue
from app import app from app import app
from auth.auth import process_auth, extract_namespace_repo_from_session from auth.auth import process_auth, extract_namespace_repo_from_session
from util import checksums, changes from util import checksums, changes
from util.http import abort
from auth.permissions import (ReadRepositoryPermission, from auth.permissions import (ReadRepositoryPermission,
ModifyRepositoryPermission) ModifyRepositoryPermission)
from data import model from data import model
@ -22,6 +23,19 @@ store = app.config['STORAGE']
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@registry.errorhandler(404)
def fallback_not_found(e):
return make_response('Not Found', 404)
@registry.errorhandler(403)
def fallback_forbidden(e):
return make_response('Forbidden', 403)
@registry.errorhandler(400)
def fallback_invalid_request(e):
return make_response('Invalid Request', 400)
class SocketReader(object): class SocketReader(object):
def __init__(self, fp): def __init__(self, fp):
self._fp = fp self._fp = fp
@ -45,8 +59,9 @@ def require_completion(f):
def wrapper(namespace, repository, *args, **kwargs): def wrapper(namespace, repository, *args, **kwargs):
if store.exists(store.image_mark_path(namespace, repository, if store.exists(store.image_mark_path(namespace, repository,
kwargs['image_id'])): kwargs['image_id'])):
logger.warning('Image is already being uploaded: %s', kwargs['image_id']) abort(400, 'Image %(image_id)s is being uploaded, retry later',
abort(400) # 'Image is being uploaded, retry later') issue='upload-in-progress', image_id=kwargs['image_id'])
return f(namespace, repository, *args, **kwargs) return f(namespace, repository, *args, **kwargs)
return wrapper return wrapper
@ -90,9 +105,8 @@ def get_image_layer(namespace, repository, image_id, headers):
try: try:
return Response(store.stream_read(path), headers=headers) return Response(store.stream_read(path), headers=headers)
except IOError: except IOError:
logger.warning('Image not found: %s', image_id) abort(404, 'Image %(image_id)s not found', issue='unknown-image', image_id=image_id)
abort(404) # 'Image not found', 404)
abort(403) abort(403)
@ -108,16 +122,20 @@ def put_image_layer(namespace, repository, image_id):
json_data = store.get_content(store.image_json_path(namespace, repository, json_data = store.get_content(store.image_json_path(namespace, repository,
image_id)) image_id))
except IOError: except IOError:
abort(404) # 'Image not found', 404) abort(404, 'Image %(image_id)s not found', issue='unknown-image', image_id=image_id)
layer_path = store.image_layer_path(namespace, repository, image_id) layer_path = store.image_layer_path(namespace, repository, image_id)
mark_path = store.image_mark_path(namespace, repository, image_id) mark_path = store.image_mark_path(namespace, repository, image_id)
if store.exists(layer_path) and not store.exists(mark_path): if store.exists(layer_path) and not store.exists(mark_path):
abort(409) # 'Image already exists', 409) abort(409, 'Image already exists', issue='image-exists', image_id=image_id)
input_stream = request.stream input_stream = request.stream
if request.headers.get('transfer-encoding') == 'chunked': if request.headers.get('transfer-encoding') == 'chunked':
# Careful, might work only with WSGI servers supporting chunked # Careful, might work only with WSGI servers supporting chunked
# encoding (Gunicorn) # encoding (Gunicorn)
input_stream = request.environ['wsgi.input'] input_stream = request.environ['wsgi.input']
# compute checksums # compute checksums
csums = [] csums = []
sr = SocketReader(input_stream) sr = SocketReader(input_stream)
@ -127,6 +145,7 @@ def put_image_layer(namespace, repository, image_id):
sr.add_handler(sum_hndlr) sr.add_handler(sum_hndlr)
store.stream_write(layer_path, sr) store.stream_write(layer_path, sr)
csums.append('sha256:{0}'.format(h.hexdigest())) csums.append('sha256:{0}'.format(h.hexdigest()))
try: try:
image_size = tmp.tell() image_size = tmp.tell()
@ -139,6 +158,7 @@ def put_image_layer(namespace, repository, image_id):
except (IOError, checksums.TarError) as e: except (IOError, checksums.TarError) as e:
logger.debug('put_image_layer: Error when computing tarsum ' logger.debug('put_image_layer: Error when computing tarsum '
'{0}'.format(e)) '{0}'.format(e))
try: try:
checksum = store.get_content(store.image_checksum_path(namespace, checksum = store.get_content(store.image_checksum_path(namespace,
repository, repository,
@ -148,10 +168,13 @@ def put_image_layer(namespace, repository, image_id):
# Not removing the mark though, image is not downloadable yet. # Not removing the mark though, image is not downloadable yet.
session['checksum'] = csums session['checksum'] = csums
return make_response('true', 200) return make_response('true', 200)
# We check if the checksums provided matches one the one we computed # We check if the checksums provided matches one the one we computed
if checksum not in csums: if checksum not in csums:
logger.warning('put_image_layer: Wrong checksum') logger.warning('put_image_layer: Wrong checksum')
abort(400) # 'Checksum mismatch, ignoring the layer') abort(400, 'Checksum mismatch; ignoring the layer for image %(image_id)s',
issue='checksum-mismatch', image_id=image_id)
# Checksum is ok, we remove the marker # Checksum is ok, we remove the marker
store.remove(mark_path) store.remove(mark_path)
@ -177,24 +200,31 @@ def put_image_checksum(namespace, repository, image_id):
checksum = request.headers.get('X-Docker-Checksum') checksum = request.headers.get('X-Docker-Checksum')
if not checksum: if not checksum:
logger.warning('Missing Image\'s checksum: %s', image_id) abort(400, "Missing checksum for image %(image_id)s", issue='missing-checksum', image_id=image_id)
abort(400) # 'Missing Image\'s checksum')
if not session.get('checksum'): if not session.get('checksum'):
logger.warning('Checksum not found in Cookie for image: %s', image_id) abort(400, 'Checksum not found in Cookie for image %(imaage_id)s',
abort(400) # 'Checksum not found in Cookie') issue='missing-checksum-cookie', image_id=image_id)
if not store.exists(store.image_json_path(namespace, repository, image_id)): if not store.exists(store.image_json_path(namespace, repository, image_id)):
abort(404) # 'Image not found', 404) abort(404, 'Image not found: %(image_id)s', issue='unknown-image', image_id=image_id)
mark_path = store.image_mark_path(namespace, repository, image_id) mark_path = store.image_mark_path(namespace, repository, image_id)
if not store.exists(mark_path): if not store.exists(mark_path):
abort(409) # 'Cannot set this image checksum', 409) abort(409, 'Cannot set checksum for image %(image_id)s',
issue='image-write-error', image_id=image_id)
err = store_checksum(namespace, repository, image_id, checksum) err = store_checksum(namespace, repository, image_id, checksum)
if err: if err:
abort(err) abort(400, err)
if checksum not in session.get('checksum', []): if checksum not in session.get('checksum', []):
logger.debug('session checksums: %s' % session.get('checksum', [])) logger.debug('session checksums: %s' % session.get('checksum', []))
logger.debug('client supplied checksum: %s' % checksum) logger.debug('client supplied checksum: %s' % checksum)
logger.debug('put_image_layer: Wrong checksum') logger.debug('put_image_layer: Wrong checksum')
abort(400) # 'Checksum mismatch') abort(400, 'Checksum mismatch for image: %(image_id)s',
issue='checksum-mismatch', image_id=image_id)
# Checksum is ok, we remove the marker # Checksum is ok, we remove the marker
store.remove(mark_path) store.remove(mark_path)
@ -225,16 +255,19 @@ def get_image_json(namespace, repository, image_id, headers):
data = store.get_content(store.image_json_path(namespace, repository, data = store.get_content(store.image_json_path(namespace, repository,
image_id)) image_id))
except IOError: except IOError:
abort(404) # 'Image not found', 404) abort(404, 'Image %(image_id)%s not found', issue='unknown-image', image_id=image_id)
try: try:
size = store.get_size(store.image_layer_path(namespace, repository, size = store.get_size(store.image_layer_path(namespace, repository,
image_id)) image_id))
headers['X-Docker-Size'] = str(size) headers['X-Docker-Size'] = str(size)
except OSError: except OSError:
pass pass
checksum_path = store.image_checksum_path(namespace, repository, image_id) checksum_path = store.image_checksum_path(namespace, repository, image_id)
if store.exists(checksum_path): if store.exists(checksum_path):
headers['X-Docker-Checksum'] = store.get_content(checksum_path) headers['X-Docker-Checksum'] = store.get_content(checksum_path)
response = make_response(data, 200) response = make_response(data, 200)
response.headers.extend(headers) response.headers.extend(headers)
return response return response
@ -255,7 +288,8 @@ def get_image_ancestry(namespace, repository, image_id, headers):
data = store.get_content(store.image_ancestry_path(namespace, repository, data = store.get_content(store.image_ancestry_path(namespace, repository,
image_id)) image_id))
except IOError: except IOError:
abort(404) # 'Image not found', 404) abort(404, 'Image %(image_id)s not found', issue='unknown-image', image_id=image_id)
response = make_response(json.dumps(json.loads(data)), 200) response = make_response(json.dumps(json.loads(data)), 200)
response.headers.extend(headers) response.headers.extend(headers)
return response return response
@ -280,6 +314,7 @@ def store_checksum(namespace, repository, image_id, checksum):
checksum_parts = checksum.split(':') checksum_parts = checksum.split(':')
if len(checksum_parts) != 2: if len(checksum_parts) != 2:
return 'Invalid checksum format' return 'Invalid checksum format'
# We store the checksum # We store the checksum
checksum_path = store.image_checksum_path(namespace, repository, image_id) checksum_path = store.image_checksum_path(namespace, repository, image_id)
store.put_content(checksum_path, checksum) store.put_content(checksum_path, checksum)
@ -298,36 +333,39 @@ def put_image_json(namespace, repository, image_id):
except json.JSONDecodeError: except json.JSONDecodeError:
pass pass
if not data or not isinstance(data, dict): if not data or not isinstance(data, dict):
logger.warning('Invalid JSON for image: %s json: %s', image_id, abort(400, 'Invalid JSON for image: %(image_id)s\nJSON: %(json)s',
request.data) issue='invalid-request', image_id=image_id, json=request.data)
abort(400) # 'Invalid JSON')
if 'id' not in data: if 'id' not in data:
logger.warning('Missing key `id\' in JSON for image: %s', image_id) abort(400, 'Missing key `id` in JSON for image: %(image_id)s',
abort(400) # 'Missing key `id\' in JSON') issue='invalid-request', image_id=image_id)
# Read the checksum # Read the checksum
checksum = request.headers.get('X-Docker-Checksum') checksum = request.headers.get('X-Docker-Checksum')
if checksum: if checksum:
# Storing the checksum is optional at this stage # Storing the checksum is optional at this stage
err = store_checksum(namespace, repository, image_id, checksum) err = store_checksum(namespace, repository, image_id, checksum)
if err: if err:
abort(err) abort(400, err, issue='write-error')
else: else:
# We cleanup any old checksum in case it's a retry after a fail # We cleanup any old checksum in case it's a retry after a fail
store.remove(store.image_checksum_path(namespace, repository, image_id)) store.remove(store.image_checksum_path(namespace, repository, image_id))
if image_id != data['id']: if image_id != data['id']:
logger.warning('JSON data contains invalid id for image: %s', image_id) abort(400, 'JSON data contains invalid id for image: %(image_id)s',
abort(400) # 'JSON data contains invalid id') issue='invalid-request', image_id=image_id)
parent_id = data.get('parent') parent_id = data.get('parent')
if parent_id and not store.exists(store.image_json_path(namespace, if (parent_id and not
repository, store.exists(store.image_json_path(namespace, repository, parent_id))):
data['parent'])): abort(400, 'Image %(image_id)s depends on non existing parent image %(parent_id)s',
logger.warning('Image depends on a non existing parent image: %s', issue='invalid-request', image_id=image_id, parent_id=parent_id)
image_id)
abort(400) # 'Image depends on a non existing parent')
json_path = store.image_json_path(namespace, repository, image_id) json_path = store.image_json_path(namespace, repository, image_id)
mark_path = store.image_mark_path(namespace, repository, image_id) mark_path = store.image_mark_path(namespace, repository, image_id)
if store.exists(json_path) and not store.exists(mark_path): if store.exists(json_path) and not store.exists(mark_path):
abort(409) # 'Image already exists', 409) abort(409, 'Image already exists', issue='image-exists', image_id=image_id)
# If we reach that point, it means that this is a new image or a retry # If we reach that point, it means that this is a new image or a retry
# on a failed push # on a failed push
# save the metadata # save the metadata

View file

@ -1242,7 +1242,7 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService
$location.path('/repository/' + created.namespace + '/' + created.name); $location.path('/repository/' + created.namespace + '/' + created.name);
}, function(result) { }, function(result) {
$scope.creating = false; $scope.creating = false;
$scope.createError = result.data; $scope.createError = result.data ? result.data.message : 'Cannot create repository';
$timeout(function() { $timeout(function() {
$('#repoName').popover('show'); $('#repoName').popover('show');
}); });

59
util/http.py Normal file
View file

@ -0,0 +1,59 @@
import logging
from app import mixpanel
from flask import request, abort as flask_abort, jsonify
from auth.auth import get_authenticated_user, get_validated_token
logger = logging.getLogger(__name__)
DEFAULT_MESSAGE = {}
DEFAULT_MESSAGE[400] = 'Invalid Request'
DEFAULT_MESSAGE[401] = 'Unauthorized'
DEFAULT_MESSAGE[403] = 'Permission Denied'
DEFAULT_MESSAGE[404] = 'Not Found'
DEFAULT_MESSAGE[409] = 'Conflict'
DEFAULT_MESSAGE[501] = 'Not Implemented'
def abort(status_code, message=None, issue=None, **kwargs):
message = (str(message) % kwargs if message else
DEFAULT_MESSAGE.get(status_code, ''))
params = dict(request.view_args)
params.update(kwargs)
params['url'] = request.url
params['status_code'] = status_code
params['message'] = message
# Add the user information.
auth_user = get_authenticated_user()
auth_token = get_validated_token()
if auth_user:
mixpanel.track(auth_user.username, 'http_error', params)
message = '%s (user: %s)' % (message, auth_user.username)
elif auth_token:
mixpanel.track(auth_token.code, 'http_error', params)
message = '%s (token: %s)' % (message,
auth_token.friendly_name or auth_token.code)
# Log the abort.
logger.error('Error %s: %s; Arguments: %s' % (status_code, message, params))
# Calculate the issue URL (if the issue ID was supplied).
issue_url = None
if issue:
issue_url = 'http://docs.quay.io/issues/%s.html' % (issue)
# Create the final response data and message.
data = {}
data['error'] = message
if issue_url:
data['info_url'] = issue_url
resp = jsonify(data)
resp.status_code = status_code
# Report the abort to the user.
flask_abort(resp)