Fix Docker Auth and our V2 registry paths to support library (i.e. namespace-less) repositories.
This support is placed behind a feature flag.
This commit is contained in:
parent
06b0f756bd
commit
e4ffaff869
37 changed files with 270 additions and 148 deletions
44
app.py
44
app.py
|
@ -50,15 +50,6 @@ DOCKER_V2_SIGNINGKEY_FILENAME = 'docker_v2.pem'
|
|||
app = Flask(__name__)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RegexConverter(BaseConverter):
|
||||
def __init__(self, url_map, *items):
|
||||
super(RegexConverter, self).__init__(url_map)
|
||||
self.regex = items[0]
|
||||
|
||||
|
||||
app.url_map.converters['regex'] = RegexConverter
|
||||
|
||||
# Instantiate the configuration.
|
||||
is_testing = 'TEST' in os.environ
|
||||
is_kubernetes = 'KUBERNETES_SERVICE_HOST' in os.environ
|
||||
|
@ -126,6 +117,41 @@ if app.config['SECRET_KEY'] is None:
|
|||
|
||||
features.import_features(app.config)
|
||||
|
||||
# Register custom converters.
|
||||
class RegexConverter(BaseConverter):
|
||||
""" Converter for handling custom regular expression patterns in paths. """
|
||||
def __init__(self, url_map, *items):
|
||||
super(RegexConverter, self).__init__(url_map)
|
||||
self.regex = items[0]
|
||||
|
||||
class RepositoryPathConverter(BaseConverter):
|
||||
""" Converter for handling repository paths. Handles both library and non-library paths (if
|
||||
configured).
|
||||
"""
|
||||
def __init__(self, url_map, *items):
|
||||
super(RepositoryPathConverter, self).__init__(url_map)
|
||||
self.weight = 200
|
||||
|
||||
if features.LIBRARY_SUPPORT:
|
||||
# Allow names without namespaces.
|
||||
self.regex = r'[^/]+(/[^/]+)?'
|
||||
else:
|
||||
self.regex = r'([^/]+/[^/]+)'
|
||||
|
||||
|
||||
class APIRepositoryPathConverter(BaseConverter):
|
||||
""" Converter for handling repository paths. Does not handle library paths.
|
||||
"""
|
||||
def __init__(self, url_map, *items):
|
||||
super(APIRepositoryPathConverter, self).__init__(url_map)
|
||||
self.weight = 200
|
||||
self.regex = r'([^/]+/[^/]+)'
|
||||
|
||||
|
||||
app.url_map.converters['regex'] = RegexConverter
|
||||
app.url_map.converters['repopath'] = RepositoryPathConverter
|
||||
app.url_map.converters['apirepopath'] = APIRepositoryPathConverter
|
||||
|
||||
Principal(app, use_sessions=False)
|
||||
|
||||
avatar = Avatar(app)
|
||||
|
|
|
@ -207,8 +207,9 @@ def identity_from_bearer_token(bearer_token, max_signed_s, public_key):
|
|||
logger.exception('We should not be minting invalid credentials')
|
||||
raise InvalidJWTException('Token contained invalid or malformed access grants')
|
||||
|
||||
lib_namespace = app.config['LIBRARY_NAMESPACE']
|
||||
for grant in payload['access']:
|
||||
namespace, repo_name = parse_namespace_repository(grant['name'])
|
||||
namespace, repo_name = parse_namespace_repository(grant['name'], lib_namespace)
|
||||
|
||||
if 'push' in grant['actions']:
|
||||
loaded_identity.provides.add(repository_write_grant(namespace, repo_name))
|
||||
|
|
10
config.py
10
config.py
|
@ -199,6 +199,16 @@ class DefaultConfig(object):
|
|||
# Feature Flag: Whether or not to rotate old action logs to storage.
|
||||
FEATURE_ACTION_LOG_ROTATION = False
|
||||
|
||||
# Feature Flag: Whether to allow for "namespace-less" repositories when pulling and pushing from
|
||||
# Docker.
|
||||
FEATURE_LIBRARY_SUPPORT = True
|
||||
|
||||
# The namespace to use for library repositories.
|
||||
# Note: This must remain 'library' until Docker removes their hard-coded namespace for libraries.
|
||||
# See: https://github.com/docker/docker/blob/master/registry/session.go#L320
|
||||
LIBRARY_NAMESPACE = 'library'
|
||||
|
||||
|
||||
BUILD_MANAGER = ('enterprise', {})
|
||||
|
||||
DISTRIBUTED_STORAGE_CONFIG = {
|
||||
|
|
|
@ -227,7 +227,7 @@ def parse_args(func):
|
|||
def parse_repository_name(func):
|
||||
@wraps(func)
|
||||
def wrapper(repository, *args, **kwargs):
|
||||
(namespace, repository) = parse_namespace_repository(repository)
|
||||
(namespace, repository) = parse_namespace_repository(repository, app.config['LIBRARY_NAMESPACE'])
|
||||
return func(namespace, repository, *args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
|
|
@ -145,7 +145,7 @@ def build_status_view(build_obj):
|
|||
return resp
|
||||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/build/')
|
||||
@resource('/v1/repository/<apirepopath:repository>/build/')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
class RepositoryBuildList(RepositoryParamResource):
|
||||
""" Resource related to creating and listing repository builds. """
|
||||
|
@ -288,7 +288,7 @@ class RepositoryBuildList(RepositoryParamResource):
|
|||
|
||||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/build/<build_uuid>')
|
||||
@resource('/v1/repository/<apirepopath:repository>/build/<build_uuid>')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
@path_param('build_uuid', 'The UUID of the build')
|
||||
class RepositoryBuildResource(RepositoryParamResource):
|
||||
|
@ -322,7 +322,7 @@ class RepositoryBuildResource(RepositoryParamResource):
|
|||
raise InvalidRequest('Build is currently running or has finished')
|
||||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/build/<build_uuid>/status')
|
||||
@resource('/v1/repository/<apirepopath:repository>/build/<build_uuid>/status')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
@path_param('build_uuid', 'The UUID of the build')
|
||||
class RepositoryBuildStatus(RepositoryParamResource):
|
||||
|
@ -339,7 +339,7 @@ class RepositoryBuildStatus(RepositoryParamResource):
|
|||
return build_status_view(build)
|
||||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/build/<build_uuid>/logs')
|
||||
@resource('/v1/repository/<apirepopath:repository>/build/<build_uuid>/logs')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
@path_param('build_uuid', 'The UUID of the build')
|
||||
class RepositoryBuildLogs(RepositoryParamResource):
|
||||
|
|
|
@ -43,7 +43,7 @@ def historical_image_view(image, image_map):
|
|||
return normal_view
|
||||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/image/')
|
||||
@resource('/v1/repository/<apirepopath:repository>/image/')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
class RepositoryImageList(RepositoryParamResource):
|
||||
""" Resource for listing repository images. """
|
||||
|
@ -82,7 +82,7 @@ class RepositoryImageList(RepositoryParamResource):
|
|||
}
|
||||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/image/<image_id>')
|
||||
@resource('/v1/repository/<apirepopath:repository>/image/<image_id>')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
@path_param('image_id', 'The Docker image ID')
|
||||
class RepositoryImage(RepositoryParamResource):
|
||||
|
|
|
@ -106,7 +106,7 @@ def get_aggregate_logs(start_time, end_time, performer_name=None, repository=Non
|
|||
}
|
||||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/logs')
|
||||
@resource('/v1/repository/<apirepopath:repository>/logs')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
class RepositoryLogs(RepositoryParamResource):
|
||||
""" Resource for fetching logs for the specific repository. """
|
||||
|
@ -175,7 +175,7 @@ class OrgLogs(ApiResource):
|
|||
raise Unauthorized()
|
||||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/aggregatelogs')
|
||||
@resource('/v1/repository/<apirepopath:repository>/aggregatelogs')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
class RepositoryAggregateLogs(RepositoryParamResource):
|
||||
""" Resource for fetching aggregated logs for the specific repository. """
|
||||
|
|
|
@ -38,7 +38,7 @@ def wrap_role_view_team(role_json, team):
|
|||
return role_json
|
||||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/permissions/team/')
|
||||
@resource('/v1/repository/<apirepopath:repository>/permissions/team/')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
class RepositoryTeamPermissionList(RepositoryParamResource):
|
||||
""" Resource for repository team permissions. """
|
||||
|
@ -57,7 +57,7 @@ class RepositoryTeamPermissionList(RepositoryParamResource):
|
|||
}
|
||||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/permissions/user/')
|
||||
@resource('/v1/repository/<apirepopath:repository>/permissions/user/')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
class RepositoryUserPermissionList(RepositoryParamResource):
|
||||
""" Resource for repository user permissions. """
|
||||
|
@ -97,7 +97,7 @@ class RepositoryUserPermissionList(RepositoryParamResource):
|
|||
}
|
||||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/permissions/user/<username>/transitive')
|
||||
@resource('/v1/repository/<apirepopath:repository>/permissions/user/<username>/transitive')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
@path_param('username', 'The username of the user to which the permissions apply')
|
||||
class RepositoryUserTransitivePermission(RepositoryParamResource):
|
||||
|
@ -121,7 +121,7 @@ class RepositoryUserTransitivePermission(RepositoryParamResource):
|
|||
}
|
||||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/permissions/user/<username>')
|
||||
@resource('/v1/repository/<apirepopath:repository>/permissions/user/<username>')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
@path_param('username', 'The username of the user to which the permission applies')
|
||||
class RepositoryUserPermission(RepositoryParamResource):
|
||||
|
@ -215,7 +215,7 @@ class RepositoryUserPermission(RepositoryParamResource):
|
|||
return 'Deleted', 204
|
||||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/permissions/team/<teamname>')
|
||||
@resource('/v1/repository/<apirepopath:repository>/permissions/team/<teamname>')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
@path_param('teamname', 'The name of the team to which the permission applies')
|
||||
class RepositoryTeamPermission(RepositoryParamResource):
|
||||
|
|
|
@ -30,7 +30,7 @@ def record_view(record):
|
|||
|
||||
@internal_only
|
||||
@show_if(features.MAILING)
|
||||
@resource('/v1/repository/<repopath:repository>/authorizedemail/<email>')
|
||||
@resource('/v1/repository/<apirepopath:repository>/authorizedemail/<email>')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
@path_param('email', 'The e-mail address')
|
||||
class RepositoryAuthorizedEmail(RepositoryParamResource):
|
||||
|
|
|
@ -225,7 +225,7 @@ class RepositoryList(ApiResource):
|
|||
}
|
||||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>')
|
||||
@resource('/v1/repository/<apirepopath:repository>')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
class Repository(RepositoryParamResource):
|
||||
"""Operations for managing a specific repository."""
|
||||
|
@ -339,7 +339,7 @@ class Repository(RepositoryParamResource):
|
|||
return 'Deleted', 204
|
||||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/changevisibility')
|
||||
@resource('/v1/repository/<apirepopath:repository>/changevisibility')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
class RepositoryVisibility(RepositoryParamResource):
|
||||
""" Custom verb for changing the visibility of the repository. """
|
||||
|
|
|
@ -38,7 +38,7 @@ def notification_view(note):
|
|||
}
|
||||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/notification/')
|
||||
@resource('/v1/repository/<apirepopath:repository>/notification/')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
class RepositoryNotificationList(RepositoryParamResource):
|
||||
""" Resource for dealing with listing and creating notifications on a repository. """
|
||||
|
@ -116,7 +116,7 @@ class RepositoryNotificationList(RepositoryParamResource):
|
|||
}
|
||||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/notification/<uuid>')
|
||||
@resource('/v1/repository/<apirepopath:repository>/notification/<uuid>')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
@path_param('uuid', 'The UUID of the notification')
|
||||
class RepositoryNotification(RepositoryParamResource):
|
||||
|
@ -149,7 +149,7 @@ class RepositoryNotification(RepositoryParamResource):
|
|||
return 'No Content', 204
|
||||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/notification/<uuid>/test')
|
||||
@resource('/v1/repository/<apirepopath:repository>/notification/<uuid>/test')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
@path_param('uuid', 'The UUID of the notification')
|
||||
class TestRepositoryNotification(RepositoryParamResource):
|
||||
|
|
|
@ -20,7 +20,7 @@ def token_view(token_obj):
|
|||
}
|
||||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/tokens/')
|
||||
@resource('/v1/repository/<apirepopath:repository>/tokens/')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
class RepositoryTokenList(RepositoryParamResource):
|
||||
""" Resource for creating and listing repository tokens. """
|
||||
|
@ -66,7 +66,7 @@ class RepositoryTokenList(RepositoryParamResource):
|
|||
return token_view(token), 201
|
||||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/tokens/<code>')
|
||||
@resource('/v1/repository/<apirepopath:repository>/tokens/<code>')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
@path_param('code', 'The token code')
|
||||
class RepositoryToken(RepositoryParamResource):
|
||||
|
|
|
@ -54,7 +54,7 @@ def _get_status(repo_image):
|
|||
|
||||
|
||||
@show_if(features.SECURITY_SCANNER)
|
||||
@resource('/v1/repository/<repopath:repository>/image/<imageid>/vulnerabilities')
|
||||
@resource('/v1/repository/<apirepopath:repository>/image/<imageid>/vulnerabilities')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
@path_param('imageid', 'The image ID')
|
||||
class RepositoryImageVulnerabilities(RepositoryParamResource):
|
||||
|
@ -89,7 +89,7 @@ class RepositoryImageVulnerabilities(RepositoryParamResource):
|
|||
|
||||
|
||||
@show_if(features.SECURITY_SCANNER)
|
||||
@resource('/v1/repository/<repopath:repository>/image/<imageid>/packages')
|
||||
@resource('/v1/repository/<apirepopath:repository>/image/<imageid>/packages')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
@path_param('imageid', 'The image ID')
|
||||
class RepositoryImagePackages(RepositoryParamResource):
|
||||
|
|
|
@ -11,7 +11,7 @@ from auth.auth_context import get_authenticated_user
|
|||
from util.names import TAG_ERROR, TAG_REGEX
|
||||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/tag/')
|
||||
@resource('/v1/repository/<apirepopath:repository>/tag/')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
class ListRepositoryTags(RepositoryParamResource):
|
||||
""" Resource for listing full repository tag history, alive *and dead*. """
|
||||
|
@ -60,7 +60,7 @@ class ListRepositoryTags(RepositoryParamResource):
|
|||
}
|
||||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/tag/<tag>')
|
||||
@resource('/v1/repository/<apirepopath:repository>/tag/<tag>')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
@path_param('tag', 'The name of the tag')
|
||||
class RepositoryTag(RepositoryParamResource):
|
||||
|
@ -128,7 +128,7 @@ class RepositoryTag(RepositoryParamResource):
|
|||
return 'Deleted', 204
|
||||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/tag/<tag>/images')
|
||||
@resource('/v1/repository/<apirepopath:repository>/tag/<tag>/images')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
@path_param('tag', 'The name of the tag')
|
||||
class RepositoryTagImages(RepositoryParamResource):
|
||||
|
@ -179,7 +179,7 @@ class RepositoryTagImages(RepositoryParamResource):
|
|||
|
||||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/tag/<tag>/revert')
|
||||
@resource('/v1/repository/<apirepopath:repository>/tag/<tag>/revert')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
@path_param('tag', 'The name of the tag')
|
||||
class RevertTag(RepositoryParamResource):
|
||||
|
|
|
@ -33,7 +33,7 @@ def _prepare_webhook_url(scheme, username, password, hostname, path):
|
|||
return urlunparse((scheme, auth_hostname, path, '', '', ''))
|
||||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/trigger/')
|
||||
@resource('/v1/repository/<apirepopath:repository>/trigger/')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
class BuildTriggerList(RepositoryParamResource):
|
||||
""" Resource for listing repository build triggers. """
|
||||
|
@ -48,7 +48,7 @@ class BuildTriggerList(RepositoryParamResource):
|
|||
}
|
||||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>')
|
||||
@resource('/v1/repository/<apirepopath:repository>/trigger/<trigger_uuid>')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
@path_param('trigger_uuid', 'The UUID of the build trigger')
|
||||
class BuildTrigger(RepositoryParamResource):
|
||||
|
@ -95,7 +95,7 @@ class BuildTrigger(RepositoryParamResource):
|
|||
return 'No Content', 204
|
||||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/subdir')
|
||||
@resource('/v1/repository/<apirepopath:repository>/trigger/<trigger_uuid>/subdir')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
@path_param('trigger_uuid', 'The UUID of the build trigger')
|
||||
@internal_only
|
||||
|
@ -143,7 +143,7 @@ class BuildTriggerSubdirs(RepositoryParamResource):
|
|||
raise Unauthorized()
|
||||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/activate')
|
||||
@resource('/v1/repository/<apirepopath:repository>/trigger/<trigger_uuid>/activate')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
@path_param('trigger_uuid', 'The UUID of the build trigger')
|
||||
class BuildTriggerActivate(RepositoryParamResource):
|
||||
|
@ -245,7 +245,7 @@ class BuildTriggerActivate(RepositoryParamResource):
|
|||
raise Unauthorized()
|
||||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/analyze')
|
||||
@resource('/v1/repository/<apirepopath:repository>/trigger/<trigger_uuid>/analyze')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
@path_param('trigger_uuid', 'The UUID of the build trigger')
|
||||
@internal_only
|
||||
|
@ -384,7 +384,7 @@ class BuildTriggerAnalyze(RepositoryParamResource):
|
|||
raise NotFound()
|
||||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/start')
|
||||
@resource('/v1/repository/<apirepopath:repository>/trigger/<trigger_uuid>/start')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
@path_param('trigger_uuid', 'The UUID of the build trigger')
|
||||
class ActivateBuildTrigger(RepositoryParamResource):
|
||||
|
@ -444,7 +444,7 @@ class ActivateBuildTrigger(RepositoryParamResource):
|
|||
return resp, 201, headers
|
||||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/builds')
|
||||
@resource('/v1/repository/<apirepopath:repository>/trigger/<trigger_uuid>/builds')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
@path_param('trigger_uuid', 'The UUID of the build trigger')
|
||||
class TriggerBuildList(RepositoryParamResource):
|
||||
|
@ -464,7 +464,7 @@ class TriggerBuildList(RepositoryParamResource):
|
|||
|
||||
FIELD_VALUE_LIMIT = 30
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/fields/<field_name>')
|
||||
@resource('/v1/repository/<apirepopath:repository>/trigger/<trigger_uuid>/fields/<field_name>')
|
||||
@internal_only
|
||||
class BuildTriggerFieldValues(RepositoryParamResource):
|
||||
""" Custom verb to fetch a values list for a particular field name. """
|
||||
|
@ -493,7 +493,7 @@ class BuildTriggerFieldValues(RepositoryParamResource):
|
|||
raise Unauthorized()
|
||||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/sources')
|
||||
@resource('/v1/repository/<apirepopath:repository>/trigger/<trigger_uuid>/sources')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
@path_param('trigger_uuid', 'The UUID of the build trigger')
|
||||
@internal_only
|
||||
|
|
|
@ -876,7 +876,7 @@ class StarredRepositoryList(ApiResource):
|
|||
}, 201
|
||||
|
||||
|
||||
@resource('/v1/user/starred/<repopath:repository>')
|
||||
@resource('/v1/user/starred/<apirepopath:repository>')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
class StarredRepository(RepositoryParamResource):
|
||||
""" Operations for managing a specific starred repository. """
|
||||
|
|
|
@ -8,7 +8,6 @@ from buildtrigger.bitbuckethandler import BitbucketBuildTrigger
|
|||
from endpoints.common import route_show_if
|
||||
from app import app
|
||||
from data import model
|
||||
from util.names import parse_repository_name
|
||||
from util.http import abort
|
||||
from auth.auth import require_session_login
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ from functools import wraps
|
|||
from config import frontend_visible_config
|
||||
from external_libraries import get_external_javascript, get_external_css
|
||||
from util.secscan.api import PRIORITY_LEVELS
|
||||
from util.names import parse_namespace_repository
|
||||
|
||||
import features
|
||||
|
||||
|
@ -47,11 +48,24 @@ def get_cache_busters():
|
|||
return CACHE_BUSTERS
|
||||
|
||||
|
||||
class RepoPathConverter(BaseConverter):
|
||||
regex = '[\.a-zA-Z0-9_\-]+/[\.a-zA-Z0-9_\-]+'
|
||||
weight = 200
|
||||
def parse_repository_name(f):
|
||||
@wraps(f)
|
||||
def wrapper(repository, *args, **kwargs):
|
||||
lib_namespace = app.config['LIBRARY_NAMESPACE']
|
||||
(namespace, repository) = parse_namespace_repository(repository, lib_namespace)
|
||||
return f(namespace, repository, *args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
def parse_repository_name_and_tag(f):
|
||||
@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
|
||||
|
||||
app.url_map.converters['repopath'] = RepoPathConverter
|
||||
|
||||
def route_show_if(value):
|
||||
def decorator(f):
|
||||
|
|
|
@ -3,10 +3,9 @@ import logging
|
|||
from flask import request, redirect, url_for, Blueprint
|
||||
from flask.ext.login import current_user
|
||||
|
||||
from endpoints.common import route_show_if
|
||||
from endpoints.common import route_show_if, parse_repository_name
|
||||
from app import app, github_trigger
|
||||
from data import model
|
||||
from util.names import parse_repository_name
|
||||
from util.http import abort
|
||||
from auth.permissions import AdministerRepositoryPermission
|
||||
from auth.auth import require_session_login
|
||||
|
@ -17,7 +16,7 @@ logger = logging.getLogger(__name__)
|
|||
client = app.config['HTTPCLIENT']
|
||||
githubtrigger = Blueprint('callback', __name__)
|
||||
|
||||
@githubtrigger.route('/github/callback/trigger/<path:repository>', methods=['GET'])
|
||||
@githubtrigger.route('/github/callback/trigger/<repopath:repository>', methods=['GET'])
|
||||
@route_show_if(features.GITHUB_BUILD)
|
||||
@require_session_login
|
||||
@parse_repository_name
|
||||
|
|
|
@ -4,11 +4,10 @@ import requests
|
|||
from flask import request, redirect, url_for, Blueprint
|
||||
from flask.ext.login import current_user
|
||||
|
||||
from endpoints.common import common_login, route_show_if
|
||||
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.names import parse_repository_name
|
||||
from util.validation import generate_valid_usernames
|
||||
from util.http import abort
|
||||
from auth.auth import require_session_login
|
||||
|
|
|
@ -9,12 +9,13 @@ from data import model
|
|||
from app import app, authentication, userevents, storage
|
||||
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 util.names import parse_repository_name, REPOSITORY_NAME_REGEX
|
||||
from util.names import REPOSITORY_NAME_REGEX
|
||||
from auth.permissions import (ModifyRepositoryPermission, UserAdminPermission,
|
||||
ReadRepositoryPermission, CreateRepositoryPermission,
|
||||
repository_read_grant, repository_write_grant)
|
||||
|
||||
from util.http import abort
|
||||
from endpoints.common import parse_repository_name
|
||||
from endpoints.v1 import v1_bp
|
||||
from endpoints.trackhelper import track_and_log
|
||||
from endpoints.notificationhelper import spawn_notification
|
||||
|
@ -167,7 +168,7 @@ def update_user(username):
|
|||
abort(403)
|
||||
|
||||
|
||||
@v1_bp.route('/repositories/<path:repository>', methods=['PUT'])
|
||||
@v1_bp.route('/repositories/<repopath:repository>', methods=['PUT'])
|
||||
@process_auth
|
||||
@parse_repository_name
|
||||
@generate_headers(scope=GrantType.WRITE_REPOSITORY, add_grant_for_status=201)
|
||||
|
@ -221,7 +222,7 @@ def create_repository(namespace, repository):
|
|||
return make_response('Created', 201)
|
||||
|
||||
|
||||
@v1_bp.route('/repositories/<path:repository>/images', methods=['PUT'])
|
||||
@v1_bp.route('/repositories/<repopath:repository>/images', methods=['PUT'])
|
||||
@process_auth
|
||||
@parse_repository_name
|
||||
@generate_headers(scope=GrantType.WRITE_REPOSITORY)
|
||||
|
@ -251,7 +252,7 @@ def update_images(namespace, repository):
|
|||
abort(403)
|
||||
|
||||
|
||||
@v1_bp.route('/repositories/<path:repository>/images', methods=['GET'])
|
||||
@v1_bp.route('/repositories/<repopath:repository>/images', methods=['GET'])
|
||||
@process_auth
|
||||
@parse_repository_name
|
||||
@generate_headers(scope=GrantType.READ_REPOSITORY)
|
||||
|
@ -277,7 +278,7 @@ def get_repository_images(namespace, repository):
|
|||
abort(403)
|
||||
|
||||
|
||||
@v1_bp.route('/repositories/<path:repository>/images', methods=['DELETE'])
|
||||
@v1_bp.route('/repositories/<repopath:repository>/images', methods=['DELETE'])
|
||||
@process_auth
|
||||
@parse_repository_name
|
||||
@generate_headers(scope=GrantType.WRITE_REPOSITORY)
|
||||
|
@ -286,7 +287,7 @@ def delete_repository_images(namespace, repository):
|
|||
abort(501, 'Not Implemented', issue='not-implemented')
|
||||
|
||||
|
||||
@v1_bp.route('/repositories/<path:repository>/auth', methods=['PUT'])
|
||||
@v1_bp.route('/repositories/<repopath:repository>/auth', methods=['PUT'])
|
||||
@parse_repository_name
|
||||
@anon_allowed
|
||||
def put_repository_auth(namespace, repository):
|
||||
|
|
|
@ -5,11 +5,12 @@ import json
|
|||
from flask import abort, request, jsonify, make_response, session
|
||||
|
||||
from app import app
|
||||
from util.names import TAG_ERROR, TAG_REGEX, parse_repository_name
|
||||
from util.names import TAG_ERROR, TAG_REGEX
|
||||
from auth.auth import process_auth
|
||||
from auth.permissions import (ReadRepositoryPermission,
|
||||
ModifyRepositoryPermission)
|
||||
from data import model
|
||||
from endpoints.common import parse_repository_name
|
||||
from endpoints.decorators import anon_protect
|
||||
from endpoints.v1 import v1_bp
|
||||
from endpoints.trackhelper import track_and_log
|
||||
|
@ -18,7 +19,7 @@ from endpoints.trackhelper import track_and_log
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@v1_bp.route('/repositories/<path:repository>/tags', methods=['GET'])
|
||||
@v1_bp.route('/repositories/<repopath:repository>/tags', methods=['GET'])
|
||||
@process_auth
|
||||
@anon_protect
|
||||
@parse_repository_name
|
||||
|
@ -33,7 +34,7 @@ def get_tags(namespace, repository):
|
|||
abort(403)
|
||||
|
||||
|
||||
@v1_bp.route('/repositories/<path:repository>/tags/<tag>', methods=['GET'])
|
||||
@v1_bp.route('/repositories/<repopath:repository>/tags/<tag>', methods=['GET'])
|
||||
@process_auth
|
||||
@anon_protect
|
||||
@parse_repository_name
|
||||
|
@ -53,7 +54,7 @@ def get_tag(namespace, repository, tag):
|
|||
abort(403)
|
||||
|
||||
|
||||
@v1_bp.route('/repositories/<path:repository>/tags/<tag>', methods=['PUT'])
|
||||
@v1_bp.route('/repositories/<repopath:repository>/tags/<tag>', methods=['PUT'])
|
||||
@process_auth
|
||||
@anon_protect
|
||||
@parse_repository_name
|
||||
|
@ -78,7 +79,7 @@ def put_tag(namespace, repository, tag):
|
|||
abort(403)
|
||||
|
||||
|
||||
@v1_bp.route('/repositories/<path:repository>/tags/<tag>', methods=['DELETE'])
|
||||
@v1_bp.route('/repositories/<repopath:repository>/tags/<tag>', methods=['DELETE'])
|
||||
@process_auth
|
||||
@anon_protect
|
||||
@parse_repository_name
|
||||
|
|
|
@ -17,13 +17,14 @@ from util.cache import cache_control
|
|||
from util.registry.filelike import wrap_with_handler, StreamSlice
|
||||
from util.registry.gzipstream import calculate_size_handler
|
||||
from util.registry.torrent import PieceHasher
|
||||
from endpoints.common import parse_repository_name
|
||||
from storage.basestorage import InvalidChunkException
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
BASE_BLOB_ROUTE = '/<namespace>/<repo_name>/blobs/<regex("{0}"):digest>'
|
||||
BASE_BLOB_ROUTE = '/<repopath:repository>/blobs/<regex("{0}"):digest>'
|
||||
BLOB_DIGEST_ROUTE = BASE_BLOB_ROUTE.format(digest_tools.DIGEST_PATTERN)
|
||||
RANGE_HEADER_REGEX = re.compile(r'^bytes=([0-9]+)-([0-9]+)$')
|
||||
BLOB_CONTENT_TYPE = 'application/octet-stream'
|
||||
|
@ -57,6 +58,7 @@ def _base_blob_fetch(namespace, repo_name, digest):
|
|||
|
||||
@v2_bp.route(BLOB_DIGEST_ROUTE, methods=['HEAD'])
|
||||
@process_registry_jwt_auth
|
||||
@parse_repository_name
|
||||
@require_repo_read
|
||||
@anon_protect
|
||||
@cache_control(max_age=31436000)
|
||||
|
@ -72,6 +74,7 @@ def check_blob_exists(namespace, repo_name, digest):
|
|||
|
||||
@v2_bp.route(BLOB_DIGEST_ROUTE, methods=['GET'])
|
||||
@process_registry_jwt_auth
|
||||
@parse_repository_name
|
||||
@require_repo_read
|
||||
@anon_protect
|
||||
@cache_control(max_age=31536000)
|
||||
|
@ -103,8 +106,9 @@ def _render_range(num_uploaded_bytes, with_bytes_prefix=True):
|
|||
return '{0}0-{1}'.format('bytes=' if with_bytes_prefix else '', num_uploaded_bytes - 1)
|
||||
|
||||
|
||||
@v2_bp.route('/<namespace>/<repo_name>/blobs/uploads/', methods=['POST'])
|
||||
@v2_bp.route('/<repopath:repository>/blobs/uploads/', methods=['POST'])
|
||||
@process_registry_jwt_auth
|
||||
@parse_repository_name
|
||||
@require_repo_write
|
||||
@anon_protect
|
||||
def start_blob_upload(namespace, repo_name):
|
||||
|
@ -121,8 +125,10 @@ def start_blob_upload(namespace, repo_name):
|
|||
if digest is None:
|
||||
# The user will send the blob data in another request
|
||||
accepted = make_response('', 202)
|
||||
accepted.headers['Location'] = url_for('v2.upload_chunk', namespace=namespace,
|
||||
repo_name=repo_name, upload_uuid=new_upload_uuid)
|
||||
accepted.headers['Location'] = url_for('v2.upload_chunk',
|
||||
repository='%s/%s' % (namespace, repo_name),
|
||||
upload_uuid=new_upload_uuid)
|
||||
|
||||
accepted.headers['Range'] = _render_range(0)
|
||||
accepted.headers['Docker-Upload-UUID'] = new_upload_uuid
|
||||
return accepted
|
||||
|
@ -136,8 +142,9 @@ def start_blob_upload(namespace, repo_name):
|
|||
return _finish_upload(namespace, repo_name, uploaded, digest)
|
||||
|
||||
|
||||
@v2_bp.route('/<namespace>/<repo_name>/blobs/uploads/<upload_uuid>', methods=['GET'])
|
||||
@v2_bp.route('/<repopath:repository>/blobs/uploads/<upload_uuid>', methods=['GET'])
|
||||
@process_registry_jwt_auth
|
||||
@parse_repository_name
|
||||
@require_repo_write
|
||||
@anon_protect
|
||||
def fetch_existing_upload(namespace, repo_name, upload_uuid):
|
||||
|
@ -311,13 +318,15 @@ def _finish_upload(namespace, repo_name, upload_obj, expected_digest):
|
|||
|
||||
response = make_response('', 201)
|
||||
response.headers['Docker-Content-Digest'] = expected_digest
|
||||
response.headers['Location'] = url_for('v2.download_blob', namespace=namespace,
|
||||
repo_name=repo_name, digest=expected_digest)
|
||||
response.headers['Location'] = url_for('v2.download_blob',
|
||||
repository='%s/%s' % (namespace, repo_name),
|
||||
digest=expected_digest)
|
||||
return response
|
||||
|
||||
|
||||
@v2_bp.route('/<namespace>/<repo_name>/blobs/uploads/<upload_uuid>', methods=['PATCH'])
|
||||
@v2_bp.route('/<repopath:repository>/blobs/uploads/<upload_uuid>', methods=['PATCH'])
|
||||
@process_registry_jwt_auth
|
||||
@parse_repository_name
|
||||
@require_repo_write
|
||||
@anon_protect
|
||||
def upload_chunk(namespace, repo_name, upload_uuid):
|
||||
|
@ -334,8 +343,9 @@ def upload_chunk(namespace, repo_name, upload_uuid):
|
|||
return accepted
|
||||
|
||||
|
||||
@v2_bp.route('/<namespace>/<repo_name>/blobs/uploads/<upload_uuid>', methods=['PUT'])
|
||||
@v2_bp.route('/<repopath:repository>/blobs/uploads/<upload_uuid>', methods=['PUT'])
|
||||
@process_registry_jwt_auth
|
||||
@parse_repository_name
|
||||
@require_repo_write
|
||||
@anon_protect
|
||||
def monolithic_upload_or_last_chunk(namespace, repo_name, upload_uuid):
|
||||
|
@ -352,8 +362,9 @@ def monolithic_upload_or_last_chunk(namespace, repo_name, upload_uuid):
|
|||
return _finish_upload(namespace, repo_name, found, digest)
|
||||
|
||||
|
||||
@v2_bp.route('/<namespace>/<repo_name>/blobs/uploads/<upload_uuid>', methods=['DELETE'])
|
||||
@v2_bp.route('/<repopath:repository>/blobs/uploads/<upload_uuid>', methods=['DELETE'])
|
||||
@process_registry_jwt_auth
|
||||
@parse_repository_name
|
||||
@require_repo_write
|
||||
@anon_protect
|
||||
def cancel_upload(namespace, repo_name, upload_uuid):
|
||||
|
@ -371,8 +382,9 @@ def cancel_upload(namespace, repo_name, upload_uuid):
|
|||
|
||||
|
||||
|
||||
@v2_bp.route('/<namespace>/<repo_name>/blobs/<digest>', methods=['DELETE'])
|
||||
@v2_bp.route('/<repopath:repository>/blobs/<digest>', methods=['DELETE'])
|
||||
@process_registry_jwt_auth
|
||||
@parse_repository_name
|
||||
@require_repo_write
|
||||
@anon_protect
|
||||
def delete_digest(namespace, repo_name, upload_uuid):
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import logging
|
||||
import jwt.utils
|
||||
import json
|
||||
import features
|
||||
|
||||
from peewee import IntegrityError
|
||||
from flask import make_response, request, url_for
|
||||
|
@ -8,25 +9,25 @@ from collections import namedtuple, OrderedDict
|
|||
from jwkest.jws import SIGNER_ALGS, keyrep
|
||||
from datetime import datetime
|
||||
|
||||
from app import docker_v2_signing_key
|
||||
from app import docker_v2_signing_key, app
|
||||
from auth.registry_jwt_auth import process_registry_jwt_auth
|
||||
from endpoints.decorators import anon_protect
|
||||
from endpoints.v2 import v2_bp, require_repo_read, require_repo_write
|
||||
from endpoints.v2.errors import (BlobUnknown, ManifestInvalid, ManifestUnverified,
|
||||
ManifestUnknown, TagInvalid, NameInvalid)
|
||||
from endpoints.v2.errors import (BlobUnknown, ManifestInvalid, ManifestUnknown, TagInvalid,
|
||||
NameInvalid)
|
||||
from endpoints.trackhelper import track_and_log
|
||||
from endpoints.notificationhelper import spawn_notification
|
||||
from digest import digest_tools
|
||||
from data import model
|
||||
from data.database import RepositoryTag
|
||||
|
||||
from endpoints.common import parse_repository_name
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
VALID_TAG_PATTERN = r'[\w][\w.-]{0,127}'
|
||||
|
||||
BASE_MANIFEST_ROUTE = '/<namespace>/<repo_name>/manifests/<regex("{0}"):manifest_ref>'
|
||||
BASE_MANIFEST_ROUTE = '/<repopath:repository>/manifests/<regex("{0}"):manifest_ref>'
|
||||
MANIFEST_DIGEST_ROUTE = BASE_MANIFEST_ROUTE.format(digest_tools.DIGEST_PATTERN)
|
||||
MANIFEST_TAGNAME_ROUTE = BASE_MANIFEST_ROUTE.format(VALID_TAG_PATTERN)
|
||||
|
||||
|
@ -61,9 +62,17 @@ class SignedManifest(object):
|
|||
|
||||
self._parsed = json.loads(manifest_bytes)
|
||||
self._signatures = self._parsed[_SIGNATURES_KEY]
|
||||
self._namespace, self._repo_name = self._parsed[_REPO_NAME_KEY].split('/')
|
||||
self._tag = self._parsed[_REPO_TAG_KEY]
|
||||
|
||||
repo_name_tuple = self._parsed[_REPO_NAME_KEY].split('/')
|
||||
if len(repo_name_tuple) > 1:
|
||||
self._namespace, self._repo_name = repo_name_tuple
|
||||
elif len(repo_name_tuple) == 1:
|
||||
self._namespace = ''
|
||||
self._repo_name = repo_name_tuple[0]
|
||||
else:
|
||||
raise ValueError('repo_name has too many or too few pieces')
|
||||
|
||||
self._validate()
|
||||
|
||||
def _validate(self):
|
||||
|
@ -144,9 +153,13 @@ class SignedManifestBuilder(object):
|
|||
""" Class which represents a manifest which is currently being built.
|
||||
"""
|
||||
def __init__(self, namespace, repo_name, tag, architecture='amd64', schema_ver=1):
|
||||
repo_name_key = '{0}/{1}'.format(namespace, repo_name)
|
||||
if namespace == '':
|
||||
repo_name_key = repo_name
|
||||
|
||||
self._base_payload = {
|
||||
_REPO_TAG_KEY: tag,
|
||||
_REPO_NAME_KEY: '{0}/{1}'.format(namespace, repo_name),
|
||||
_REPO_NAME_KEY: repo_name_key,
|
||||
_ARCH_KEY: architecture,
|
||||
_SCHEMA_VER: schema_ver,
|
||||
}
|
||||
|
@ -213,6 +226,7 @@ class SignedManifestBuilder(object):
|
|||
|
||||
@v2_bp.route(MANIFEST_TAGNAME_ROUTE, methods=['GET'])
|
||||
@process_registry_jwt_auth
|
||||
@parse_repository_name
|
||||
@require_repo_read
|
||||
@anon_protect
|
||||
def fetch_manifest_by_tagname(namespace, repo_name, manifest_ref):
|
||||
|
@ -242,6 +256,7 @@ def fetch_manifest_by_tagname(namespace, repo_name, manifest_ref):
|
|||
|
||||
@v2_bp.route(MANIFEST_DIGEST_ROUTE, methods=['GET'])
|
||||
@process_registry_jwt_auth
|
||||
@parse_repository_name
|
||||
@require_repo_read
|
||||
@anon_protect
|
||||
def fetch_manifest_by_digest(namespace, repo_name, manifest_ref):
|
||||
|
@ -262,12 +277,13 @@ def fetch_manifest_by_digest(namespace, repo_name, manifest_ref):
|
|||
|
||||
@v2_bp.route(MANIFEST_TAGNAME_ROUTE, methods=['PUT'])
|
||||
@process_registry_jwt_auth
|
||||
@parse_repository_name
|
||||
@require_repo_write
|
||||
@anon_protect
|
||||
def write_manifest_by_tagname(namespace, repo_name, manifest_ref):
|
||||
try:
|
||||
manifest = SignedManifest(request.data)
|
||||
except ValueError:
|
||||
except ValueError as ve:
|
||||
raise ManifestInvalid()
|
||||
|
||||
if manifest.tag != manifest_ref:
|
||||
|
@ -278,6 +294,7 @@ def write_manifest_by_tagname(namespace, repo_name, manifest_ref):
|
|||
|
||||
@v2_bp.route(MANIFEST_DIGEST_ROUTE, methods=['PUT'])
|
||||
@process_registry_jwt_auth
|
||||
@parse_repository_name
|
||||
@require_repo_write
|
||||
@anon_protect
|
||||
def write_manifest_by_digest(namespace, repo_name, manifest_ref):
|
||||
|
@ -293,8 +310,16 @@ def write_manifest_by_digest(namespace, repo_name, manifest_ref):
|
|||
|
||||
|
||||
def _write_manifest(namespace, repo_name, manifest):
|
||||
# Ensure that the manifest is for this repository.
|
||||
if manifest.namespace != namespace or manifest.repo_name != repo_name:
|
||||
# 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.
|
||||
if (manifest.namespace == '' and features.LIBRARY_SUPPORT and
|
||||
namespace == app.config['LIBRARY_NAMESPACE']):
|
||||
# This is a library manifest. All good.
|
||||
pass
|
||||
elif manifest.namespace != namespace:
|
||||
raise NameInvalid()
|
||||
|
||||
if manifest.repo_name != repo_name:
|
||||
raise NameInvalid()
|
||||
|
||||
# Ensure that the repository exists.
|
||||
|
@ -369,13 +394,15 @@ def _write_manifest(namespace, repo_name, manifest):
|
|||
|
||||
response = make_response('OK', 202)
|
||||
response.headers['Docker-Content-Digest'] = manifest_digest
|
||||
response.headers['Location'] = url_for('v2.fetch_manifest_by_digest', namespace=namespace,
|
||||
repo_name=repo_name, manifest_ref=manifest_digest)
|
||||
response.headers['Location'] = url_for('v2.fetch_manifest_by_digest',
|
||||
repository='%s/%s' % (namespace, repo_name),
|
||||
manifest_ref=manifest_digest)
|
||||
return response
|
||||
|
||||
|
||||
@v2_bp.route(MANIFEST_DIGEST_ROUTE, methods=['DELETE'])
|
||||
@process_registry_jwt_auth
|
||||
@parse_repository_name
|
||||
@require_repo_write
|
||||
@anon_protect
|
||||
def delete_manifest_by_digest(namespace, repo_name, manifest_ref):
|
||||
|
@ -406,8 +433,14 @@ def _generate_and_store_manifest(namespace, repo_name, tag_name):
|
|||
image = model.tag.get_tag_image(namespace, repo_name, tag_name)
|
||||
parents = model.image.get_parent_images(namespace, repo_name, image)
|
||||
|
||||
# If the manifest is being generated under the library namespace, then we make its namespace
|
||||
# empty.
|
||||
manifest_namespace = namespace
|
||||
if features.LIBRARY_SUPPORT and namespace == app.config['LIBRARY_NAMESPACE']:
|
||||
manifest_namespace = ''
|
||||
|
||||
# Create and populate the manifest builder
|
||||
builder = SignedManifestBuilder(namespace, repo_name, tag_name)
|
||||
builder = SignedManifestBuilder(manifest_namespace, repo_name, tag_name)
|
||||
|
||||
# Add the leaf layer
|
||||
builder.add_layer(image.storage.content_checksum, image.v1_json_metadata)
|
||||
|
|
|
@ -6,9 +6,11 @@ from endpoints.v2.errors import NameUnknown
|
|||
from endpoints.v2.v2util import add_pagination
|
||||
from endpoints.decorators import anon_protect
|
||||
from data import model
|
||||
from endpoints.common import parse_repository_name
|
||||
|
||||
@v2_bp.route('/<namespace>/<repo_name>/tags/list', methods=['GET'])
|
||||
@v2_bp.route('/<repopath:repository>/tags/list', methods=['GET'])
|
||||
@process_registry_jwt_auth
|
||||
@parse_repository_name
|
||||
@require_repo_read
|
||||
@anon_protect
|
||||
def list_all_tags(namespace, repo_name):
|
||||
|
@ -17,8 +19,7 @@ def list_all_tags(namespace, repo_name):
|
|||
raise NameUnknown()
|
||||
|
||||
query = model.tag.list_repository_tags(namespace, repo_name)
|
||||
|
||||
url = url_for('v2.list_all_tags', namespace=namespace, repo_name=repo_name)
|
||||
url = url_for('v2.list_all_tags', repository='%s/%s' % (namespace, repo_name))
|
||||
link, query = add_pagination(query, url)
|
||||
|
||||
response = jsonify({
|
||||
|
|
|
@ -23,7 +23,7 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
TOKEN_VALIDITY_LIFETIME_S = 60 * 60 # 1 hour
|
||||
SCOPE_REGEX = re.compile(
|
||||
r'^repository:([\.a-zA-Z0-9_\-]+/[\.a-zA-Z0-9_\-]+):(((push|pull|\*),)*(push|pull|\*))$'
|
||||
r'^repository:(([\.a-zA-Z0-9_\-]+/)?[\.a-zA-Z0-9_\-]+):(((push|pull|\*),)*(push|pull|\*))$'
|
||||
)
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
|
@ -74,9 +74,10 @@ def generate_registry_jwt():
|
|||
logger.debug('Match: %s', match.groups())
|
||||
|
||||
namespace_and_repo = match.group(1)
|
||||
actions = match.group(2).split(',')
|
||||
actions = match.group(3).split(',')
|
||||
|
||||
namespace, reponame = parse_namespace_repository(namespace_and_repo)
|
||||
lib_namespace = app.config['LIBRARY_NAMESPACE']
|
||||
namespace, reponame = parse_namespace_repository(namespace_and_repo, lib_namespace)
|
||||
|
||||
# Ensure that we are never creating an invalid repository.
|
||||
if not REPOSITORY_NAME_REGEX.match(reponame):
|
||||
|
|
|
@ -22,7 +22,7 @@ from formats.squashed import SquashedDockerImage
|
|||
from formats.aci import ACIImage
|
||||
from storage import Storage
|
||||
from endpoints.v2.blob import BLOB_DIGEST_ROUTE
|
||||
from endpoints.common import route_show_if
|
||||
from endpoints.common import route_show_if, parse_repository_name
|
||||
|
||||
|
||||
verbs = Blueprint('verbs', __name__)
|
||||
|
@ -376,6 +376,7 @@ def get_squashed_tag(namespace, repository, tag):
|
|||
@anon_protect
|
||||
@verbs.route('/torrent{0}'.format(BLOB_DIGEST_ROUTE), methods=['GET'])
|
||||
@process_auth
|
||||
@parse_repository_name
|
||||
def get_tag_torrent(namespace, repo_name, digest):
|
||||
permission = ReadRepositoryPermission(namespace, repo_name)
|
||||
public_repo = model.repository.repository_is_public(namespace, repo_name)
|
||||
|
|
|
@ -25,14 +25,14 @@ from buildtrigger.triggerutil import TriggerProviderException
|
|||
from data import model
|
||||
from data.database import db
|
||||
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)
|
||||
from endpoints.csrf import csrf_protect, generate_csrf_token, verify_csrf
|
||||
from endpoints.decorators import anon_protect, anon_allowed
|
||||
from health.healthcheck import get_healthchecker
|
||||
from util.cache import no_cache, cache_control
|
||||
from util.headers import parse_basic_auth
|
||||
from util.invoice import renderInvoiceToPdf
|
||||
from util.names import parse_repository_name, parse_repository_name_and_tag
|
||||
from util.seo import render_snapshot
|
||||
from util.systemlogs import build_logs_archive
|
||||
from util.useremails import send_email_changed
|
||||
|
@ -409,7 +409,7 @@ def confirm_recovery():
|
|||
abort(403)
|
||||
|
||||
|
||||
@web.route('/repository/<path:repository>/status', methods=['GET'])
|
||||
@web.route('/repository/<repopath:repository>/status', methods=['GET'])
|
||||
@parse_repository_name
|
||||
@anon_protect
|
||||
def build_status_badge(namespace, repository):
|
||||
|
@ -597,7 +597,7 @@ def download_logs_archive():
|
|||
abort(403)
|
||||
|
||||
|
||||
@web.route('/bitbucket/setup/<path:repository>', methods=['GET'])
|
||||
@web.route('/bitbucket/setup/<repopath:repository>', methods=['GET'])
|
||||
@require_session_login
|
||||
@parse_repository_name
|
||||
@route_show_if(features.BITBUCKET_BUILD)
|
||||
|
@ -630,7 +630,7 @@ def attach_bitbucket_trigger(namespace, repository_name):
|
|||
abort(403)
|
||||
|
||||
|
||||
@web.route('/customtrigger/setup/<path:repository>', methods=['GET'])
|
||||
@web.route('/customtrigger/setup/<repopath:repository>', methods=['GET'])
|
||||
@require_session_login
|
||||
@parse_repository_name
|
||||
def attach_custom_build_trigger(namespace, repository_name):
|
||||
|
@ -653,7 +653,7 @@ def attach_custom_build_trigger(namespace, repository_name):
|
|||
|
||||
abort(403)
|
||||
|
||||
@web.route('/<path:repository>')
|
||||
@web.route('/<repopath:repository>')
|
||||
@no_cache
|
||||
@process_oauth
|
||||
@parse_repository_name_and_tag
|
||||
|
|
|
@ -67,7 +67,7 @@ def stripe_webhook():
|
|||
return make_response('Okay')
|
||||
|
||||
|
||||
@webhooks.route('/push/<path:repository>/trigger/<trigger_uuid>', methods=['POST'])
|
||||
@webhooks.route('/push/<repopath:repository>/trigger/<trigger_uuid>', methods=['POST'])
|
||||
@webhooks.route('/push/trigger/<trigger_uuid>', methods=['POST'], defaults={'repository': ''})
|
||||
@process_auth
|
||||
def build_trigger_webhook(trigger_uuid, **kwargs):
|
||||
|
|
|
@ -537,6 +537,9 @@ def populate_database(minimal=False):
|
|||
org.stripe_id = TEST_STRIPE_ID
|
||||
org.save()
|
||||
|
||||
liborg = model.organization.create_organization('library', 'quay+library@devtable.com', new_user_1)
|
||||
liborg.save()
|
||||
|
||||
model.user.create_robot('coolrobot', org)
|
||||
|
||||
oauth_app_1 = model.oauth.create_application(org, 'Some Test App', 'http://localhost:8000',
|
||||
|
|
Binary file not shown.
|
@ -138,7 +138,14 @@ _CLEAN_DATABASE_PATH = None
|
|||
_JWK = RSAKey(key=RSA.generate(2048))
|
||||
|
||||
|
||||
def get_full_contents(image_data):
|
||||
def _get_repo_name(namespace, name):
|
||||
if namespace == '':
|
||||
return name
|
||||
|
||||
return '%s/%s' % (namespace, name)
|
||||
|
||||
|
||||
def _get_full_contents(image_data):
|
||||
if 'chunks' in image_data:
|
||||
# Data is just for chunking; no need for a real TAR.
|
||||
return image_data['contents']
|
||||
|
@ -213,7 +220,8 @@ class RegistryTestCaseMixin(LiveServerTestCase):
|
|||
self.csrf_token = self.conduct('GET', '/__test/csrf').text
|
||||
|
||||
def do_tag(self, namespace, repository, tag, image_id, expected_code=200):
|
||||
self.conduct('PUT', '/v1/repositories/%s/%s/tags/%s' % (namespace, repository, tag),
|
||||
repo_name = _get_repo_name(namespace, repository)
|
||||
self.conduct('PUT', '/v1/repositories/%s/tags/%s' % (repo_name, tag),
|
||||
data='"%s"' % image_id, expected_code=expected_code, auth='sig')
|
||||
|
||||
def conduct_api_login(self, username, password):
|
||||
|
@ -221,8 +229,9 @@ class RegistryTestCaseMixin(LiveServerTestCase):
|
|||
data=json.dumps(dict(username=username, password=password)),
|
||||
headers={'Content-Type': 'application/json'})
|
||||
|
||||
def change_repo_visibility(self, repository, namespace, visibility):
|
||||
self.conduct('POST', '/api/v1/repository/%s/%s/changevisibility' % (repository, namespace),
|
||||
def change_repo_visibility(self, namespace, repository, visibility):
|
||||
repo_name = _get_repo_name(namespace, repository)
|
||||
self.conduct('POST', '/api/v1/repository/%s/changevisibility' % repo_name,
|
||||
data=json.dumps(dict(visibility=visibility)),
|
||||
headers={'Content-Type': 'application/json'})
|
||||
|
||||
|
@ -284,12 +293,13 @@ class V1RegistryPushMixin(V1RegistryMixin):
|
|||
def do_push(self, namespace, repository, username, password, images=None, expected_code=201):
|
||||
images = images or self._get_default_images()
|
||||
auth = (username, password)
|
||||
repo_name = _get_repo_name(namespace, repository)
|
||||
|
||||
# Ping!
|
||||
self.v1_ping()
|
||||
|
||||
# PUT /v1/repositories/{namespace}/{repository}/
|
||||
self.conduct('PUT', '/v1/repositories/%s/%s' % (namespace, repository),
|
||||
self.conduct('PUT', '/v1/repositories/%s' % repo_name,
|
||||
data=json.dumps(images), auth=auth,
|
||||
expected_code=expected_code)
|
||||
|
||||
|
@ -310,7 +320,7 @@ class V1RegistryPushMixin(V1RegistryMixin):
|
|||
data=json.dumps(image_json_data), auth='sig')
|
||||
|
||||
# PUT /v1/images/{imageID}/layer
|
||||
layer_bytes = get_full_contents(image_data)
|
||||
layer_bytes = _get_full_contents(image_data)
|
||||
self.conduct('PUT', '/v1/images/%s/layer' % image_id,
|
||||
data=StringIO(layer_bytes), auth='sig')
|
||||
|
||||
|
@ -325,7 +335,7 @@ class V1RegistryPushMixin(V1RegistryMixin):
|
|||
self.do_tag(namespace, repository, 'latest', images[0]['id'])
|
||||
|
||||
# PUT /v1/repositories/{namespace}/{repository}/images
|
||||
self.conduct('PUT', '/v1/repositories/%s/%s/images' % (namespace, repository),
|
||||
self.conduct('PUT', '/v1/repositories/%s/images' % repo_name,
|
||||
expected_code=204,
|
||||
auth='sig')
|
||||
|
||||
|
@ -334,6 +344,7 @@ class V1RegistryPullMixin(V1RegistryMixin):
|
|||
def do_pull(self, namespace, repository, username=None, password='password', expected_code=200,
|
||||
images=None):
|
||||
images = images or self._get_default_images()
|
||||
repo_name = _get_repo_name(namespace, repository)
|
||||
|
||||
auth = None
|
||||
if username:
|
||||
|
@ -342,7 +353,7 @@ class V1RegistryPullMixin(V1RegistryMixin):
|
|||
# Ping!
|
||||
self.v1_ping()
|
||||
|
||||
prefix = '/v1/repositories/%s/%s/' % (namespace, repository)
|
||||
prefix = '/v1/repositories/%s/' % repo_name
|
||||
|
||||
# GET /v1/repositories/{namespace}/{repository}/
|
||||
self.conduct('GET', prefix + 'images', auth=auth, expected_code=expected_code)
|
||||
|
@ -417,9 +428,11 @@ class V2RegistryMixin(BaseRegistryMixin):
|
|||
|
||||
def do_auth(self, username, password, namespace, repository, expected_code=200, scopes=[]):
|
||||
auth = (username, password)
|
||||
repo_name = _get_repo_name(namespace, repository)
|
||||
|
||||
params = {
|
||||
'account': username,
|
||||
'scope': 'repository:%s/%s:%s' % (namespace, repository, ','.join(scopes)),
|
||||
'scope': 'repository:%s:%s' % (repo_name, ','.join(scopes)),
|
||||
'service': app.config['SERVER_HOSTNAME'],
|
||||
}
|
||||
|
||||
|
@ -439,6 +452,7 @@ class V2RegistryPushMixin(V2RegistryMixin):
|
|||
cancel=False, invalid=False, expected_manifest_code=202, expected_auth_code=200,
|
||||
scopes=None):
|
||||
images = images or self._get_default_images()
|
||||
repo_name = _get_repo_name(namespace, repository)
|
||||
|
||||
# Ping!
|
||||
self.v2_ping()
|
||||
|
@ -456,7 +470,7 @@ class V2RegistryPushMixin(V2RegistryMixin):
|
|||
full_contents = {}
|
||||
|
||||
for image_data in images:
|
||||
full_contents[image_data['id']] = get_full_contents(image_data)
|
||||
full_contents[image_data['id']] = _get_full_contents(image_data)
|
||||
|
||||
checksum = 'sha256:' + hashlib.sha256(full_contents[image_data['id']]).hexdigest()
|
||||
if invalid:
|
||||
|
@ -476,11 +490,11 @@ class V2RegistryPushMixin(V2RegistryMixin):
|
|||
|
||||
# Layer data should not yet exist.
|
||||
checksum = 'sha256:' + hashlib.sha256(layer_bytes).hexdigest()
|
||||
self.conduct('HEAD', '/v2/%s/%s/blobs/%s' % (namespace, repository, checksum),
|
||||
self.conduct('HEAD', '/v2/%s/blobs/%s' % (repo_name, checksum),
|
||||
expected_code=404, auth='jwt')
|
||||
|
||||
# Start a new upload of the layer data.
|
||||
response = self.conduct('POST', '/v2/%s/%s/blobs/uploads/' % (namespace, repository),
|
||||
response = self.conduct('POST', '/v2/%s/blobs/uploads/' % repo_name,
|
||||
expected_code=202, auth='jwt')
|
||||
|
||||
upload_uuid = response.headers['Docker-Upload-UUID']
|
||||
|
@ -505,7 +519,7 @@ class V2RegistryPushMixin(V2RegistryMixin):
|
|||
return
|
||||
|
||||
# Retrieve the upload status at each point.
|
||||
status_url = '/v2/%s/%s/blobs/uploads/%s' % (namespace, repository, upload_uuid)
|
||||
status_url = '/v2/%s/blobs/uploads/%s' % (repo_name, upload_uuid)
|
||||
response = self.conduct('GET', status_url, expected_code=204, auth='jwt',
|
||||
headers=dict(host=self.get_server_url()))
|
||||
self.assertEquals(response.headers['Docker-Upload-UUID'], upload_uuid)
|
||||
|
@ -516,7 +530,7 @@ class V2RegistryPushMixin(V2RegistryMixin):
|
|||
auth='jwt')
|
||||
|
||||
# Ensure the upload was canceled.
|
||||
status_url = '/v2/%s/%s/blobs/uploads/%s' % (namespace, repository, upload_uuid)
|
||||
status_url = '/v2/%s/blobs/uploads/%s' % (repo_name, upload_uuid)
|
||||
self.conduct('GET', status_url, expected_code=404, auth='jwt',
|
||||
headers=dict(host=self.get_server_url()))
|
||||
return
|
||||
|
@ -529,14 +543,14 @@ class V2RegistryPushMixin(V2RegistryMixin):
|
|||
checksums[image_id] = checksum
|
||||
|
||||
# Ensure the layer exists now.
|
||||
response = self.conduct('HEAD', '/v2/%s/%s/blobs/%s' % (namespace, repository, checksum),
|
||||
response = self.conduct('HEAD', '/v2/%s/blobs/%s' % (repo_name, checksum),
|
||||
expected_code=200, auth='jwt')
|
||||
self.assertEquals(response.headers['Docker-Content-Digest'], checksum)
|
||||
self.assertEquals(response.headers['Content-Length'], str(len(layer_bytes)))
|
||||
|
||||
# Write the manifest.
|
||||
put_code = 404 if invalid else expected_manifest_code
|
||||
self.conduct('PUT', '/v2/%s/%s/manifests/%s' % (namespace, repository, tag_name),
|
||||
self.conduct('PUT', '/v2/%s/manifests/%s' % (repo_name, tag_name),
|
||||
data=manifest.bytes, expected_code=put_code,
|
||||
headers={'Content-Type': 'application/json'}, auth='jwt')
|
||||
|
||||
|
@ -547,6 +561,7 @@ class V2RegistryPullMixin(V2RegistryMixin):
|
|||
def do_pull(self, namespace, repository, username=None, password='password', expected_code=200,
|
||||
manifest_id=None, expected_manifest_code=200, images=None):
|
||||
images = images or self._get_default_images()
|
||||
repo_name = _get_repo_name(namespace, repository)
|
||||
|
||||
# Ping!
|
||||
self.v2_ping()
|
||||
|
@ -559,7 +574,7 @@ class V2RegistryPullMixin(V2RegistryMixin):
|
|||
|
||||
# Retrieve the manifest for the tag or digest.
|
||||
manifest_id = manifest_id or 'latest'
|
||||
response = self.conduct('GET', '/v2/%s/%s/manifests/%s' % (namespace, repository, manifest_id),
|
||||
response = self.conduct('GET', '/v2/%s/manifests/%s' % (repo_name, manifest_id),
|
||||
auth='jwt', expected_code=expected_manifest_code)
|
||||
if expected_manifest_code != 200:
|
||||
return
|
||||
|
@ -573,7 +588,7 @@ class V2RegistryPullMixin(V2RegistryMixin):
|
|||
blobs = {}
|
||||
for layer in manifest_data['fsLayers']:
|
||||
blob_id = layer['blobSum']
|
||||
result = self.conduct('GET', '/v2/%s/%s/blobs/%s' % (namespace, repository, blob_id),
|
||||
result = self.conduct('GET', '/v2/%s/blobs/%s' % (repo_name, blob_id),
|
||||
expected_code=200, auth='jwt')
|
||||
|
||||
blobs[blob_id] = result.content
|
||||
|
@ -863,6 +878,17 @@ class RegistryTestsMixin(object):
|
|||
self.do_pull('buynlarge', 'newrepo', 'devtable', 'password')
|
||||
|
||||
|
||||
def test_library_repo(self):
|
||||
self.do_push('', 'newrepo', 'devtable', 'password')
|
||||
self.do_pull('', 'newrepo', 'devtable', 'password')
|
||||
self.do_pull('library', 'newrepo', 'devtable', 'password')
|
||||
|
||||
def test_library_disabled(self):
|
||||
with TestFeature(self, 'LIBRARY_SUPPORT', False):
|
||||
self.do_push('library', 'newrepo', 'devtable', 'password')
|
||||
self.do_pull('library', 'newrepo', 'devtable', 'password')
|
||||
|
||||
|
||||
class V1RegistryTests(V1RegistryPullMixin, V1RegistryPushMixin, RegistryTestsMixin,
|
||||
RegistryTestCaseMixin, LiveServerTestCase):
|
||||
""" Tests for V1 registry. """
|
||||
|
@ -872,7 +898,7 @@ class V1RegistryTests(V1RegistryPullMixin, V1RegistryPushMixin, RegistryTestsMix
|
|||
'id': 'onlyimagehere',
|
||||
'contents': 'somecontents',
|
||||
}]
|
||||
self.do_push('public', 'newrepo/somesubrepo', 'public', 'password', images, expected_code=400)
|
||||
self.do_push('public', 'newrepo/somesubrepo', 'public', 'password', images, expected_code=404)
|
||||
|
||||
def test_push_unicode_metadata(self):
|
||||
self.conduct_api_login('devtable', 'password')
|
||||
|
@ -896,7 +922,7 @@ class V1RegistryTests(V1RegistryPullMixin, V1RegistryPushMixin, RegistryTestsMix
|
|||
self.do_push('public', 'newrepo', 'public', 'password', images)
|
||||
self.do_tag('public', 'newrepo', '1', image_id)
|
||||
self.do_tag('public', 'newrepo', 'x' * 128, image_id)
|
||||
self.do_tag('public', 'newrepo', '', image_id, expected_code=400)
|
||||
self.do_tag('public', 'newrepo', '', image_id, expected_code=404)
|
||||
self.do_tag('public', 'newrepo', 'x' * 129, image_id, expected_code=400)
|
||||
self.do_tag('public', 'newrepo', '.fail', image_id, expected_code=400)
|
||||
self.do_tag('public', 'newrepo', '-fail', image_id, expected_code=400)
|
||||
|
@ -1440,6 +1466,16 @@ class V2LoginTests(V2RegistryLoginMixin, LoginTests, RegistryTestCaseMixin, Base
|
|||
def test_nouser_push_publicrepo(self):
|
||||
self.do_login('', '', expected_code=401, scope='repository:public/publicrepo:push')
|
||||
|
||||
def test_library_invaliduser(self):
|
||||
self.do_login('invaliduser', 'password', expected_code=401, scope='repository:librepo:pull,push')
|
||||
|
||||
def test_library_noaccess(self):
|
||||
self.do_login('freshuser', 'password', expected_code=403, scope='repository:librepo:pull,push')
|
||||
|
||||
def test_library_access(self):
|
||||
self.do_login('devtable', 'password', expect_success=200, scope='repository:librepo:pull,push')
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
|
@ -276,8 +276,7 @@ class IndexV2TestSpec(object):
|
|||
return self
|
||||
|
||||
def get_url(self):
|
||||
namespace, repo_name = parse_namespace_repository(self.repo_name)
|
||||
return url_for(self.index_name, namespace=namespace, repo_name=repo_name, **self.kwargs)
|
||||
return url_for(self.index_name, repository=self.repo_name, **self.kwargs)
|
||||
|
||||
def gen_basic_auth(self, username, password):
|
||||
encoded = b64encode('%s:%s' % (username, password))
|
||||
|
|
|
@ -644,7 +644,7 @@ class TestConductSearch(ApiTestCase):
|
|||
json = self.getJsonResponse(ConductSearch,
|
||||
params=dict(query='owners'))
|
||||
|
||||
self.assertEquals(1, len(json['results']))
|
||||
self.assertEquals(2, len(json['results']))
|
||||
self.assertEquals(json['results'][0]['kind'], 'team')
|
||||
self.assertEquals(json['results'][0]['name'], 'owners')
|
||||
|
||||
|
|
|
@ -63,7 +63,7 @@ class _SpecTestBuilder(type):
|
|||
|
||||
session_vars = []
|
||||
if test_spec.sess_repo:
|
||||
ns, repo = parse_namespace_repository(test_spec.sess_repo)
|
||||
ns, repo = parse_namespace_repository(test_spec.sess_repo, 'library')
|
||||
session_vars.append(('namespace', ns))
|
||||
session_vars.append(('repository', repo))
|
||||
|
||||
|
|
|
@ -3,7 +3,6 @@ import endpoints.decorated
|
|||
import json
|
||||
|
||||
from app import app
|
||||
from util.names import parse_namespace_repository
|
||||
from initdb import setup_database_for_testing, finished_database_for_testing
|
||||
from specs import build_v2_index_specs
|
||||
from endpoints.v2 import v2_bp
|
||||
|
|
|
@ -10,10 +10,11 @@ TAG_REGEX = re.compile(r'^[\w][\w\.-]{0,127}$')
|
|||
TAG_ERROR = ('Invalid tag: must match [A-Za-z0-9_.-], NOT start with "." or "-", '
|
||||
'and can contain 1-128 characters')
|
||||
|
||||
def parse_namespace_repository(repository, include_tag=False):
|
||||
|
||||
def parse_namespace_repository(repository, library_namespace, include_tag=False):
|
||||
parts = repository.rstrip('/').split('/', 1)
|
||||
if len(parts) < 2:
|
||||
namespace = 'library'
|
||||
namespace = library_namespace
|
||||
repository = parts[0]
|
||||
else:
|
||||
(namespace, repository) = parts
|
||||
|
@ -30,20 +31,6 @@ def parse_namespace_repository(repository, include_tag=False):
|
|||
return (namespace, repository, tag)
|
||||
return (namespace, repository)
|
||||
|
||||
def parse_repository_name(f):
|
||||
@wraps(f)
|
||||
def wrapper(repository, *args, **kwargs):
|
||||
(namespace, repository) = parse_namespace_repository(repository)
|
||||
return f(namespace, repository, *args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
def parse_repository_name_and_tag(f):
|
||||
@wraps(f)
|
||||
def wrapper(repository, *args, **kwargs):
|
||||
namespace, repository, tag = parse_namespace_repository(repository, include_tag=True)
|
||||
return f(namespace, repository, tag, *args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
def format_robot_username(parent_username, robot_shortname):
|
||||
return '%s+%s' % (parent_username, robot_shortname)
|
||||
|
|
Reference in a new issue