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
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
|
||||
|
|
Reference in a new issue