use kwargs for parse_repository_name
This commit is contained in:
parent
3b52a255b2
commit
bb46cc933d
15 changed files with 285 additions and 270 deletions
|
@ -1,8 +1,9 @@
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from jsonschema import validate, ValidationError
|
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
|
from jsonschema import validate, ValidationError
|
||||||
from flask import request, url_for
|
from flask import request, url_for
|
||||||
from flask.ext.principal import identity_changed, Identity
|
from flask.ext.principal import identity_changed, Identity
|
||||||
from cryptography.x509 import load_pem_x509_certificate
|
from cryptography.x509 import load_pem_x509_certificate
|
||||||
|
|
|
@ -3,10 +3,11 @@
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from flask import request, url_for
|
|
||||||
from urllib import quote
|
from urllib import quote
|
||||||
from urlparse import urlunparse
|
from urlparse import urlunparse
|
||||||
|
|
||||||
|
from flask import request, url_for
|
||||||
|
|
||||||
from app import app
|
from app import app
|
||||||
from buildtrigger.basehandler import BuildTriggerHandler
|
from buildtrigger.basehandler import BuildTriggerHandler
|
||||||
from buildtrigger.triggerutil import (TriggerDeactivationException,
|
from buildtrigger.triggerutil import (TriggerDeactivationException,
|
||||||
|
@ -40,9 +41,9 @@ class BuildTriggerList(RepositoryParamResource):
|
||||||
|
|
||||||
@require_repo_admin
|
@require_repo_admin
|
||||||
@nickname('listBuildTriggers')
|
@nickname('listBuildTriggers')
|
||||||
def get(self, namespace, repository):
|
def get(self, namespace_name, repo_name):
|
||||||
""" List the triggers for the specified repository. """
|
""" List the triggers for the specified repository. """
|
||||||
triggers = model.build.list_build_triggers(namespace, repository)
|
triggers = model.build.list_build_triggers(namespace_name, repo_name)
|
||||||
return {
|
return {
|
||||||
'triggers': [trigger_view(trigger, can_admin=True) for trigger in triggers]
|
'triggers': [trigger_view(trigger, can_admin=True) for trigger in triggers]
|
||||||
}
|
}
|
||||||
|
@ -56,7 +57,7 @@ class BuildTrigger(RepositoryParamResource):
|
||||||
|
|
||||||
@require_repo_admin
|
@require_repo_admin
|
||||||
@nickname('getBuildTrigger')
|
@nickname('getBuildTrigger')
|
||||||
def get(self, namespace, repository, trigger_uuid):
|
def get(self, namespace_name, repo_name, trigger_uuid):
|
||||||
""" Get information for the specified build trigger. """
|
""" Get information for the specified build trigger. """
|
||||||
try:
|
try:
|
||||||
trigger = model.build.get_build_trigger(trigger_uuid)
|
trigger = model.build.get_build_trigger(trigger_uuid)
|
||||||
|
@ -67,7 +68,7 @@ class BuildTrigger(RepositoryParamResource):
|
||||||
|
|
||||||
@require_repo_admin
|
@require_repo_admin
|
||||||
@nickname('deleteBuildTrigger')
|
@nickname('deleteBuildTrigger')
|
||||||
def delete(self, namespace, repository, trigger_uuid):
|
def delete(self, namespace_name, repo_name, trigger_uuid):
|
||||||
""" Delete the specified build trigger. """
|
""" Delete the specified build trigger. """
|
||||||
try:
|
try:
|
||||||
trigger = model.build.get_build_trigger(trigger_uuid)
|
trigger = model.build.get_build_trigger(trigger_uuid)
|
||||||
|
@ -82,10 +83,10 @@ class BuildTrigger(RepositoryParamResource):
|
||||||
# We are just going to eat this error
|
# We are just going to eat this error
|
||||||
logger.warning('Trigger deactivation problem: %s', ex)
|
logger.warning('Trigger deactivation problem: %s', ex)
|
||||||
|
|
||||||
log_action('delete_repo_trigger', namespace,
|
log_action('delete_repo_trigger', namespace_name,
|
||||||
{'repo': repository, 'trigger_id': trigger_uuid,
|
{'repo': repo_name, 'trigger_id': trigger_uuid,
|
||||||
'service': trigger.service.name},
|
'service': trigger.service.name},
|
||||||
repo=model.repository.get_repository(namespace, repository))
|
repo=model.repository.get_repository(namespace_name, repo_name))
|
||||||
|
|
||||||
trigger.delete_instance(recursive=True)
|
trigger.delete_instance(recursive=True)
|
||||||
|
|
||||||
|
@ -111,7 +112,7 @@ class BuildTriggerSubdirs(RepositoryParamResource):
|
||||||
@require_repo_admin
|
@require_repo_admin
|
||||||
@nickname('listBuildTriggerSubdirs')
|
@nickname('listBuildTriggerSubdirs')
|
||||||
@validate_json_request('BuildTriggerSubdirRequest')
|
@validate_json_request('BuildTriggerSubdirRequest')
|
||||||
def post(self, namespace, repository, trigger_uuid):
|
def post(self, namespace_name, repo_name, trigger_uuid):
|
||||||
""" List the subdirectories available for the specified build trigger and source. """
|
""" List the subdirectories available for the specified build trigger and source. """
|
||||||
try:
|
try:
|
||||||
trigger = model.build.get_build_trigger(trigger_uuid)
|
trigger = model.build.get_build_trigger(trigger_uuid)
|
||||||
|
@ -171,7 +172,7 @@ class BuildTriggerActivate(RepositoryParamResource):
|
||||||
@require_repo_admin
|
@require_repo_admin
|
||||||
@nickname('activateBuildTrigger')
|
@nickname('activateBuildTrigger')
|
||||||
@validate_json_request('BuildTriggerActivateRequest')
|
@validate_json_request('BuildTriggerActivateRequest')
|
||||||
def post(self, namespace, repository, trigger_uuid):
|
def post(self, namespace_name, repo_name, trigger_uuid):
|
||||||
""" Activate the specified build trigger. """
|
""" Activate the specified build trigger. """
|
||||||
try:
|
try:
|
||||||
trigger = model.build.get_build_trigger(trigger_uuid)
|
trigger = model.build.get_build_trigger(trigger_uuid)
|
||||||
|
@ -198,7 +199,7 @@ class BuildTriggerActivate(RepositoryParamResource):
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
# Make sure the namespace matches that of the trigger.
|
# Make sure the namespace matches that of the trigger.
|
||||||
if robot_namespace != namespace:
|
if robot_namespace != namespace_name:
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
|
|
||||||
# Set the pull robot.
|
# Set the pull robot.
|
||||||
|
@ -208,7 +209,7 @@ class BuildTriggerActivate(RepositoryParamResource):
|
||||||
new_config_dict = request.get_json()['config']
|
new_config_dict = request.get_json()['config']
|
||||||
|
|
||||||
write_token_name = 'Build Trigger: %s' % trigger.service.name
|
write_token_name = 'Build Trigger: %s' % trigger.service.name
|
||||||
write_token = model.token.create_delegate_token(namespace, repository, write_token_name,
|
write_token = model.token.create_delegate_token(namespace_name, repo_name, write_token_name,
|
||||||
'write')
|
'write')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -233,12 +234,13 @@ class BuildTriggerActivate(RepositoryParamResource):
|
||||||
trigger.save()
|
trigger.save()
|
||||||
|
|
||||||
# Log the trigger setup.
|
# Log the trigger setup.
|
||||||
repo = model.repository.get_repository(namespace, repository)
|
repo = model.repository.get_repository(namespace_name, repo_name)
|
||||||
log_action('setup_repo_trigger', namespace,
|
log_action('setup_repo_trigger', namespace_name,
|
||||||
{'repo': repository, 'namespace': namespace,
|
{'repo': repo_name, 'namespace': namespace_name,
|
||||||
'trigger_id': trigger.uuid, 'service': trigger.service.name,
|
'trigger_id': trigger.uuid, 'service': trigger.service.name,
|
||||||
'pull_robot': trigger.pull_robot.username if trigger.pull_robot else None,
|
'pull_robot': trigger.pull_robot.username if trigger.pull_robot else None,
|
||||||
'config': final_config}, repo=repo)
|
'config': final_config},
|
||||||
|
repo=repo)
|
||||||
|
|
||||||
return trigger_view(trigger, can_admin=True)
|
return trigger_view(trigger, can_admin=True)
|
||||||
else:
|
else:
|
||||||
|
@ -271,7 +273,7 @@ class BuildTriggerAnalyze(RepositoryParamResource):
|
||||||
@require_repo_admin
|
@require_repo_admin
|
||||||
@nickname('analyzeBuildTrigger')
|
@nickname('analyzeBuildTrigger')
|
||||||
@validate_json_request('BuildTriggerAnalyzeRequest')
|
@validate_json_request('BuildTriggerAnalyzeRequest')
|
||||||
def post(self, namespace, repository, trigger_uuid):
|
def post(self, namespace_name, repo_name, trigger_uuid):
|
||||||
""" Analyze the specified build trigger configuration. """
|
""" Analyze the specified build trigger configuration. """
|
||||||
try:
|
try:
|
||||||
trigger = model.build.get_build_trigger(trigger_uuid)
|
trigger = model.build.get_build_trigger(trigger_uuid)
|
||||||
|
@ -414,7 +416,7 @@ class ActivateBuildTrigger(RepositoryParamResource):
|
||||||
@require_repo_admin
|
@require_repo_admin
|
||||||
@nickname('manuallyStartBuildTrigger')
|
@nickname('manuallyStartBuildTrigger')
|
||||||
@validate_json_request('RunParameters')
|
@validate_json_request('RunParameters')
|
||||||
def post(self, namespace, repository, trigger_uuid):
|
def post(self, namespace_name, repo_name, trigger_uuid):
|
||||||
""" Manually start a build from the specified trigger. """
|
""" Manually start a build from the specified trigger. """
|
||||||
try:
|
try:
|
||||||
trigger = model.build.get_build_trigger(trigger_uuid)
|
trigger = model.build.get_build_trigger(trigger_uuid)
|
||||||
|
@ -426,7 +428,7 @@ class ActivateBuildTrigger(RepositoryParamResource):
|
||||||
raise InvalidRequest('Trigger is not active.')
|
raise InvalidRequest('Trigger is not active.')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
repo = model.repository.get_repository(namespace, repository)
|
repo = model.repository.get_repository(namespace_name, repo_name)
|
||||||
pull_robot_name = model.build.get_pull_robot_name(trigger)
|
pull_robot_name = model.build.get_pull_robot_name(trigger)
|
||||||
|
|
||||||
run_parameters = request.get_json()
|
run_parameters = request.get_json()
|
||||||
|
@ -436,7 +438,7 @@ class ActivateBuildTrigger(RepositoryParamResource):
|
||||||
raise InvalidRequest(tse.message)
|
raise InvalidRequest(tse.message)
|
||||||
|
|
||||||
resp = build_status_view(build_request)
|
resp = build_status_view(build_request)
|
||||||
repo_string = '%s/%s' % (namespace, repository)
|
repo_string = '%s/%s' % (namespace_name, repo_name)
|
||||||
headers = {
|
headers = {
|
||||||
'Location': api.url_for(RepositoryBuildStatus, repository=repo_string,
|
'Location': api.url_for(RepositoryBuildStatus, repository=repo_string,
|
||||||
build_uuid=build_request.uuid),
|
build_uuid=build_request.uuid),
|
||||||
|
@ -453,10 +455,10 @@ class TriggerBuildList(RepositoryParamResource):
|
||||||
@parse_args()
|
@parse_args()
|
||||||
@query_param('limit', 'The maximum number of builds to return', type=int, default=5)
|
@query_param('limit', 'The maximum number of builds to return', type=int, default=5)
|
||||||
@nickname('listTriggerRecentBuilds')
|
@nickname('listTriggerRecentBuilds')
|
||||||
def get(self, namespace, repository, trigger_uuid, parsed_args):
|
def get(self, namespace_name, repo_name, trigger_uuid, parsed_args):
|
||||||
""" List the builds started by the specified trigger. """
|
""" List the builds started by the specified trigger. """
|
||||||
limit = parsed_args['limit']
|
limit = parsed_args['limit']
|
||||||
builds = model.build.list_trigger_builds(namespace, repository, trigger_uuid, limit)
|
builds = model.build.list_trigger_builds(namespace_name, repo_name, trigger_uuid, limit)
|
||||||
return {
|
return {
|
||||||
'builds': [build_status_view(bld) for bld in builds]
|
'builds': [build_status_view(bld) for bld in builds]
|
||||||
}
|
}
|
||||||
|
@ -470,7 +472,7 @@ class BuildTriggerFieldValues(RepositoryParamResource):
|
||||||
""" Custom verb to fetch a values list for a particular field name. """
|
""" Custom verb to fetch a values list for a particular field name. """
|
||||||
@require_repo_admin
|
@require_repo_admin
|
||||||
@nickname('listTriggerFieldValues')
|
@nickname('listTriggerFieldValues')
|
||||||
def post(self, namespace, repository, trigger_uuid, field_name):
|
def post(self, namespace_name, repo_name, trigger_uuid, field_name):
|
||||||
""" List the field values for a custom run field. """
|
""" List the field values for a custom run field. """
|
||||||
try:
|
try:
|
||||||
trigger = model.build.get_build_trigger(trigger_uuid)
|
trigger = model.build.get_build_trigger(trigger_uuid)
|
||||||
|
@ -501,7 +503,7 @@ class BuildTriggerSources(RepositoryParamResource):
|
||||||
""" Custom verb to fetch the list of build sources for the trigger config. """
|
""" Custom verb to fetch the list of build sources for the trigger config. """
|
||||||
@require_repo_admin
|
@require_repo_admin
|
||||||
@nickname('listTriggerBuildSources')
|
@nickname('listTriggerBuildSources')
|
||||||
def get(self, namespace, repository, trigger_uuid):
|
def get(self, namespace_name, repo_name, trigger_uuid):
|
||||||
""" List the build sources for the trigger configuration thus far. """
|
""" List the build sources for the trigger configuration thus far. """
|
||||||
try:
|
try:
|
||||||
trigger = model.build.get_build_trigger(trigger_uuid)
|
trigger = model.build.get_build_trigger(trigger_uuid)
|
||||||
|
|
|
@ -47,24 +47,28 @@ def get_cache_busters():
|
||||||
return CACHE_BUSTERS
|
return CACHE_BUSTERS
|
||||||
|
|
||||||
|
|
||||||
def parse_repository_name(f):
|
def parse_repository_name(include_tag=False,
|
||||||
@wraps(f)
|
ns_kwarg_name='namespace_name',
|
||||||
def wrapper(repository, *args, **kwargs):
|
repo_kwarg_name='repo_name',
|
||||||
lib_namespace = app.config['LIBRARY_NAMESPACE']
|
tag_kwarg_name='tag_name',
|
||||||
(namespace, repository) = parse_namespace_repository(repository, lib_namespace)
|
incoming_repo_kwarg='repository'):
|
||||||
return f(namespace, repository, *args, **kwargs)
|
def inner(func):
|
||||||
return wrapper
|
@wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
parsed_stuff = parse_namespace_repository(kwargs[incoming_repo_kwarg],
|
||||||
|
app.config['LIBRARY_NAMESPACE'],
|
||||||
|
include_tag=include_tag)
|
||||||
|
del kwargs[incoming_repo_kwarg]
|
||||||
|
kwargs[ns_kwarg_name] = parsed_stuff[0]
|
||||||
|
kwargs[repo_kwarg_name] = parsed_stuff[1]
|
||||||
|
if include_tag:
|
||||||
|
kwargs[tag_kwarg_name] = parsed_stuff[2]
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
return wrapper
|
||||||
|
return inner
|
||||||
|
|
||||||
|
|
||||||
def parse_repository_name_and_tag(f):
|
# TODO get rid of all calls to this parse_repository_name_and_tag
|
||||||
@wraps(f)
|
|
||||||
def wrapper(repository, *args, **kwargs):
|
|
||||||
lib_namespace = app.config['LIBRARY_NAMESPACE']
|
|
||||||
namespace, repository, tag = parse_namespace_repository(repository, lib_namespace,
|
|
||||||
include_tag=True)
|
|
||||||
return f(namespace, repository, tag, *args, **kwargs)
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
|
|
||||||
def route_show_if(value):
|
def route_show_if(value):
|
||||||
def decorator(f):
|
def decorator(f):
|
||||||
|
|
|
@ -3,35 +3,37 @@ import logging
|
||||||
from flask import request, redirect, url_for, Blueprint
|
from flask import request, redirect, url_for, Blueprint
|
||||||
from flask.ext.login import current_user
|
from flask.ext.login import current_user
|
||||||
|
|
||||||
from endpoints.common import route_show_if, parse_repository_name
|
|
||||||
from app import app, github_trigger
|
|
||||||
from data import model
|
|
||||||
from util.http import abort
|
|
||||||
from auth.permissions import AdministerRepositoryPermission
|
|
||||||
from auth.auth import require_session_login
|
|
||||||
|
|
||||||
import features
|
import features
|
||||||
|
|
||||||
|
from app import app, github_trigger
|
||||||
|
from auth.auth import require_session_login
|
||||||
|
from auth.permissions import AdministerRepositoryPermission
|
||||||
|
from data import model
|
||||||
|
from endpoints.common import route_show_if, parse_repository_name
|
||||||
|
from util.http import abort
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
client = app.config['HTTPCLIENT']
|
client = app.config['HTTPCLIENT']
|
||||||
githubtrigger = Blueprint('callback', __name__)
|
githubtrigger = Blueprint('callback', __name__)
|
||||||
|
|
||||||
|
|
||||||
@githubtrigger.route('/github/callback/trigger/<repopath:repository>', methods=['GET'])
|
@githubtrigger.route('/github/callback/trigger/<repopath:repository>', methods=['GET'])
|
||||||
@route_show_if(features.GITHUB_BUILD)
|
@route_show_if(features.GITHUB_BUILD)
|
||||||
@require_session_login
|
@require_session_login
|
||||||
@parse_repository_name
|
@parse_repository_name()
|
||||||
def attach_github_build_trigger(namespace, repository):
|
def attach_github_build_trigger(namespace_name, repo_name):
|
||||||
permission = AdministerRepositoryPermission(namespace, repository)
|
permission = AdministerRepositoryPermission(namespace_name, repo_name)
|
||||||
if permission.can():
|
if permission.can():
|
||||||
code = request.args.get('code')
|
code = request.args.get('code')
|
||||||
token = github_trigger.exchange_code_for_token(app.config, client, code)
|
token = github_trigger.exchange_code_for_token(app.config, client, code)
|
||||||
repo = model.repository.get_repository(namespace, repository)
|
repo = model.repository.get_repository(namespace_name, repo_name)
|
||||||
if not repo:
|
if not repo:
|
||||||
msg = 'Invalid repository: %s/%s' % (namespace, repository)
|
msg = 'Invalid repository: %s/%s' % (namespace_name, repo_name)
|
||||||
abort(404, message=msg)
|
abort(404, message=msg)
|
||||||
|
|
||||||
trigger = model.build.create_build_trigger(repo, 'github', token, current_user.db_user())
|
trigger = model.build.create_build_trigger(repo, 'github', token, current_user.db_user())
|
||||||
repo_path = '%s/%s' % (namespace, repository)
|
repo_path = '%s/%s' % (namespace_name, repo_name)
|
||||||
full_url = '%s%s%s' % (url_for('web.repository', path=repo_path), '?tab=builds&newtrigger=',
|
full_url = '%s%s%s' % (url_for('web.repository', path=repo_path), '?tab=builds&newtrigger=',
|
||||||
trigger.uuid)
|
trigger.uuid)
|
||||||
|
|
||||||
|
|
|
@ -3,18 +3,18 @@ import requests
|
||||||
|
|
||||||
from flask import request, redirect, url_for, Blueprint
|
from flask import request, redirect, url_for, Blueprint
|
||||||
from flask.ext.login import current_user
|
from flask.ext.login import current_user
|
||||||
|
|
||||||
from endpoints.common import common_login, route_show_if, parse_repository_name
|
|
||||||
from endpoints.web import render_page_template_with_routedata
|
|
||||||
from app import app, analytics, get_app_url, github_login, google_login, dex_login
|
|
||||||
from data import model
|
|
||||||
from util.validation import generate_valid_usernames
|
|
||||||
from util.http import abort
|
|
||||||
from auth.auth import require_session_login
|
|
||||||
from peewee import IntegrityError
|
from peewee import IntegrityError
|
||||||
|
|
||||||
import features
|
import features
|
||||||
|
|
||||||
|
from app import app, analytics, get_app_url, github_login, google_login, dex_login
|
||||||
|
from auth.auth import require_session_login
|
||||||
|
from data import model
|
||||||
|
from endpoints.common import common_login, route_show_if
|
||||||
|
from endpoints.web import render_page_template_with_routedata
|
||||||
from util.security.strictjwt import decode, InvalidTokenError
|
from util.security.strictjwt import decode, InvalidTokenError
|
||||||
|
from util.validation import generate_valid_usernames
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
client = app.config['HTTPCLIENT']
|
client = app.config['HTTPCLIENT']
|
||||||
|
@ -24,10 +24,10 @@ def render_ologin_error(service_name,
|
||||||
error_message='Could not load user data. The token may have expired.'):
|
error_message='Could not load user data. The token may have expired.'):
|
||||||
user_creation = features.USER_CREATION and features.DIRECT_LOGIN
|
user_creation = features.USER_CREATION and features.DIRECT_LOGIN
|
||||||
return render_page_template_with_routedata('ologinerror.html',
|
return render_page_template_with_routedata('ologinerror.html',
|
||||||
service_name=service_name,
|
service_name=service_name,
|
||||||
error_message=error_message,
|
error_message=error_message,
|
||||||
service_url=get_app_url(),
|
service_url=get_app_url(),
|
||||||
user_creation=user_creation)
|
user_creation=user_creation)
|
||||||
|
|
||||||
|
|
||||||
def get_user(service, token):
|
def get_user(service, token):
|
||||||
|
@ -150,7 +150,7 @@ def github_oauth_callback():
|
||||||
# Retrieve the user's orgnizations (if organization filtering is turned on)
|
# Retrieve the user's orgnizations (if organization filtering is turned on)
|
||||||
if github_login.allowed_organizations() is not None:
|
if github_login.allowed_organizations() is not None:
|
||||||
get_orgs = client.get(github_login.orgs_endpoint(), params=token_param,
|
get_orgs = client.get(github_login.orgs_endpoint(), params=token_param,
|
||||||
headers={'Accept': 'application/vnd.github.moondragon+json'})
|
headers={'Accept': 'application/vnd.github.moondragon+json'})
|
||||||
|
|
||||||
organizations = set([org.get('login').lower() for org in get_orgs.json()])
|
organizations = set([org.get('login').lower() for org in get_orgs.json()])
|
||||||
if not (organizations & set(github_login.allowed_organizations())):
|
if not (organizations & set(github_login.allowed_organizations())):
|
||||||
|
@ -271,8 +271,10 @@ def dex_oauth_callback():
|
||||||
payload = decode_user_jwt(token, dex_login)
|
payload = decode_user_jwt(token, dex_login)
|
||||||
except InvalidTokenError:
|
except InvalidTokenError:
|
||||||
logger.exception('Exception when decoding returned JWT')
|
logger.exception('Exception when decoding returned JWT')
|
||||||
return render_ologin_error(dex_login.public_title,
|
return render_ologin_error(
|
||||||
'Could not decode response. Please contact your system administrator about this error.')
|
dex_login.public_title,
|
||||||
|
'Could not decode response. Please contact your system administrator about this error.',
|
||||||
|
)
|
||||||
|
|
||||||
username = get_email_username(payload)
|
username = get_email_username(payload)
|
||||||
metadata = {}
|
metadata = {}
|
||||||
|
@ -281,9 +283,11 @@ def dex_oauth_callback():
|
||||||
email_address = payload['email']
|
email_address = payload['email']
|
||||||
|
|
||||||
if not payload.get('email_verified', False):
|
if not payload.get('email_verified', False):
|
||||||
return render_ologin_error(dex_login.public_title,
|
return render_ologin_error(
|
||||||
|
dex_login.public_title,
|
||||||
'A verified e-mail address is required for login. Please verify your ' +
|
'A verified e-mail address is required for login. Please verify your ' +
|
||||||
'e-mail address in %s and try again.' % dex_login.public_title)
|
'e-mail address in %s and try again.' % dex_login.public_title,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
return conduct_oauth_login(dex_login, dex_id, username, email_address,
|
return conduct_oauth_login(dex_login, dex_id, username, email_address,
|
||||||
|
@ -304,8 +308,10 @@ def dex_oauth_attach():
|
||||||
payload = decode_user_jwt(token, dex_login)
|
payload = decode_user_jwt(token, dex_login)
|
||||||
except jwt.InvalidTokenError:
|
except jwt.InvalidTokenError:
|
||||||
logger.exception('Exception when decoding returned JWT')
|
logger.exception('Exception when decoding returned JWT')
|
||||||
return render_ologin_error(dex_login.public_title,
|
return render_ologin_error(
|
||||||
'Could not decode response. Please contact your system administrator about this error.')
|
dex_login.public_title,
|
||||||
|
'Could not decode response. Please contact your system administrator about this error.',
|
||||||
|
)
|
||||||
|
|
||||||
user_obj = current_user.db_user()
|
user_obj = current_user.db_user()
|
||||||
dex_id = payload['sub']
|
dex_id = payload['sub']
|
||||||
|
@ -315,7 +321,7 @@ def dex_oauth_attach():
|
||||||
model.user.attach_federated_login(user_obj, 'dex', dex_id, metadata=metadata)
|
model.user.attach_federated_login(user_obj, 'dex', dex_id, metadata=metadata)
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
err = '%s account is already attached to a %s account' % (dex_login.public_title,
|
err = '%s account is already attached to a %s account' % (dex_login.public_title,
|
||||||
app.config['REGISTRY_TITLE_SHORT'])
|
app.config['REGISTRY_TITLE_SHORT'])
|
||||||
return render_ologin_error(dex_login.public_title, err)
|
return render_ologin_error(dex_login.public_title, err)
|
||||||
|
|
||||||
return redirect(url_for('web.user'))
|
return redirect(url_for('web.user'))
|
||||||
|
|
|
@ -2,19 +2,19 @@ import json
|
||||||
import logging
|
import logging
|
||||||
import urlparse
|
import urlparse
|
||||||
|
|
||||||
from flask import request, make_response, jsonify, session
|
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
|
from flask import request, make_response, jsonify, session
|
||||||
|
|
||||||
from data import model
|
from data import model
|
||||||
from app import app, authentication, userevents, storage
|
from app import authentication, userevents
|
||||||
from auth.auth import process_auth, generate_signed_token
|
from auth.auth import process_auth, generate_signed_token
|
||||||
from auth.auth_context import get_authenticated_user, get_validated_token, get_validated_oauth_token
|
from auth.auth_context import get_authenticated_user, get_validated_token, get_validated_oauth_token
|
||||||
from util.names import REPOSITORY_NAME_REGEX
|
|
||||||
from auth.permissions import (ModifyRepositoryPermission, UserAdminPermission,
|
from auth.permissions import (ModifyRepositoryPermission, UserAdminPermission,
|
||||||
ReadRepositoryPermission, CreateRepositoryPermission,
|
ReadRepositoryPermission, CreateRepositoryPermission,
|
||||||
repository_read_grant, repository_write_grant)
|
repository_read_grant, repository_write_grant)
|
||||||
|
|
||||||
from util.http import abort
|
from util.http import abort
|
||||||
|
from util.names import REPOSITORY_NAME_REGEX
|
||||||
from endpoints.common import parse_repository_name
|
from endpoints.common import parse_repository_name
|
||||||
from endpoints.v1 import v1_bp
|
from endpoints.v1 import v1_bp
|
||||||
from endpoints.trackhelper import track_and_log
|
from endpoints.trackhelper import track_and_log
|
||||||
|
@ -33,12 +33,12 @@ class GrantType(object):
|
||||||
def generate_headers(scope=GrantType.READ_REPOSITORY, add_grant_for_status=None):
|
def generate_headers(scope=GrantType.READ_REPOSITORY, add_grant_for_status=None):
|
||||||
def decorator_method(f):
|
def decorator_method(f):
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def wrapper(namespace, repository, *args, **kwargs):
|
def wrapper(namespace_name, repo_name, *args, **kwargs):
|
||||||
response = f(namespace, repository, *args, **kwargs)
|
response = f(namespace_name, repo_name, *args, **kwargs)
|
||||||
|
|
||||||
# Setting session namespace and repository
|
# Setting session namespace and repository
|
||||||
session['namespace'] = namespace
|
session['namespace'] = namespace_name
|
||||||
session['repository'] = repository
|
session['repository'] = repo_name
|
||||||
|
|
||||||
# We run our index and registry on the same hosts for now
|
# We run our index and registry on the same hosts for now
|
||||||
registry_server = urlparse.urlparse(request.url).netloc
|
registry_server = urlparse.urlparse(request.url).netloc
|
||||||
|
@ -51,11 +51,11 @@ def generate_headers(scope=GrantType.READ_REPOSITORY, add_grant_for_status=None)
|
||||||
grants = []
|
grants = []
|
||||||
|
|
||||||
if scope == GrantType.READ_REPOSITORY:
|
if scope == GrantType.READ_REPOSITORY:
|
||||||
if force_grant or ReadRepositoryPermission(namespace, repository).can():
|
if force_grant or ReadRepositoryPermission(namespace_name, repo_name).can():
|
||||||
grants.append(repository_read_grant(namespace, repository))
|
grants.append(repository_read_grant(namespace_name, repo_name))
|
||||||
elif scope == GrantType.WRITE_REPOSITORY:
|
elif scope == GrantType.WRITE_REPOSITORY:
|
||||||
if force_grant or ModifyRepositoryPermission(namespace, repository).can():
|
if force_grant or ModifyRepositoryPermission(namespace_name, repo_name).can():
|
||||||
grants.append(repository_write_grant(namespace, repository))
|
grants.append(repository_write_grant(namespace_name, repo_name))
|
||||||
|
|
||||||
# Generate a signed token for the user (if any) and the grants (if any)
|
# Generate a signed token for the user (if any) and the grants (if any)
|
||||||
if grants or get_authenticated_user():
|
if grants or get_authenticated_user():
|
||||||
|
@ -170,50 +170,50 @@ def update_user(username):
|
||||||
|
|
||||||
@v1_bp.route('/repositories/<repopath:repository>/', methods=['PUT'])
|
@v1_bp.route('/repositories/<repopath:repository>/', methods=['PUT'])
|
||||||
@process_auth
|
@process_auth
|
||||||
@parse_repository_name
|
@parse_repository_name()
|
||||||
@generate_headers(scope=GrantType.WRITE_REPOSITORY, add_grant_for_status=201)
|
@generate_headers(scope=GrantType.WRITE_REPOSITORY, add_grant_for_status=201)
|
||||||
@anon_allowed
|
@anon_allowed
|
||||||
def create_repository(namespace, repository):
|
def create_repository(namespace_name, repo_name):
|
||||||
# Verify that the repository name is valid.
|
# Verify that the repository name is valid.
|
||||||
if not REPOSITORY_NAME_REGEX.match(repository):
|
if not REPOSITORY_NAME_REGEX.match(repo_name):
|
||||||
abort(400, message='Invalid repository name. Repository names cannot contain slashes.')
|
abort(400, message='Invalid repository name. Repository names cannot contain slashes.')
|
||||||
|
|
||||||
logger.debug('Looking up repository %s/%s', namespace, repository)
|
logger.debug('Looking up repository %s/%s', namespace_name, repo_name)
|
||||||
repo = model.repository.get_repository(namespace, repository)
|
repo = model.repository.get_repository(namespace_name, repo_name)
|
||||||
|
|
||||||
logger.debug('Found repository %s/%s', namespace, repository)
|
logger.debug('Found repository %s/%s', namespace_name, repo_name)
|
||||||
if not repo and get_authenticated_user() is None:
|
if not repo and get_authenticated_user() is None:
|
||||||
logger.debug('Attempt to create repository %s/%s without user auth', namespace, repository)
|
logger.debug('Attempt to create repository %s/%s without user auth', namespace_name, repo_name)
|
||||||
abort(401,
|
abort(401,
|
||||||
message='Cannot create a repository as a guest. Please login via "docker login" first.',
|
message='Cannot create a repository as a guest. Please login via "docker login" first.',
|
||||||
issue='no-login')
|
issue='no-login')
|
||||||
|
|
||||||
elif repo:
|
elif repo:
|
||||||
permission = ModifyRepositoryPermission(namespace, repository)
|
permission = ModifyRepositoryPermission(namespace_name, repo_name)
|
||||||
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',
|
message='You do not have permission to modify repository %(namespace)s/%(repository)s',
|
||||||
issue='no-repo-write-permission',
|
issue='no-repo-write-permission',
|
||||||
namespace=namespace, repository=repository)
|
namespace=namespace_name, repository=repo_name)
|
||||||
else:
|
else:
|
||||||
permission = CreateRepositoryPermission(namespace)
|
permission = CreateRepositoryPermission(namespace_name)
|
||||||
if not permission.can():
|
if not permission.can():
|
||||||
logger.info('Attempt to create a new repo %s/%s with insufficient perms', namespace,
|
logger.info('Attempt to create a new repo %s/%s with insufficient perms', namespace_name,
|
||||||
repository)
|
repo_name)
|
||||||
msg = 'You do not have permission to create repositories in namespace "%(namespace)s"'
|
msg = 'You do not have permission to create repositories in namespace "%(namespace)s"'
|
||||||
abort(403, message=msg, issue='no-create-permission', namespace=namespace)
|
abort(403, message=msg, issue='no-create-permission', namespace=namespace_name)
|
||||||
|
|
||||||
# Attempt to create the new repository.
|
# Attempt to create the new repository.
|
||||||
logger.debug('Creating repository %s/%s with owner: %s', namespace, repository,
|
logger.debug('Creating repository %s/%s with owner: %s', namespace_name, repo_name,
|
||||||
get_authenticated_user().username)
|
get_authenticated_user().username)
|
||||||
|
|
||||||
repo = model.repository.create_repository(namespace, repository, get_authenticated_user())
|
repo = model.repository.create_repository(namespace_name, repo_name, get_authenticated_user())
|
||||||
|
|
||||||
if get_authenticated_user():
|
if get_authenticated_user():
|
||||||
user_event_data = {
|
user_event_data = {
|
||||||
'action': 'push_start',
|
'action': 'push_start',
|
||||||
'repository': repository,
|
'repository': repo_name,
|
||||||
'namespace': namespace
|
'namespace': namespace_name,
|
||||||
}
|
}
|
||||||
|
|
||||||
event = userevents.get_event(get_authenticated_user().username)
|
event = userevents.get_event(get_authenticated_user().username)
|
||||||
|
@ -224,15 +224,15 @@ def create_repository(namespace, repository):
|
||||||
|
|
||||||
@v1_bp.route('/repositories/<repopath:repository>/images', methods=['PUT'])
|
@v1_bp.route('/repositories/<repopath:repository>/images', methods=['PUT'])
|
||||||
@process_auth
|
@process_auth
|
||||||
@parse_repository_name
|
@parse_repository_name()
|
||||||
@generate_headers(scope=GrantType.WRITE_REPOSITORY)
|
@generate_headers(scope=GrantType.WRITE_REPOSITORY)
|
||||||
@anon_allowed
|
@anon_allowed
|
||||||
def update_images(namespace, repository):
|
def update_images(namespace_name, repo_name):
|
||||||
permission = ModifyRepositoryPermission(namespace, repository)
|
permission = ModifyRepositoryPermission(namespace_name, repo_name)
|
||||||
|
|
||||||
if permission.can():
|
if permission.can():
|
||||||
logger.debug('Looking up repository')
|
logger.debug('Looking up repository')
|
||||||
repo = model.repository.get_repository(namespace, repository)
|
repo = model.repository.get_repository(namespace_name, repo_name)
|
||||||
if not repo:
|
if not repo:
|
||||||
# Make sure the repo actually exists.
|
# Make sure the repo actually exists.
|
||||||
abort(404, message='Unknown repository', issue='unknown-repo')
|
abort(404, message='Unknown repository', issue='unknown-repo')
|
||||||
|
@ -254,17 +254,17 @@ def update_images(namespace, repository):
|
||||||
|
|
||||||
@v1_bp.route('/repositories/<repopath:repository>/images', methods=['GET'])
|
@v1_bp.route('/repositories/<repopath:repository>/images', methods=['GET'])
|
||||||
@process_auth
|
@process_auth
|
||||||
@parse_repository_name
|
@parse_repository_name()
|
||||||
@generate_headers(scope=GrantType.READ_REPOSITORY)
|
@generate_headers(scope=GrantType.READ_REPOSITORY)
|
||||||
@anon_protect
|
@anon_protect
|
||||||
def get_repository_images(namespace, repository):
|
def get_repository_images(namespace_name, repo_name):
|
||||||
permission = ReadRepositoryPermission(namespace, repository)
|
permission = ReadRepositoryPermission(namespace_name, repo_name)
|
||||||
|
|
||||||
# TODO invalidate token?
|
# TODO invalidate token?
|
||||||
if permission.can() or model.repository.repository_is_public(namespace, repository):
|
if permission.can() or model.repository.repository_is_public(namespace_name, repo_name):
|
||||||
# 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
|
||||||
logger.debug('Looking up repository')
|
logger.debug('Looking up repository')
|
||||||
repo = model.repository.get_repository(namespace, repository)
|
repo = model.repository.get_repository(namespace_name, repo_name)
|
||||||
if not repo:
|
if not repo:
|
||||||
abort(404, message='Unknown repository', issue='unknown-repo')
|
abort(404, message='Unknown repository', issue='unknown-repo')
|
||||||
|
|
||||||
|
@ -280,17 +280,17 @@ def get_repository_images(namespace, repository):
|
||||||
|
|
||||||
@v1_bp.route('/repositories/<repopath:repository>/images', methods=['DELETE'])
|
@v1_bp.route('/repositories/<repopath:repository>/images', methods=['DELETE'])
|
||||||
@process_auth
|
@process_auth
|
||||||
@parse_repository_name
|
@parse_repository_name()
|
||||||
@generate_headers(scope=GrantType.WRITE_REPOSITORY)
|
@generate_headers(scope=GrantType.WRITE_REPOSITORY)
|
||||||
@anon_allowed
|
@anon_allowed
|
||||||
def delete_repository_images(namespace, repository):
|
def delete_repository_images(namespace_name, repo_name):
|
||||||
abort(501, 'Not Implemented', issue='not-implemented')
|
abort(501, 'Not Implemented', issue='not-implemented')
|
||||||
|
|
||||||
|
|
||||||
@v1_bp.route('/repositories/<repopath:repository>/auth', methods=['PUT'])
|
@v1_bp.route('/repositories/<repopath:repository>/auth', methods=['PUT'])
|
||||||
@parse_repository_name
|
@parse_repository_name()
|
||||||
@anon_allowed
|
@anon_allowed
|
||||||
def put_repository_auth(namespace, repository):
|
def put_repository_auth(namespace_name, repo_name):
|
||||||
abort(501, 'Not Implemented', issue='not-implemented')
|
abort(501, 'Not Implemented', issue='not-implemented')
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from flask import abort, request, jsonify, make_response, session
|
from flask import abort, request, jsonify, make_response, session
|
||||||
|
|
||||||
from app import app
|
|
||||||
from util.names import TAG_ERROR, TAG_REGEX
|
from util.names import TAG_ERROR, TAG_REGEX
|
||||||
from auth.auth import process_auth
|
from auth.auth import process_auth
|
||||||
from auth.permissions import (ReadRepositoryPermission,
|
from auth.permissions import (ReadRepositoryPermission,
|
||||||
|
@ -22,12 +20,12 @@ logger = logging.getLogger(__name__)
|
||||||
@v1_bp.route('/repositories/<repopath:repository>/tags', methods=['GET'])
|
@v1_bp.route('/repositories/<repopath:repository>/tags', methods=['GET'])
|
||||||
@process_auth
|
@process_auth
|
||||||
@anon_protect
|
@anon_protect
|
||||||
@parse_repository_name
|
@parse_repository_name()
|
||||||
def get_tags(namespace, repository):
|
def get_tags(namespace_name, repo_name):
|
||||||
permission = ReadRepositoryPermission(namespace, repository)
|
permission = ReadRepositoryPermission(namespace_name, repo_name)
|
||||||
|
|
||||||
if permission.can() or model.repository.repository_is_public(namespace, repository):
|
if permission.can() or model.repository.repository_is_public(namespace_name, repo_name):
|
||||||
tags = model.tag.list_repository_tags(namespace, repository)
|
tags = model.tag.list_repository_tags(namespace_name, repo_name)
|
||||||
tag_map = {tag.name: tag.image.docker_image_id for tag in tags}
|
tag_map = {tag.name: tag.image.docker_image_id for tag in tags}
|
||||||
return jsonify(tag_map)
|
return jsonify(tag_map)
|
||||||
|
|
||||||
|
@ -37,13 +35,13 @@ def get_tags(namespace, repository):
|
||||||
@v1_bp.route('/repositories/<repopath:repository>/tags/<tag>', methods=['GET'])
|
@v1_bp.route('/repositories/<repopath:repository>/tags/<tag>', methods=['GET'])
|
||||||
@process_auth
|
@process_auth
|
||||||
@anon_protect
|
@anon_protect
|
||||||
@parse_repository_name
|
@parse_repository_name()
|
||||||
def get_tag(namespace, repository, tag):
|
def get_tag(namespace_name, repo_name, tag):
|
||||||
permission = ReadRepositoryPermission(namespace, repository)
|
permission = ReadRepositoryPermission(namespace_name, repo_name)
|
||||||
|
|
||||||
if permission.can() or model.repository.repository_is_public(namespace, repository):
|
if permission.can() or model.repository.repository_is_public(namespace_name, repo_name):
|
||||||
try:
|
try:
|
||||||
tag_image = model.tag.get_tag_image(namespace, repository, tag)
|
tag_image = model.tag.get_tag_image(namespace_name, repo_name, tag)
|
||||||
except model.DataModelException:
|
except model.DataModelException:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
|
@ -57,19 +55,19 @@ def get_tag(namespace, repository, tag):
|
||||||
@v1_bp.route('/repositories/<repopath:repository>/tags/<tag>', methods=['PUT'])
|
@v1_bp.route('/repositories/<repopath:repository>/tags/<tag>', methods=['PUT'])
|
||||||
@process_auth
|
@process_auth
|
||||||
@anon_protect
|
@anon_protect
|
||||||
@parse_repository_name
|
@parse_repository_name()
|
||||||
def put_tag(namespace, repository, tag):
|
def put_tag(namespace_name, repo_name, tag):
|
||||||
permission = ModifyRepositoryPermission(namespace, repository)
|
permission = ModifyRepositoryPermission(namespace_name, repo_name)
|
||||||
|
|
||||||
if permission.can():
|
if permission.can():
|
||||||
if not TAG_REGEX.match(tag):
|
if not TAG_REGEX.match(tag):
|
||||||
abort(400, TAG_ERROR)
|
abort(400, TAG_ERROR)
|
||||||
|
|
||||||
docker_image_id = json.loads(request.data)
|
docker_image_id = json.loads(request.data)
|
||||||
model.tag.create_or_update_tag(namespace, repository, tag, docker_image_id)
|
model.tag.create_or_update_tag(namespace_name, repo_name, tag, docker_image_id)
|
||||||
|
|
||||||
# Store the updated tag.
|
# Store the updated tag.
|
||||||
if not 'pushed_tags' in session:
|
if 'pushed_tags' not in session:
|
||||||
session['pushed_tags'] = {}
|
session['pushed_tags'] = {}
|
||||||
|
|
||||||
session['pushed_tags'][tag] = docker_image_id
|
session['pushed_tags'][tag] = docker_image_id
|
||||||
|
@ -82,13 +80,13 @@ def put_tag(namespace, repository, tag):
|
||||||
@v1_bp.route('/repositories/<repopath:repository>/tags/<tag>', methods=['DELETE'])
|
@v1_bp.route('/repositories/<repopath:repository>/tags/<tag>', methods=['DELETE'])
|
||||||
@process_auth
|
@process_auth
|
||||||
@anon_protect
|
@anon_protect
|
||||||
@parse_repository_name
|
@parse_repository_name()
|
||||||
def delete_tag(namespace, repository, tag):
|
def delete_tag(namespace_name, repo_name, tag):
|
||||||
permission = ModifyRepositoryPermission(namespace, repository)
|
permission = ModifyRepositoryPermission(namespace_name, repo_name)
|
||||||
|
|
||||||
if permission.can():
|
if permission.can():
|
||||||
model.tag.delete_tag(namespace, repository, tag)
|
model.tag.delete_tag(namespace_name, repo_name, tag)
|
||||||
track_and_log('delete_tag', model.repository.get_repository(namespace, repository),
|
track_and_log('delete_tag', model.repository.get_repository(namespace_name, repo_name),
|
||||||
tag=tag)
|
tag=tag)
|
||||||
return make_response('Deleted', 200)
|
return make_response('Deleted', 200)
|
||||||
|
|
||||||
|
|
|
@ -41,13 +41,14 @@ def handle_registry_v2_exception(error):
|
||||||
def _require_repo_permission(permission_class, allow_public=False):
|
def _require_repo_permission(permission_class, allow_public=False):
|
||||||
def wrapper(func):
|
def wrapper(func):
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
def wrapped(namespace, repo_name, *args, **kwargs):
|
def wrapped(namespace_name, repo_name, *args, **kwargs):
|
||||||
logger.debug('Checking permission %s for repo: %s/%s', permission_class, namespace, repo_name)
|
logger.debug('Checking permission %s for repo: %s/%s', permission_class,
|
||||||
permission = permission_class(namespace, repo_name)
|
namespace_name, repo_name)
|
||||||
|
permission = permission_class(namespace_name, repo_name)
|
||||||
if (permission.can() or
|
if (permission.can() or
|
||||||
(allow_public and
|
(allow_public and
|
||||||
model.repository.repository_is_public(namespace, repo_name))):
|
model.repository.repository_is_public(namespace_name, repo_name))):
|
||||||
return func(namespace, repo_name, *args, **kwargs)
|
return func(namespace_name, repo_name, *args, **kwargs)
|
||||||
raise Unauthorized()
|
raise Unauthorized()
|
||||||
return wrapped
|
return wrapped
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
|
@ -9,6 +9,7 @@ from app import storage, app
|
||||||
from auth.registry_jwt_auth import process_registry_jwt_auth
|
from auth.registry_jwt_auth import process_registry_jwt_auth
|
||||||
from data import model, database
|
from data import model, database
|
||||||
from digest import digest_tools
|
from digest import digest_tools
|
||||||
|
from endpoints.common import parse_repository_name
|
||||||
from endpoints.v2 import v2_bp, require_repo_read, require_repo_write, get_input_stream
|
from endpoints.v2 import v2_bp, require_repo_read, require_repo_write, get_input_stream
|
||||||
from endpoints.v2.errors import (BlobUnknown, BlobUploadInvalid, BlobUploadUnknown, Unsupported,
|
from endpoints.v2.errors import (BlobUnknown, BlobUploadInvalid, BlobUploadUnknown, Unsupported,
|
||||||
NameUnknown)
|
NameUnknown)
|
||||||
|
@ -17,7 +18,6 @@ from util.cache import cache_control
|
||||||
from util.registry.filelike import wrap_with_handler, StreamSlice
|
from util.registry.filelike import wrap_with_handler, StreamSlice
|
||||||
from util.registry.gzipstream import calculate_size_handler
|
from util.registry.gzipstream import calculate_size_handler
|
||||||
from util.registry.torrent import PieceHasher
|
from util.registry.torrent import PieceHasher
|
||||||
from endpoints.common import parse_repository_name
|
|
||||||
from storage.basestorage import InvalidChunkException
|
from storage.basestorage import InvalidChunkException
|
||||||
|
|
||||||
|
|
||||||
|
@ -34,12 +34,12 @@ class _InvalidRangeHeader(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _base_blob_fetch(namespace, repo_name, digest):
|
def _base_blob_fetch(namespace_name, repo_name, digest):
|
||||||
""" Some work that is common to both GET and HEAD requests. Callers MUST check for proper
|
""" Some work that is common to both GET and HEAD requests. Callers MUST check for proper
|
||||||
authorization before calling this method.
|
authorization before calling this method.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
found = model.blob.get_repo_blob_by_digest(namespace, repo_name, digest)
|
found = model.blob.get_repo_blob_by_digest(namespace_name, repo_name, digest)
|
||||||
except model.BlobDoesNotExist:
|
except model.BlobDoesNotExist:
|
||||||
raise BlobUnknown()
|
raise BlobUnknown()
|
||||||
|
|
||||||
|
@ -58,12 +58,12 @@ def _base_blob_fetch(namespace, repo_name, digest):
|
||||||
|
|
||||||
@v2_bp.route(BLOB_DIGEST_ROUTE, methods=['HEAD'])
|
@v2_bp.route(BLOB_DIGEST_ROUTE, methods=['HEAD'])
|
||||||
@process_registry_jwt_auth
|
@process_registry_jwt_auth
|
||||||
@parse_repository_name
|
@parse_repository_name()
|
||||||
@require_repo_read
|
@require_repo_read
|
||||||
@anon_protect
|
@anon_protect
|
||||||
@cache_control(max_age=31436000)
|
@cache_control(max_age=31436000)
|
||||||
def check_blob_exists(namespace, repo_name, digest):
|
def check_blob_exists(namespace_name, repo_name, digest):
|
||||||
found, headers = _base_blob_fetch(namespace, repo_name, digest)
|
found, headers = _base_blob_fetch(namespace_name, repo_name, digest)
|
||||||
|
|
||||||
response = make_response('')
|
response = make_response('')
|
||||||
response.headers.extend(headers)
|
response.headers.extend(headers)
|
||||||
|
@ -74,12 +74,12 @@ def check_blob_exists(namespace, repo_name, digest):
|
||||||
|
|
||||||
@v2_bp.route(BLOB_DIGEST_ROUTE, methods=['GET'])
|
@v2_bp.route(BLOB_DIGEST_ROUTE, methods=['GET'])
|
||||||
@process_registry_jwt_auth
|
@process_registry_jwt_auth
|
||||||
@parse_repository_name
|
@parse_repository_name()
|
||||||
@require_repo_read
|
@require_repo_read
|
||||||
@anon_protect
|
@anon_protect
|
||||||
@cache_control(max_age=31536000)
|
@cache_control(max_age=31536000)
|
||||||
def download_blob(namespace, repo_name, digest):
|
def download_blob(namespace_name, repo_name, digest):
|
||||||
found, headers = _base_blob_fetch(namespace, repo_name, digest)
|
found, headers = _base_blob_fetch(namespace_name, repo_name, digest)
|
||||||
|
|
||||||
path = model.storage.get_layer_path(found)
|
path = model.storage.get_layer_path(found)
|
||||||
logger.debug('Looking up the direct download URL for path: %s', path)
|
logger.debug('Looking up the direct download URL for path: %s', path)
|
||||||
|
@ -108,15 +108,15 @@ def _render_range(num_uploaded_bytes, with_bytes_prefix=True):
|
||||||
|
|
||||||
@v2_bp.route('/<repopath:repository>/blobs/uploads/', methods=['POST'])
|
@v2_bp.route('/<repopath:repository>/blobs/uploads/', methods=['POST'])
|
||||||
@process_registry_jwt_auth
|
@process_registry_jwt_auth
|
||||||
@parse_repository_name
|
@parse_repository_name()
|
||||||
@require_repo_write
|
@require_repo_write
|
||||||
@anon_protect
|
@anon_protect
|
||||||
def start_blob_upload(namespace, repo_name):
|
def start_blob_upload(namespace_name, repo_name):
|
||||||
location_name = storage.preferred_locations[0]
|
location_name = storage.preferred_locations[0]
|
||||||
new_upload_uuid, upload_metadata = storage.initiate_chunked_upload(location_name)
|
new_upload_uuid, upload_metadata = storage.initiate_chunked_upload(location_name)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
model.blob.initiate_upload(namespace, repo_name, new_upload_uuid, location_name,
|
model.blob.initiate_upload(namespace_name, repo_name, new_upload_uuid, location_name,
|
||||||
upload_metadata)
|
upload_metadata)
|
||||||
except database.Repository.DoesNotExist:
|
except database.Repository.DoesNotExist:
|
||||||
raise NameUnknown()
|
raise NameUnknown()
|
||||||
|
@ -126,7 +126,7 @@ def start_blob_upload(namespace, repo_name):
|
||||||
# The user will send the blob data in another request
|
# The user will send the blob data in another request
|
||||||
accepted = make_response('', 202)
|
accepted = make_response('', 202)
|
||||||
accepted.headers['Location'] = url_for('v2.upload_chunk',
|
accepted.headers['Location'] = url_for('v2.upload_chunk',
|
||||||
repository='%s/%s' % (namespace, repo_name),
|
repository='%s/%s' % (namespace_name, repo_name),
|
||||||
upload_uuid=new_upload_uuid)
|
upload_uuid=new_upload_uuid)
|
||||||
|
|
||||||
accepted.headers['Range'] = _render_range(0)
|
accepted.headers['Range'] = _render_range(0)
|
||||||
|
@ -134,22 +134,22 @@ def start_blob_upload(namespace, repo_name):
|
||||||
return accepted
|
return accepted
|
||||||
else:
|
else:
|
||||||
# The user plans to send us the entire body right now
|
# The user plans to send us the entire body right now
|
||||||
uploaded, error = _upload_chunk(namespace, repo_name, new_upload_uuid)
|
uploaded, error = _upload_chunk(namespace_name, repo_name, new_upload_uuid)
|
||||||
uploaded.save()
|
uploaded.save()
|
||||||
if error:
|
if error:
|
||||||
_range_not_satisfiable(uploaded.byte_count)
|
_range_not_satisfiable(uploaded.byte_count)
|
||||||
|
|
||||||
return _finish_upload(namespace, repo_name, uploaded, digest)
|
return _finish_upload(namespace_name, repo_name, uploaded, digest)
|
||||||
|
|
||||||
|
|
||||||
@v2_bp.route('/<repopath:repository>/blobs/uploads/<upload_uuid>', methods=['GET'])
|
@v2_bp.route('/<repopath:repository>/blobs/uploads/<upload_uuid>', methods=['GET'])
|
||||||
@process_registry_jwt_auth
|
@process_registry_jwt_auth
|
||||||
@parse_repository_name
|
@parse_repository_name()
|
||||||
@require_repo_write
|
@require_repo_write
|
||||||
@anon_protect
|
@anon_protect
|
||||||
def fetch_existing_upload(namespace, repo_name, upload_uuid):
|
def fetch_existing_upload(namespace_name, repo_name, upload_uuid):
|
||||||
try:
|
try:
|
||||||
found = model.blob.get_blob_upload(namespace, repo_name, upload_uuid)
|
found = model.blob.get_blob_upload(namespace_name, repo_name, upload_uuid)
|
||||||
except model.InvalidBlobUpload:
|
except model.InvalidBlobUpload:
|
||||||
raise BlobUploadUnknown()
|
raise BlobUploadUnknown()
|
||||||
|
|
||||||
|
@ -189,12 +189,12 @@ def _parse_range_header(range_header_text):
|
||||||
return (start, length)
|
return (start, length)
|
||||||
|
|
||||||
|
|
||||||
def _upload_chunk(namespace, repo_name, upload_uuid):
|
def _upload_chunk(namespace_name, repo_name, upload_uuid):
|
||||||
""" Common code among the various uploading paths for appending data to blobs.
|
""" Common code among the various uploading paths for appending data to blobs.
|
||||||
Callers MUST call .save() or .delete_instance() on the returned database object.
|
Callers MUST call .save() or .delete_instance() on the returned database object.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
found = model.blob.get_blob_upload(namespace, repo_name, upload_uuid)
|
found = model.blob.get_blob_upload(namespace_name, repo_name, upload_uuid)
|
||||||
except model.InvalidBlobUpload:
|
except model.InvalidBlobUpload:
|
||||||
raise BlobUploadUnknown()
|
raise BlobUploadUnknown()
|
||||||
|
|
||||||
|
@ -280,7 +280,7 @@ def _upload_chunk(namespace, repo_name, upload_uuid):
|
||||||
return found, error
|
return found, error
|
||||||
|
|
||||||
|
|
||||||
def _finish_upload(namespace, repo_name, upload_obj, expected_digest):
|
def _finish_upload(namespace_name, repo_name, upload_obj, expected_digest):
|
||||||
# Verify that the digest's SHA matches that of the uploaded data.
|
# Verify that the digest's SHA matches that of the uploaded data.
|
||||||
computed_digest = digest_tools.sha256_digest_from_hashlib(upload_obj.sha_state)
|
computed_digest = digest_tools.sha256_digest_from_hashlib(upload_obj.sha_state)
|
||||||
if not digest_tools.digests_equal(computed_digest, expected_digest):
|
if not digest_tools.digests_equal(computed_digest, expected_digest):
|
||||||
|
@ -303,7 +303,7 @@ def _finish_upload(namespace, repo_name, upload_obj, expected_digest):
|
||||||
final_blob_location, upload_obj.storage_metadata)
|
final_blob_location, upload_obj.storage_metadata)
|
||||||
|
|
||||||
# Mark the blob as uploaded.
|
# Mark the blob as uploaded.
|
||||||
blob_storage = model.blob.store_blob_record_and_temp_link(namespace, repo_name, expected_digest,
|
blob_storage = model.blob.store_blob_record_and_temp_link(namespace_name, repo_name, expected_digest,
|
||||||
upload_obj.location,
|
upload_obj.location,
|
||||||
upload_obj.byte_count,
|
upload_obj.byte_count,
|
||||||
app.config['PUSH_TEMP_TAG_EXPIRATION_SEC'],
|
app.config['PUSH_TEMP_TAG_EXPIRATION_SEC'],
|
||||||
|
@ -319,18 +319,18 @@ def _finish_upload(namespace, repo_name, upload_obj, expected_digest):
|
||||||
response = make_response('', 201)
|
response = make_response('', 201)
|
||||||
response.headers['Docker-Content-Digest'] = expected_digest
|
response.headers['Docker-Content-Digest'] = expected_digest
|
||||||
response.headers['Location'] = url_for('v2.download_blob',
|
response.headers['Location'] = url_for('v2.download_blob',
|
||||||
repository='%s/%s' % (namespace, repo_name),
|
repository='%s/%s' % (namespace_name, repo_name),
|
||||||
digest=expected_digest)
|
digest=expected_digest)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@v2_bp.route('/<repopath:repository>/blobs/uploads/<upload_uuid>', methods=['PATCH'])
|
@v2_bp.route('/<repopath:repository>/blobs/uploads/<upload_uuid>', methods=['PATCH'])
|
||||||
@process_registry_jwt_auth
|
@process_registry_jwt_auth
|
||||||
@parse_repository_name
|
@parse_repository_name()
|
||||||
@require_repo_write
|
@require_repo_write
|
||||||
@anon_protect
|
@anon_protect
|
||||||
def upload_chunk(namespace, repo_name, upload_uuid):
|
def upload_chunk(namespace_name, repo_name, upload_uuid):
|
||||||
upload, error = _upload_chunk(namespace, repo_name, upload_uuid)
|
upload, error = _upload_chunk(namespace_name, repo_name, upload_uuid)
|
||||||
upload.save()
|
upload.save()
|
||||||
|
|
||||||
if error:
|
if error:
|
||||||
|
@ -345,31 +345,31 @@ def upload_chunk(namespace, repo_name, upload_uuid):
|
||||||
|
|
||||||
@v2_bp.route('/<repopath:repository>/blobs/uploads/<upload_uuid>', methods=['PUT'])
|
@v2_bp.route('/<repopath:repository>/blobs/uploads/<upload_uuid>', methods=['PUT'])
|
||||||
@process_registry_jwt_auth
|
@process_registry_jwt_auth
|
||||||
@parse_repository_name
|
@parse_repository_name()
|
||||||
@require_repo_write
|
@require_repo_write
|
||||||
@anon_protect
|
@anon_protect
|
||||||
def monolithic_upload_or_last_chunk(namespace, repo_name, upload_uuid):
|
def monolithic_upload_or_last_chunk(namespace_name, repo_name, upload_uuid):
|
||||||
digest = request.args.get('digest', None)
|
digest = request.args.get('digest', None)
|
||||||
if digest is None:
|
if digest is None:
|
||||||
raise BlobUploadInvalid()
|
raise BlobUploadInvalid()
|
||||||
|
|
||||||
found, error = _upload_chunk(namespace, repo_name, upload_uuid)
|
found, error = _upload_chunk(namespace_name, repo_name, upload_uuid)
|
||||||
|
|
||||||
if error:
|
if error:
|
||||||
found.save()
|
found.save()
|
||||||
_range_not_satisfiable(found.byte_count)
|
_range_not_satisfiable(found.byte_count)
|
||||||
|
|
||||||
return _finish_upload(namespace, repo_name, found, digest)
|
return _finish_upload(namespace_name, repo_name, found, digest)
|
||||||
|
|
||||||
|
|
||||||
@v2_bp.route('/<repopath:repository>/blobs/uploads/<upload_uuid>', methods=['DELETE'])
|
@v2_bp.route('/<repopath:repository>/blobs/uploads/<upload_uuid>', methods=['DELETE'])
|
||||||
|
@parse_repository_name()
|
||||||
@process_registry_jwt_auth
|
@process_registry_jwt_auth
|
||||||
@parse_repository_name
|
|
||||||
@require_repo_write
|
@require_repo_write
|
||||||
@anon_protect
|
@anon_protect
|
||||||
def cancel_upload(namespace, repo_name, upload_uuid):
|
def cancel_upload(namespace_name, repo_name, upload_uuid):
|
||||||
try:
|
try:
|
||||||
found = model.blob.get_blob_upload(namespace, repo_name, upload_uuid)
|
found = model.blob.get_blob_upload(namespace_name, repo_name, upload_uuid)
|
||||||
except model.InvalidBlobUpload:
|
except model.InvalidBlobUpload:
|
||||||
raise BlobUploadUnknown()
|
raise BlobUploadUnknown()
|
||||||
|
|
||||||
|
@ -384,11 +384,9 @@ def cancel_upload(namespace, repo_name, upload_uuid):
|
||||||
|
|
||||||
@v2_bp.route('/<repopath:repository>/blobs/<digest>', methods=['DELETE'])
|
@v2_bp.route('/<repopath:repository>/blobs/<digest>', methods=['DELETE'])
|
||||||
@process_registry_jwt_auth
|
@process_registry_jwt_auth
|
||||||
@parse_repository_name
|
@parse_repository_name()
|
||||||
@require_repo_write
|
@require_repo_write
|
||||||
@anon_protect
|
@anon_protect
|
||||||
def delete_digest(namespace, repo_name, upload_uuid):
|
def delete_digest(namespace_name, repo_name, upload_uuid):
|
||||||
# We do not support deleting arbitrary digests, as they break repo images.
|
# We do not support deleting arbitrary digests, as they break repo images.
|
||||||
raise Unsupported()
|
raise Unsupported()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,22 @@
|
||||||
import logging
|
import logging
|
||||||
import jwt.utils
|
|
||||||
import json
|
import json
|
||||||
import features
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
from peewee import IntegrityError
|
|
||||||
from flask import make_response, request, url_for
|
|
||||||
from collections import namedtuple, OrderedDict
|
from collections import namedtuple, OrderedDict
|
||||||
from jwkest.jws import SIGNER_ALGS, keyrep
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
|
import jwt.utils
|
||||||
|
|
||||||
|
from peewee import IntegrityError
|
||||||
|
from flask import make_response, request, url_for
|
||||||
|
from jwkest.jws import SIGNER_ALGS, keyrep
|
||||||
|
|
||||||
|
import features
|
||||||
|
|
||||||
from app import docker_v2_signing_key, app
|
from app import docker_v2_signing_key, app
|
||||||
from auth.registry_jwt_auth import process_registry_jwt_auth
|
from auth.registry_jwt_auth import process_registry_jwt_auth
|
||||||
|
from endpoints.common import parse_repository_name
|
||||||
from endpoints.decorators import anon_protect
|
from endpoints.decorators import anon_protect
|
||||||
from endpoints.v2 import v2_bp, require_repo_read, require_repo_write
|
from endpoints.v2 import v2_bp, require_repo_read, require_repo_write
|
||||||
from endpoints.v2.errors import (BlobUnknown, ManifestInvalid, ManifestUnknown, TagInvalid,
|
from endpoints.v2.errors import (BlobUnknown, ManifestInvalid, ManifestUnknown, TagInvalid,
|
||||||
|
@ -22,7 +26,6 @@ from endpoints.notificationhelper import spawn_notification
|
||||||
from digest import digest_tools
|
from digest import digest_tools
|
||||||
from data import model
|
from data import model
|
||||||
from data.database import RepositoryTag
|
from data.database import RepositoryTag
|
||||||
from endpoints.common import parse_repository_name
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -84,7 +87,8 @@ class SignedManifest(object):
|
||||||
|
|
||||||
def _validate(self):
|
def _validate(self):
|
||||||
for signature in self._signatures:
|
for signature in self._signatures:
|
||||||
bytes_to_verify = '{0}.{1}'.format(signature['protected'], jwt.utils.base64url_encode(self.payload))
|
bytes_to_verify = '{0}.{1}'.format(signature['protected'],
|
||||||
|
jwt.utils.base64url_encode(self.payload))
|
||||||
signer = SIGNER_ALGS[signature['header']['alg']]
|
signer = SIGNER_ALGS[signature['header']['alg']]
|
||||||
key = keyrep(signature['header']['jwk'])
|
key = keyrep(signature['header']['jwk'])
|
||||||
gk = key.get_key()
|
gk = key.get_key()
|
||||||
|
@ -163,9 +167,9 @@ class SignedManifest(object):
|
||||||
class SignedManifestBuilder(object):
|
class SignedManifestBuilder(object):
|
||||||
""" Class which represents a manifest which is currently being built.
|
""" Class which represents a manifest which is currently being built.
|
||||||
"""
|
"""
|
||||||
def __init__(self, namespace, repo_name, tag, architecture='amd64', schema_ver=1):
|
def __init__(self, namespace_name, repo_name, tag, architecture='amd64', schema_ver=1):
|
||||||
repo_name_key = '{0}/{1}'.format(namespace, repo_name)
|
repo_name_key = '{0}/{1}'.format(namespace_name, repo_name)
|
||||||
if namespace == '':
|
if namespace_name == '':
|
||||||
repo_name_key = repo_name
|
repo_name_key = repo_name
|
||||||
|
|
||||||
self._base_payload = {
|
self._base_payload = {
|
||||||
|
@ -238,26 +242,26 @@ class SignedManifestBuilder(object):
|
||||||
|
|
||||||
@v2_bp.route(MANIFEST_TAGNAME_ROUTE, methods=['GET'])
|
@v2_bp.route(MANIFEST_TAGNAME_ROUTE, methods=['GET'])
|
||||||
@process_registry_jwt_auth
|
@process_registry_jwt_auth
|
||||||
@parse_repository_name
|
@parse_repository_name()
|
||||||
@require_repo_read
|
@require_repo_read
|
||||||
@anon_protect
|
@anon_protect
|
||||||
def fetch_manifest_by_tagname(namespace, repo_name, manifest_ref):
|
def fetch_manifest_by_tagname(namespace_name, repo_name, manifest_ref):
|
||||||
try:
|
try:
|
||||||
manifest = model.tag.load_tag_manifest(namespace, repo_name, manifest_ref)
|
manifest = model.tag.load_tag_manifest(namespace_name, repo_name, manifest_ref)
|
||||||
except model.InvalidManifestException:
|
except model.InvalidManifestException:
|
||||||
try:
|
try:
|
||||||
model.tag.get_active_tag(namespace, repo_name, manifest_ref)
|
model.tag.get_active_tag(namespace_name, repo_name, manifest_ref)
|
||||||
except RepositoryTag.DoesNotExist:
|
except RepositoryTag.DoesNotExist:
|
||||||
raise ManifestUnknown()
|
raise ManifestUnknown()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
manifest = _generate_and_store_manifest(namespace, repo_name, manifest_ref)
|
manifest = _generate_and_store_manifest(namespace_name, repo_name, manifest_ref)
|
||||||
except model.DataModelException:
|
except model.DataModelException:
|
||||||
logger.exception('Exception when generating manifest for %s/%s:%s', namespace, repo_name,
|
logger.exception('Exception when generating manifest for %s/%s:%s', namespace_name, repo_name,
|
||||||
manifest_ref)
|
manifest_ref)
|
||||||
raise ManifestUnknown()
|
raise ManifestUnknown()
|
||||||
|
|
||||||
repo = model.repository.get_repository(namespace, repo_name)
|
repo = model.repository.get_repository(namespace_name, repo_name)
|
||||||
if repo is not None:
|
if repo is not None:
|
||||||
track_and_log('pull_repo', repo, analytics_name='pull_repo_100x', analytics_sample=0.01)
|
track_and_log('pull_repo', repo, analytics_name='pull_repo_100x', analytics_sample=0.01)
|
||||||
|
|
||||||
|
@ -269,17 +273,17 @@ def fetch_manifest_by_tagname(namespace, repo_name, manifest_ref):
|
||||||
|
|
||||||
@v2_bp.route(MANIFEST_DIGEST_ROUTE, methods=['GET'])
|
@v2_bp.route(MANIFEST_DIGEST_ROUTE, methods=['GET'])
|
||||||
@process_registry_jwt_auth
|
@process_registry_jwt_auth
|
||||||
@parse_repository_name
|
@parse_repository_name()
|
||||||
@require_repo_read
|
@require_repo_read
|
||||||
@anon_protect
|
@anon_protect
|
||||||
def fetch_manifest_by_digest(namespace, repo_name, manifest_ref):
|
def fetch_manifest_by_digest(namespace_name, repo_name, manifest_ref):
|
||||||
try:
|
try:
|
||||||
manifest = model.tag.load_manifest_by_digest(namespace, repo_name, manifest_ref)
|
manifest = model.tag.load_manifest_by_digest(namespace_name, repo_name, manifest_ref)
|
||||||
except model.InvalidManifestException:
|
except model.InvalidManifestException:
|
||||||
# Without a tag name to reference, we can't make an attempt to generate the manifest
|
# Without a tag name to reference, we can't make an attempt to generate the manifest
|
||||||
raise ManifestUnknown()
|
raise ManifestUnknown()
|
||||||
|
|
||||||
repo = model.repository.get_repository(namespace, repo_name)
|
repo = model.repository.get_repository(namespace_name, repo_name)
|
||||||
if repo is not None:
|
if repo is not None:
|
||||||
track_and_log('pull_repo', repo)
|
track_and_log('pull_repo', repo)
|
||||||
|
|
||||||
|
@ -301,11 +305,11 @@ def _reject_manifest2_schema2(func):
|
||||||
|
|
||||||
@v2_bp.route(MANIFEST_TAGNAME_ROUTE, methods=['PUT'])
|
@v2_bp.route(MANIFEST_TAGNAME_ROUTE, methods=['PUT'])
|
||||||
@process_registry_jwt_auth
|
@process_registry_jwt_auth
|
||||||
@parse_repository_name
|
@parse_repository_name()
|
||||||
@require_repo_write
|
@require_repo_write
|
||||||
@anon_protect
|
@anon_protect
|
||||||
@_reject_manifest2_schema2
|
@_reject_manifest2_schema2
|
||||||
def write_manifest_by_tagname(namespace, repo_name, manifest_ref):
|
def write_manifest_by_tagname(namespace_name, repo_name, manifest_ref):
|
||||||
try:
|
try:
|
||||||
manifest = SignedManifest(request.data)
|
manifest = SignedManifest(request.data)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
@ -314,16 +318,16 @@ def write_manifest_by_tagname(namespace, repo_name, manifest_ref):
|
||||||
if manifest.tag != manifest_ref:
|
if manifest.tag != manifest_ref:
|
||||||
raise TagInvalid()
|
raise TagInvalid()
|
||||||
|
|
||||||
return _write_manifest(namespace, repo_name, manifest)
|
return _write_manifest(namespace_name, repo_name, manifest)
|
||||||
|
|
||||||
|
|
||||||
@v2_bp.route(MANIFEST_DIGEST_ROUTE, methods=['PUT'])
|
@v2_bp.route(MANIFEST_DIGEST_ROUTE, methods=['PUT'])
|
||||||
@process_registry_jwt_auth
|
@process_registry_jwt_auth
|
||||||
@parse_repository_name
|
@parse_repository_name()
|
||||||
@require_repo_write
|
@require_repo_write
|
||||||
@anon_protect
|
@anon_protect
|
||||||
@_reject_manifest2_schema2
|
@_reject_manifest2_schema2
|
||||||
def write_manifest_by_digest(namespace, repo_name, manifest_ref):
|
def write_manifest_by_digest(namespace_name, repo_name, manifest_ref):
|
||||||
try:
|
try:
|
||||||
manifest = SignedManifest(request.data)
|
manifest = SignedManifest(request.data)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
@ -332,7 +336,7 @@ def write_manifest_by_digest(namespace, repo_name, manifest_ref):
|
||||||
if manifest.digest != manifest_ref:
|
if manifest.digest != manifest_ref:
|
||||||
raise ManifestInvalid(detail={'message': 'manifest digest mismatch'})
|
raise ManifestInvalid(detail={'message': 'manifest digest mismatch'})
|
||||||
|
|
||||||
return _write_manifest(namespace, repo_name, manifest)
|
return _write_manifest(namespace_name, repo_name, manifest)
|
||||||
|
|
||||||
|
|
||||||
def _updated_v1_metadata(v1_metadata_json, updated_id_map):
|
def _updated_v1_metadata(v1_metadata_json, updated_id_map):
|
||||||
|
@ -350,21 +354,21 @@ def _updated_v1_metadata(v1_metadata_json, updated_id_map):
|
||||||
return json.dumps(parsed)
|
return json.dumps(parsed)
|
||||||
|
|
||||||
|
|
||||||
def _write_manifest(namespace, repo_name, manifest):
|
def _write_manifest(namespace_name, repo_name, manifest):
|
||||||
# Ensure that the manifest is for this repository. If the manifest's namespace is empty, then
|
# Ensure that the manifest is for this repository. If the manifest's namespace is empty, then
|
||||||
# it is for the library namespace and we need an extra check.
|
# it is for the library namespace and we need an extra check.
|
||||||
if (manifest.namespace == '' and features.LIBRARY_SUPPORT and
|
if (manifest.namespace == '' and features.LIBRARY_SUPPORT and
|
||||||
namespace == app.config['LIBRARY_NAMESPACE']):
|
namespace_name == app.config['LIBRARY_NAMESPACE']):
|
||||||
# This is a library manifest. All good.
|
# This is a library manifest. All good.
|
||||||
pass
|
pass
|
||||||
elif manifest.namespace != namespace:
|
elif manifest.namespace != namespace_name:
|
||||||
raise NameInvalid()
|
raise NameInvalid()
|
||||||
|
|
||||||
if manifest.repo_name != repo_name:
|
if manifest.repo_name != repo_name:
|
||||||
raise NameInvalid()
|
raise NameInvalid()
|
||||||
|
|
||||||
# Ensure that the repository exists.
|
# Ensure that the repository exists.
|
||||||
repo = model.repository.get_repository(namespace, repo_name)
|
repo = model.repository.get_repository(namespace_name, repo_name)
|
||||||
if repo is None:
|
if repo is None:
|
||||||
raise NameInvalid()
|
raise NameInvalid()
|
||||||
|
|
||||||
|
@ -411,8 +415,8 @@ def _write_manifest(namespace, repo_name, manifest):
|
||||||
|
|
||||||
v1_metadata_str = mdata.v1_metadata_str.encode('utf-8')
|
v1_metadata_str = mdata.v1_metadata_str.encode('utf-8')
|
||||||
working_docker_id = hashlib.sha256(v1_metadata_str + '@' + digest_str).hexdigest()
|
working_docker_id = hashlib.sha256(v1_metadata_str + '@' + digest_str).hexdigest()
|
||||||
logger.debug('Rewriting docker_id %s/%s %s -> %s', namespace, repo_name, v1_mdata.docker_id,
|
logger.debug('Rewriting docker_id %s/%s %s -> %s', namespace_name, repo_name,
|
||||||
working_docker_id)
|
v1_mdata.docker_id, working_docker_id)
|
||||||
has_rewritten_ids = True
|
has_rewritten_ids = True
|
||||||
|
|
||||||
# Store the new docker id in the map
|
# Store the new docker id in the map
|
||||||
|
@ -447,7 +451,7 @@ def _write_manifest(namespace, repo_name, manifest):
|
||||||
# Store the manifest pointing to the tag.
|
# Store the manifest pointing to the tag.
|
||||||
manifest_digest = manifest.digest
|
manifest_digest = manifest.digest
|
||||||
leaf_layer_id = images_map[layers[-1].v1_metadata.docker_id].docker_image_id
|
leaf_layer_id = images_map[layers[-1].v1_metadata.docker_id].docker_image_id
|
||||||
model.tag.store_tag_manifest(namespace, repo_name, tag_name, leaf_layer_id, manifest_digest,
|
model.tag.store_tag_manifest(namespace_name, repo_name, tag_name, leaf_layer_id, manifest_digest,
|
||||||
manifest.bytes)
|
manifest.bytes)
|
||||||
|
|
||||||
# Spawn the repo_push event.
|
# Spawn the repo_push event.
|
||||||
|
@ -461,29 +465,29 @@ def _write_manifest(namespace, repo_name, manifest):
|
||||||
response = make_response('OK', 202)
|
response = make_response('OK', 202)
|
||||||
response.headers['Docker-Content-Digest'] = manifest_digest
|
response.headers['Docker-Content-Digest'] = manifest_digest
|
||||||
response.headers['Location'] = url_for('v2.fetch_manifest_by_digest',
|
response.headers['Location'] = url_for('v2.fetch_manifest_by_digest',
|
||||||
repository='%s/%s' % (namespace, repo_name),
|
repository='%s/%s' % (namespace_name, repo_name),
|
||||||
manifest_ref=manifest_digest)
|
manifest_ref=manifest_digest)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
@v2_bp.route(MANIFEST_DIGEST_ROUTE, methods=['DELETE'])
|
@v2_bp.route(MANIFEST_DIGEST_ROUTE, methods=['DELETE'])
|
||||||
@process_registry_jwt_auth
|
@process_registry_jwt_auth
|
||||||
@parse_repository_name
|
@parse_repository_name()
|
||||||
@require_repo_write
|
@require_repo_write
|
||||||
@anon_protect
|
@anon_protect
|
||||||
def delete_manifest_by_digest(namespace, repo_name, manifest_ref):
|
def delete_manifest_by_digest(namespace_name, repo_name, manifest_ref):
|
||||||
""" Delete the manifest specified by the digest. Note: there is no equivalent
|
""" Delete the manifest specified by the digest. Note: there is no equivalent
|
||||||
method for deleting by tag name because it is forbidden by the spec.
|
method for deleting by tag name because it is forbidden by the spec.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
manifest = model.tag.load_manifest_by_digest(namespace, repo_name, manifest_ref)
|
manifest = model.tag.load_manifest_by_digest(namespace_name, repo_name, manifest_ref)
|
||||||
except model.InvalidManifestException:
|
except model.InvalidManifestException:
|
||||||
# Without a tag name to reference, we can't make an attempt to generate the manifest
|
# Without a tag name to reference, we can't make an attempt to generate the manifest
|
||||||
raise ManifestUnknown()
|
raise ManifestUnknown()
|
||||||
|
|
||||||
# Mark the tag as no longer alive.
|
# Mark the tag as no longer alive.
|
||||||
try:
|
try:
|
||||||
model.tag.delete_tag(namespace, repo_name, manifest.tag.name)
|
model.tag.delete_tag(namespace_name, repo_name, manifest.tag.name)
|
||||||
except model.DataModelException:
|
except model.DataModelException:
|
||||||
# Tag is not alive.
|
# Tag is not alive.
|
||||||
raise ManifestUnknown()
|
raise ManifestUnknown()
|
||||||
|
@ -494,15 +498,15 @@ def delete_manifest_by_digest(namespace, repo_name, manifest_ref):
|
||||||
return make_response('', 202)
|
return make_response('', 202)
|
||||||
|
|
||||||
|
|
||||||
def _generate_and_store_manifest(namespace, repo_name, tag_name):
|
def _generate_and_store_manifest(namespace_name, repo_name, tag_name):
|
||||||
# First look up the tag object and its ancestors
|
# First look up the tag object and its ancestors
|
||||||
image = model.tag.get_tag_image(namespace, repo_name, tag_name)
|
image = model.tag.get_tag_image(namespace_name, repo_name, tag_name)
|
||||||
parents = model.image.get_parent_images(namespace, repo_name, image)
|
parents = model.image.get_parent_images(namespace_name, repo_name, image)
|
||||||
|
|
||||||
# If the manifest is being generated under the library namespace, then we make its namespace
|
# If the manifest is being generated under the library namespace, then we make its namespace
|
||||||
# empty.
|
# empty.
|
||||||
manifest_namespace = namespace
|
manifest_namespace = namespace_name
|
||||||
if features.LIBRARY_SUPPORT and namespace == app.config['LIBRARY_NAMESPACE']:
|
if features.LIBRARY_SUPPORT and namespace_name == app.config['LIBRARY_NAMESPACE']:
|
||||||
manifest_namespace = ''
|
manifest_namespace = ''
|
||||||
|
|
||||||
# Create and populate the manifest builder
|
# Create and populate the manifest builder
|
||||||
|
@ -520,13 +524,12 @@ def _generate_and_store_manifest(namespace, repo_name, tag_name):
|
||||||
# Write the manifest to the DB. If an existing manifest already exists, return the
|
# Write the manifest to the DB. If an existing manifest already exists, return the
|
||||||
# one found.
|
# one found.
|
||||||
try:
|
try:
|
||||||
return model.tag.associate_generated_tag_manifest(namespace, repo_name, tag_name,
|
return model.tag.associate_generated_tag_manifest(namespace_name, repo_name, tag_name,
|
||||||
manifest.digest, manifest.bytes)
|
manifest.digest, manifest.bytes)
|
||||||
except IntegrityError as ie:
|
except IntegrityError as ie:
|
||||||
logger.debug('Got integrity error: %s', ie)
|
logger.debug('Got integrity error: %s', ie)
|
||||||
try:
|
try:
|
||||||
return model.tag.load_tag_manifest(namespace, repo_name, tag_name)
|
return model.tag.load_tag_manifest(namespace_name, repo_name, tag_name)
|
||||||
except model.InvalidManifestException:
|
except model.InvalidManifestException:
|
||||||
logger.exception('Exception when generating manifest')
|
logger.exception('Exception when generating manifest')
|
||||||
raise model.DataModelException('Could not load or generate manifest')
|
raise model.DataModelException('Could not load or generate manifest')
|
||||||
|
|
||||||
|
|
|
@ -1,29 +1,29 @@
|
||||||
from flask import jsonify, url_for
|
from flask import jsonify, url_for
|
||||||
|
|
||||||
from auth.registry_jwt_auth import process_registry_jwt_auth
|
from auth.registry_jwt_auth import process_registry_jwt_auth
|
||||||
|
from endpoints.common import parse_repository_name
|
||||||
from endpoints.v2 import v2_bp, require_repo_read
|
from endpoints.v2 import v2_bp, require_repo_read
|
||||||
from endpoints.v2.errors import NameUnknown
|
from endpoints.v2.errors import NameUnknown
|
||||||
from endpoints.v2.v2util import add_pagination
|
from endpoints.v2.v2util import add_pagination
|
||||||
from endpoints.decorators import anon_protect
|
from endpoints.decorators import anon_protect
|
||||||
from data import model
|
from data import model
|
||||||
from endpoints.common import parse_repository_name
|
|
||||||
|
|
||||||
@v2_bp.route('/<repopath:repository>/tags/list', methods=['GET'])
|
@v2_bp.route('/<repopath:repository>/tags/list', methods=['GET'])
|
||||||
@process_registry_jwt_auth
|
@process_registry_jwt_auth
|
||||||
@parse_repository_name
|
@parse_repository_name()
|
||||||
@require_repo_read
|
@require_repo_read
|
||||||
@anon_protect
|
@anon_protect
|
||||||
def list_all_tags(namespace, repo_name):
|
def list_all_tags(namespace_name, repo_name):
|
||||||
repository = model.repository.get_repository(namespace, repo_name)
|
repository = model.repository.get_repository(namespace_name, repo_name)
|
||||||
if repository is None:
|
if repository is None:
|
||||||
raise NameUnknown()
|
raise NameUnknown()
|
||||||
|
|
||||||
query = model.tag.list_repository_tags(namespace, repo_name)
|
query = model.tag.list_repository_tags(namespace_name, repo_name)
|
||||||
url = url_for('v2.list_all_tags', repository='%s/%s' % (namespace, repo_name))
|
url = url_for('v2.list_all_tags', repository='%s/%s' % (namespace_name, repo_name))
|
||||||
link, query = add_pagination(query, url)
|
link, query = add_pagination(query, url)
|
||||||
|
|
||||||
response = jsonify({
|
response = jsonify({
|
||||||
'name': '{0}/{1}'.format(namespace, repo_name),
|
'name': '{0}/{1}'.format(namespace_name, repo_name),
|
||||||
'tags': [tag.name for tag in query],
|
'tags': [tag.name for tag in query],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -381,10 +381,10 @@ def get_squashed_tag(namespace, repository, tag):
|
||||||
@anon_protect
|
@anon_protect
|
||||||
@verbs.route('/torrent{0}'.format(BLOB_DIGEST_ROUTE), methods=['GET'])
|
@verbs.route('/torrent{0}'.format(BLOB_DIGEST_ROUTE), methods=['GET'])
|
||||||
@process_auth
|
@process_auth
|
||||||
@parse_repository_name
|
@parse_repository_name()
|
||||||
def get_tag_torrent(namespace, repo_name, digest):
|
def get_tag_torrent(namespace_name, repo_name, digest):
|
||||||
permission = ReadRepositoryPermission(namespace, repo_name)
|
permission = ReadRepositoryPermission(namespace_name, repo_name)
|
||||||
public_repo = model.repository.repository_is_public(namespace, repo_name)
|
public_repo = model.repository.repository_is_public(namespace_name, repo_name)
|
||||||
if not permission.can() and not public_repo:
|
if not permission.can() and not public_repo:
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
|
@ -394,7 +394,7 @@ def get_tag_torrent(namespace, repo_name, digest):
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
blob = model.blob.get_repo_blob_by_digest(namespace, repo_name, digest)
|
blob = model.blob.get_repo_blob_by_digest(namespace_name, repo_name, digest)
|
||||||
except model.BlobDoesNotExist:
|
except model.BlobDoesNotExist:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ from data import model
|
||||||
from data.database import db
|
from data.database import db
|
||||||
from endpoints.api.discovery import swagger_route_data
|
from endpoints.api.discovery import swagger_route_data
|
||||||
from endpoints.common import (common_login, render_page_template, route_show_if, param_required,
|
from endpoints.common import (common_login, render_page_template, route_show_if, param_required,
|
||||||
parse_repository_name, parse_repository_name_and_tag)
|
parse_repository_name)
|
||||||
from endpoints.csrf import csrf_protect, generate_csrf_token, verify_csrf
|
from endpoints.csrf import csrf_protect, generate_csrf_token, verify_csrf
|
||||||
from endpoints.decorators import anon_protect, anon_allowed
|
from endpoints.decorators import anon_protect, anon_allowed
|
||||||
from health.healthcheck import get_healthchecker
|
from health.healthcheck import get_healthchecker
|
||||||
|
@ -411,20 +411,20 @@ def confirm_recovery():
|
||||||
|
|
||||||
|
|
||||||
@web.route('/repository/<repopath:repository>/status', methods=['GET'])
|
@web.route('/repository/<repopath:repository>/status', methods=['GET'])
|
||||||
@parse_repository_name
|
@parse_repository_name()
|
||||||
@anon_protect
|
@anon_protect
|
||||||
def build_status_badge(namespace, repository):
|
def build_status_badge(namespace_name, repo_name):
|
||||||
token = request.args.get('token', None)
|
token = request.args.get('token', None)
|
||||||
is_public = model.repository.repository_is_public(namespace, repository)
|
is_public = model.repository.repository_is_public(namespace_name, repo_name)
|
||||||
if not is_public:
|
if not is_public:
|
||||||
repo = model.repository.get_repository(namespace, repository)
|
repo = model.repository.get_repository(namespace_name, repo_name)
|
||||||
if not repo or token != repo.badge_token:
|
if not repo or token != repo.badge_token:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
# Lookup the tags for the repository.
|
# Lookup the tags for the repository.
|
||||||
tags = model.tag.list_repository_tags(namespace, repository)
|
tags = model.tag.list_repository_tags(namespace_name, repo_name)
|
||||||
is_empty = len(list(tags)) == 0
|
is_empty = len(list(tags)) == 0
|
||||||
recent_build = model.build.get_recent_repository_build(namespace, repository)
|
recent_build = model.build.get_recent_repository_build(namespace_name, repo_name)
|
||||||
|
|
||||||
if not is_empty and (not recent_build or recent_build.phase == 'complete'):
|
if not is_empty and (not recent_build or recent_build.phase == 'complete'):
|
||||||
status_name = 'ready'
|
status_name = 'ready'
|
||||||
|
@ -600,14 +600,14 @@ def download_logs_archive():
|
||||||
|
|
||||||
@web.route('/bitbucket/setup/<repopath:repository>', methods=['GET'])
|
@web.route('/bitbucket/setup/<repopath:repository>', methods=['GET'])
|
||||||
@require_session_login
|
@require_session_login
|
||||||
@parse_repository_name
|
@parse_repository_name()
|
||||||
@route_show_if(features.BITBUCKET_BUILD)
|
@route_show_if(features.BITBUCKET_BUILD)
|
||||||
def attach_bitbucket_trigger(namespace, repository_name):
|
def attach_bitbucket_trigger(namespace_name, repo_name):
|
||||||
permission = AdministerRepositoryPermission(namespace, repository_name)
|
permission = AdministerRepositoryPermission(namespace_name, repo_name)
|
||||||
if permission.can():
|
if permission.can():
|
||||||
repo = model.repository.get_repository(namespace, repository_name)
|
repo = model.repository.get_repository(namespace_name, repo_name)
|
||||||
if not repo:
|
if not repo:
|
||||||
msg = 'Invalid repository: %s/%s' % (namespace, repository_name)
|
msg = 'Invalid repository: %s/%s' % (namespace_name, repo_name)
|
||||||
abort(404, message=msg)
|
abort(404, message=msg)
|
||||||
|
|
||||||
trigger = model.build.create_build_trigger(repo, BitbucketBuildTrigger.service_name(), None,
|
trigger = model.build.create_build_trigger(repo, BitbucketBuildTrigger.service_name(), None,
|
||||||
|
@ -634,19 +634,19 @@ def attach_bitbucket_trigger(namespace, repository_name):
|
||||||
|
|
||||||
@web.route('/customtrigger/setup/<repopath:repository>', methods=['GET'])
|
@web.route('/customtrigger/setup/<repopath:repository>', methods=['GET'])
|
||||||
@require_session_login
|
@require_session_login
|
||||||
@parse_repository_name
|
@parse_repository_name()
|
||||||
def attach_custom_build_trigger(namespace, repository_name):
|
def attach_custom_build_trigger(namespace_name, repo_name):
|
||||||
permission = AdministerRepositoryPermission(namespace, repository_name)
|
permission = AdministerRepositoryPermission(namespace_name, repo_name)
|
||||||
if permission.can():
|
if permission.can():
|
||||||
repo = model.repository.get_repository(namespace, repository_name)
|
repo = model.repository.get_repository(namespace_name, repo_name)
|
||||||
if not repo:
|
if not repo:
|
||||||
msg = 'Invalid repository: %s/%s' % (namespace, repository_name)
|
msg = 'Invalid repository: %s/%s' % (namespace_name, repo_name)
|
||||||
abort(404, message=msg)
|
abort(404, message=msg)
|
||||||
|
|
||||||
trigger = model.build.create_build_trigger(repo, CustomBuildTrigger.service_name(),
|
trigger = model.build.create_build_trigger(repo, CustomBuildTrigger.service_name(),
|
||||||
None, current_user.db_user())
|
None, current_user.db_user())
|
||||||
|
|
||||||
repo_path = '%s/%s' % (namespace, repository_name)
|
repo_path = '%s/%s' % (namespace_name, repo_name)
|
||||||
full_url = '%s%s%s' % (url_for('web.repository', path=repo_path), '?tab=builds&newtrigger=',
|
full_url = '%s%s%s' % (url_for('web.repository', path=repo_path), '?tab=builds&newtrigger=',
|
||||||
trigger.uuid)
|
trigger.uuid)
|
||||||
|
|
||||||
|
@ -655,21 +655,22 @@ def attach_custom_build_trigger(namespace, repository_name):
|
||||||
|
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
|
|
||||||
@web.route('/<repopath:repository>')
|
@web.route('/<repopath:repository>')
|
||||||
@no_cache
|
@no_cache
|
||||||
@process_oauth
|
@process_oauth
|
||||||
@parse_repository_name_and_tag
|
@parse_repository_name(include_tag=True)
|
||||||
@anon_protect
|
@anon_protect
|
||||||
def redirect_to_repository(namespace, reponame, tag_name):
|
def redirect_to_repository(namespace_name, repo_name, tag_name):
|
||||||
permission = ReadRepositoryPermission(namespace, reponame)
|
permission = ReadRepositoryPermission(namespace_name, repo_name)
|
||||||
is_public = model.repository.repository_is_public(namespace, reponame)
|
is_public = model.repository.repository_is_public(namespace_name, repo_name)
|
||||||
|
|
||||||
if request.args.get('ac-discovery', 0) == 1:
|
if request.args.get('ac-discovery', 0) == 1:
|
||||||
return index('')
|
return index('')
|
||||||
|
|
||||||
if permission.can() or is_public:
|
if permission.can() or is_public:
|
||||||
repository_name = '/'.join([namespace, reponame])
|
repo_path = '/'.join([namespace_name, repo_name])
|
||||||
return redirect(url_for('web.repository', path=repository_name, tag=tag_name))
|
return redirect(url_for('web.repository', path=repo_path, tag=tag_name))
|
||||||
|
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
|
|
|
@ -5,4 +5,4 @@ export TROLLIUSDEBUG=1
|
||||||
|
|
||||||
python -m unittest discover -f
|
python -m unittest discover -f
|
||||||
python -m test.registry_tests -f
|
python -m test.registry_tests -f
|
||||||
python -m test.queue_threads -f
|
#python -m test.queue_threads -f
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import unittest
|
import unittest
|
||||||
import endpoints.decorated
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from app import app
|
from app import app
|
||||||
from initdb import setup_database_for_testing, finished_database_for_testing
|
from initdb import setup_database_for_testing, finished_database_for_testing
|
||||||
from specs import build_v2_index_specs
|
from test.specs import build_v2_index_specs
|
||||||
from endpoints.v2 import v2_bp
|
from endpoints.v2 import v2_bp
|
||||||
|
|
||||||
app.register_blueprint(v2_bp, url_prefix='/v2')
|
app.register_blueprint(v2_bp, url_prefix='/v2')
|
||||||
|
|
Reference in a new issue