Merge pull request #1155 from coreos-inc/torrenttest

Add torrent tests
This commit is contained in:
josephschorr 2016-01-20 13:42:42 -05:00
commit 3fdadb51b7
2 changed files with 159 additions and 60 deletions

View file

@ -15,6 +15,15 @@ class FakeStorage(BaseStorageV2):
def _init_path(self, path=None, create=False): def _init_path(self, path=None, create=False):
return path return path
def get_direct_download_url(self, path, expires_in=60, requires_cors=False):
try:
if self.get_content('supports_direct_download') == 'true':
return 'http://somefakeurl'
except:
pass
return None
def get_content(self, path): def get_content(self, path):
if not path in _FAKE_STORAGE_MAP: if not path in _FAKE_STORAGE_MAP:
raise IOError('Fake file %s not found' % path) raise IOError('Fake file %s not found' % path)

View file

@ -4,13 +4,15 @@ import os
import math import math
import random import random
import string import string
import resumablehashlib
import binascii
import Crypto.Random import Crypto.Random
from flask import request, jsonify from flask import request, jsonify
from flask.blueprints import Blueprint from flask.blueprints import Blueprint
from flask.ext.testing import LiveServerTestCase from flask.ext.testing import LiveServerTestCase
from app import app from app import app, storage
from data.database import close_db_filter, configure from data.database import close_db_filter, configure
from data import model from data import model
from endpoints.v1 import v1_bp from endpoints.v1 import v1_bp
@ -28,6 +30,7 @@ import json
import features import features
import hashlib import hashlib
import logging import logging
import bencode
import tarfile import tarfile
import shutil import shutil
@ -61,6 +64,12 @@ def generate_csrf():
return generate_csrf_token() return generate_csrf_token()
@testbp.route('/fakestoragedd/<enabled>', methods=['POST'])
def set_fakestorage_directdownload(enabled):
storage.put_content(['local_us'], 'supports_direct_download', enabled)
return 'OK'
@testbp.route('/feature/<feature_name>', methods=['POST']) @testbp.route('/feature/<feature_name>', methods=['POST'])
def set_feature(feature_name): def set_feature(feature_name):
import features import features
@ -129,6 +138,26 @@ _CLEAN_DATABASE_PATH = None
_JWK = RSAKey(key=RSA.generate(2048)) _JWK = RSAKey(key=RSA.generate(2048))
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']
layer_data = StringIO()
tar_file_info = tarfile.TarInfo(name='contents')
tar_file_info.type = tarfile.REGTYPE
tar_file_info.size = len(image_data['contents'])
tar_file = tarfile.open(fileobj=layer_data, mode='w|gz')
tar_file.addfile(tar_file_info, StringIO(image_data['contents']))
tar_file.close()
layer_bytes = layer_data.getvalue()
layer_data.close()
return layer_bytes
def get_new_database_uri(): def get_new_database_uri():
# If a clean copy of the database has not yet been created, create one now. # If a clean copy of the database has not yet been created, create one now.
global _CLEAN_DATABASE_PATH global _CLEAN_DATABASE_PATH
@ -281,19 +310,7 @@ class V1RegistryPushMixin(V1RegistryMixin):
data=json.dumps(image_json_data), auth='sig') data=json.dumps(image_json_data), auth='sig')
# PUT /v1/images/{imageID}/layer # PUT /v1/images/{imageID}/layer
tar_file_info = tarfile.TarInfo(name='image_name') layer_bytes = get_full_contents(image_data)
tar_file_info.type = tarfile.REGTYPE
tar_file_info.size = len(image_id)
layer_data = StringIO()
tar_file = tarfile.open(fileobj=layer_data, mode='w|gz')
tar_file.addfile(tar_file_info, StringIO(image_id))
tar_file.close()
layer_bytes = layer_data.getvalue()
layer_data.close()
self.conduct('PUT', '/v1/images/%s/layer' % image_id, self.conduct('PUT', '/v1/images/%s/layer' % image_id,
data=StringIO(layer_bytes), auth='sig') data=StringIO(layer_bytes), auth='sig')
@ -436,8 +453,12 @@ class V2RegistryPushMixin(V2RegistryMixin):
# Build a fake manifest. # Build a fake manifest.
tag_name = tag_name or 'latest' tag_name = tag_name or 'latest'
builder = SignedManifestBuilder(namespace, repository, tag_name) builder = SignedManifestBuilder(namespace, repository, tag_name)
full_contents = {}
for image_data in images: for image_data in images:
checksum = 'sha256:' + hashlib.sha256(image_data['contents']).hexdigest() full_contents[image_data['id']] = get_full_contents(image_data)
checksum = 'sha256:' + hashlib.sha256(full_contents[image_data['id']]).hexdigest()
if invalid: if invalid:
checksum = 'sha256:' + hashlib.sha256('foobarbaz').hexdigest() checksum = 'sha256:' + hashlib.sha256('foobarbaz').hexdigest()
@ -450,11 +471,11 @@ class V2RegistryPushMixin(V2RegistryMixin):
checksums = {} checksums = {}
for image_data in images: for image_data in images:
image_id = image_data['id'] image_id = image_data['id']
full_contents = image_data['contents'] layer_bytes = full_contents[image_data['id']]
chunks = image_data.get('chunks') chunks = image_data.get('chunks')
# Layer data should not yet exist. # Layer data should not yet exist.
checksum = 'sha256:' + hashlib.sha256(full_contents).hexdigest() checksum = 'sha256:' + hashlib.sha256(layer_bytes).hexdigest()
self.conduct('HEAD', '/v2/%s/%s/blobs/%s' % (namespace, repository, checksum), self.conduct('HEAD', '/v2/%s/%s/blobs/%s' % (namespace, repository, checksum),
expected_code=404, auth='jwt') expected_code=404, auth='jwt')
@ -467,7 +488,7 @@ class V2RegistryPushMixin(V2RegistryMixin):
# PATCH the image data into the layer. # PATCH the image data into the layer.
if chunks is None: if chunks is None:
self.conduct('PATCH', location, data=full_contents, expected_code=204, auth='jwt') self.conduct('PATCH', location, data=layer_bytes, expected_code=204, auth='jwt')
else: else:
for chunk in chunks: for chunk in chunks:
if len(chunk) == 3: if len(chunk) == 3:
@ -476,7 +497,7 @@ class V2RegistryPushMixin(V2RegistryMixin):
(start_byte, end_byte) = chunk (start_byte, end_byte) = chunk
expected_code = 204 expected_code = 204
contents_chunk = full_contents[start_byte:end_byte] contents_chunk = layer_bytes[start_byte:end_byte]
self.conduct('PATCH', location, data=contents_chunk, expected_code=expected_code, auth='jwt', self.conduct('PATCH', location, data=contents_chunk, expected_code=expected_code, auth='jwt',
headers={'Range': 'bytes=%s-%s' % (start_byte, end_byte)}) headers={'Range': 'bytes=%s-%s' % (start_byte, end_byte)})
@ -511,7 +532,7 @@ class V2RegistryPushMixin(V2RegistryMixin):
response = self.conduct('HEAD', '/v2/%s/%s/blobs/%s' % (namespace, repository, checksum), response = self.conduct('HEAD', '/v2/%s/%s/blobs/%s' % (namespace, repository, checksum),
expected_code=200, auth='jwt') expected_code=200, auth='jwt')
self.assertEquals(response.headers['Docker-Content-Digest'], checksum) self.assertEquals(response.headers['Docker-Content-Digest'], checksum)
self.assertEquals(response.headers['Content-Length'], str(len(full_contents))) self.assertEquals(response.headers['Content-Length'], str(len(layer_bytes)))
# Write the manifest. # Write the manifest.
put_code = 404 if invalid else expected_manifest_code put_code = 404 if invalid else expected_manifest_code
@ -555,7 +576,7 @@ class V2RegistryPullMixin(V2RegistryMixin):
result = self.conduct('GET', '/v2/%s/%s/blobs/%s' % (namespace, repository, blob_id), result = self.conduct('GET', '/v2/%s/%s/blobs/%s' % (namespace, repository, blob_id),
expected_code=200, auth='jwt') expected_code=200, auth='jwt')
blobs[blob_id] = result.text blobs[blob_id] = result.content
# Verify the V1 metadata is present for each expected image. # Verify the V1 metadata is present for each expected image.
found_v1_layers = set() found_v1_layers = set()
@ -570,6 +591,39 @@ class V2RegistryPullMixin(V2RegistryMixin):
return blobs return blobs
class V1RegistryLoginMixin(object):
def do_login(self, username, password, scope, expect_success=True):
data = {
'username': username,
'password': password,
}
response = self.conduct('POST', '/v1/users/', json_data=data, expected_code=400)
if expect_success:
self.assertEquals(response.text, '"Username or email already exists"')
else:
self.assertNotEquals(response.text, '"Username or email already exists"')
class V2RegistryLoginMixin(object):
def do_login(self, username, password, scope, expect_success=True, expected_code=None):
params = {
'account': username,
'scope': scope,
'service': app.config['SERVER_HOSTNAME'],
}
if expected_code is None:
if expect_success:
expected_code = 200
else:
expected_code = 403
response = self.conduct('GET', '/v2/auth', params=params, auth=(username, password),
expected_code=expected_code)
return response
class RegistryTestsMixin(object): class RegistryTestsMixin(object):
def test_push_pull_logging(self): def test_push_pull_logging(self):
# Push a new repository. # Push a new repository.
@ -815,7 +869,8 @@ class V1RegistryTests(V1RegistryPullMixin, V1RegistryPushMixin, RegistryTestsMix
def test_push_reponame_with_slashes(self): def test_push_reponame_with_slashes(self):
# Attempt to add a repository name with slashes. This should fail as we do not support it. # Attempt to add a repository name with slashes. This should fail as we do not support it.
images = [{ images = [{
'id': 'onlyimagehere' '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=400)
@ -824,7 +879,8 @@ class V1RegistryTests(V1RegistryPullMixin, V1RegistryPushMixin, RegistryTestsMix
images = [{ images = [{
'id': 'onlyimagehere', 'id': 'onlyimagehere',
'comment': 'Pawe\xc5\x82 Kami\xc5\x84ski <pawel.kaminski@codewise.com>'.decode('utf-8') 'comment': 'Pawe\xc5\x82 Kami\xc5\x84ski <pawel.kaminski@codewise.com>'.decode('utf-8'),
'contents': 'somecontents',
}] }]
self.do_push('devtable', 'unicodetest', 'devtable', 'password', images) self.do_push('devtable', 'unicodetest', 'devtable', 'password', images)
@ -833,8 +889,10 @@ class V1RegistryTests(V1RegistryPullMixin, V1RegistryPushMixin, RegistryTestsMix
def test_tag_validation(self): def test_tag_validation(self):
image_id = 'onlyimagehere' image_id = 'onlyimagehere'
images = [{ images = [{
'id': image_id 'id': image_id,
'contents': 'somecontents',
}] }]
self.do_push('public', 'newrepo', 'public', 'password', images) self.do_push('public', 'newrepo', 'public', 'password', images)
self.do_tag('public', 'newrepo', '1', image_id) self.do_tag('public', 'newrepo', '1', image_id)
self.do_tag('public', 'newrepo', 'x' * 128, image_id) self.do_tag('public', 'newrepo', 'x' * 128, image_id)
@ -887,8 +945,10 @@ class V2RegistryTests(V2RegistryPullMixin, V2RegistryPushMixin, RegistryTestsMix
def test_push_reponame_with_slashes(self): def test_push_reponame_with_slashes(self):
# Attempt to add a repository name with slashes. This should fail as we do not support it. # Attempt to add a repository name with slashes. This should fail as we do not support it.
images = [{ images = [{
'id': 'onlyimagehere' 'id': 'onlyimagehere',
'contents': 'somecontents',
}] }]
self.do_push('public', 'newrepo/somesubrepo', 'devtable', 'password', images, self.do_push('public', 'newrepo/somesubrepo', 'devtable', 'password', images,
expected_auth_code=400) expected_auth_code=400)
@ -1081,13 +1141,74 @@ class V1PushV2PullRegistryTests(V2RegistryPullMixin, V1RegistryPushMixin, Regist
RegistryTestCaseMixin, LiveServerTestCase): RegistryTestCaseMixin, LiveServerTestCase):
""" Tests for V1 push, V2 pull registry. """ """ Tests for V1 push, V2 pull registry. """
class V1PullV2PushRegistryTests(V1RegistryPullMixin, V2RegistryPushMixin, RegistryTestsMixin, class V1PullV2PushRegistryTests(V1RegistryPullMixin, V2RegistryPushMixin, RegistryTestsMixin,
RegistryTestCaseMixin, LiveServerTestCase): RegistryTestCaseMixin, LiveServerTestCase):
""" Tests for V1 pull, V2 push registry. """ """ Tests for V1 pull, V2 push registry. """
class VerbTests(RegistryTestCaseMixin, V1RegistryPushMixin, LiveServerTestCase): class TorrentTestMixin(V2RegistryPullMixin):
""" Tests for registry verbs. """ """ Mixin of tests for torrent support. """
def get_torrent(self, blobsum):
# Enable direct download URLs in fake storage.
self.conduct('POST', '/__test/fakestoragedd/true')
response = self.conduct('GET', '/c1/torrent/devtable/newrepo/blobs/' + blobsum,
auth=('devtable', 'password'))
# Disable direct download URLs in fake storage.
self.conduct('POST', '/__test/fakestoragedd/false')
return response.content
def test_get_basic_torrent(self):
initial_images = [
{
'id': 'initialid',
'contents': 'the initial image',
},
]
# Create the repo.
self.do_push('devtable', 'newrepo', 'devtable', 'password', images=initial_images)
# Retrieve the manifest for the tag.
blobs = self.do_pull('devtable', 'newrepo', 'devtable', 'password', manifest_id='latest',
images=initial_images)
self.assertEquals(1, len(list(blobs.keys())))
blobsum = list(blobs.keys())[0]
# Retrieve the torrent for the tag.
torrent = self.get_torrent(blobsum)
contents = bencode.bdecode(torrent)
# Ensure that there is a webseed.
self.assertEquals(contents['url-list'], 'http://somefakeurl')
# Ensure there is an announce and some pieces.
self.assertIsNotNone(contents.get('info', {}).get('pieces'))
self.assertIsNotNone(contents.get('announce'))
sha = resumablehashlib.sha1()
sha.update(blobs[blobsum])
expected = binascii.hexlify(sha.digest())
found = binascii.hexlify(contents['info']['pieces'])
self.assertEquals(expected, found)
class TorrentV1PushTests(RegistryTestCaseMixin, TorrentTestMixin, V1RegistryPushMixin, LiveServerTestCase):
""" Torrent tests via V1 push. """
pass
class TorrentV2PushTests(RegistryTestCaseMixin, TorrentTestMixin, V2RegistryPushMixin, LiveServerTestCase):
""" Torrent tests via V2 push. """
pass
class SquashingTests(RegistryTestCaseMixin, V1RegistryPushMixin, LiveServerTestCase):
""" Tests for registry squashing. """
def get_squashed_image(self): def get_squashed_image(self):
response = self.conduct('GET', '/c1/squash/devtable/newrepo/latest', auth='sig') response = self.conduct('GET', '/c1/squash/devtable/newrepo/latest', auth='sig')
@ -1184,39 +1305,8 @@ class VerbTests(RegistryTestCaseMixin, V1RegistryPushMixin, LiveServerTestCase):
# Ensure that the "image_name" file refers to the latest image, as it is the top layer. # Ensure that the "image_name" file refers to the latest image, as it is the top layer.
layer_tar = tarfile.open(fileobj=tar.extractfile(tar.getmember('%s/layer.tar' % expected_image_id))) layer_tar = tarfile.open(fileobj=tar.extractfile(tar.getmember('%s/layer.tar' % expected_image_id)))
image_name = layer_tar.extractfile(layer_tar.getmember('image_name')).read() image_contents = layer_tar.extractfile(layer_tar.getmember('contents')).read()
self.assertEquals('latestid', image_name) self.assertEquals('the latest image', image_contents)
class V1RegistryLoginMixin(object):
def do_login(self, username, password, scope, expect_success=True):
data = {
'username': username,
'password': password,
}
response = self.conduct('POST', '/v1/users/', json_data=data, expected_code=400)
if expect_success:
self.assertEquals(response.text, '"Username or email already exists"')
else:
self.assertNotEquals(response.text, '"Username or email already exists"')
class V2RegistryLoginMixin(object):
def do_login(self, username, password, scope, expect_success=True, expected_code=None):
params = {
'account': username,
'scope': scope,
'service': app.config['SERVER_HOSTNAME'],
}
if expected_code is None:
if expect_success:
expected_code = 200
else:
expected_code = 403
response = self.conduct('GET', '/v2/auth', params=params, auth=(username, password),
expected_code=expected_code)
return response
class LoginTests(object): class LoginTests(object):