diff --git a/test/registry/protocol_fixtures.py b/test/registry/protocol_fixtures.py index f4ed8d9a2..04a1e0f37 100644 --- a/test/registry/protocol_fixtures.py +++ b/test/registry/protocol_fixtures.py @@ -37,6 +37,45 @@ def sized_images(): ] +@pytest.fixture(scope="session") +def multi_layer_images(): + """ Returns complex images (with sizes) for push and pull testing. """ + # Note: order is from base layer down to leaf. + layer1_bytes = layer_bytes_for_contents('layer 1 contents', mode='', other_files={ + 'file1': 'from-layer-1', + }) + + layer2_bytes = layer_bytes_for_contents('layer 2 contents', mode='', other_files={ + 'file2': 'from-layer-2', + }) + + layer3_bytes = layer_bytes_for_contents('layer 3 contents', mode='', other_files={ + 'file1': 'from-layer-3', + 'file3': 'from-layer-3', + }) + + layer4_bytes = layer_bytes_for_contents('layer 4 contents', mode='', other_files={ + 'file3': 'from-layer-4', + }) + + layer5_bytes = layer_bytes_for_contents('layer 5 contents', mode='', other_files={ + 'file4': 'from-layer-5', + }) + + return [ + Image(id='layer1', bytes=layer1_bytes, parent_id=None, size=len(layer1_bytes), + config={'internal_id': 'layer1'}), + Image(id='layer2', bytes=layer2_bytes, parent_id='layer1', size=len(layer2_bytes), + config={'internal_id': 'layer2'}), + Image(id='layer3', bytes=layer3_bytes, parent_id='layer2', size=len(layer3_bytes), + config={'internal_id': 'layer3'}), + Image(id='layer4', bytes=layer4_bytes, parent_id='layer3', size=len(layer4_bytes), + config={'internal_id': 'layer4'}), + Image(id='someid', bytes=layer5_bytes, parent_id='layer4', size=len(layer5_bytes), + config={'internal_id': 'layer5'}), + ] + + @pytest.fixture(scope="session") def jwk(): return RSAKey(key=RSA.generate(2048)) diff --git a/test/registry/protocols.py b/test/registry/protocols.py index 4311f271c..aa9edae3c 100644 --- a/test/registry/protocols.py +++ b/test/registry/protocols.py @@ -14,8 +14,9 @@ PushResult = namedtuple('PushResult', ['checksums', 'manifests', 'headers']) PullResult = namedtuple('PullResult', ['manifests', 'image_ids']) -def layer_bytes_for_contents(contents, mode='|gz'): +def layer_bytes_for_contents(contents, mode='|gz', other_files=None): layer_data = StringIO() + tar_file = tarfile.open(fileobj=layer_data, mode='w' + mode) def add_file(name, contents): tar_file_info = tarfile.TarInfo(name=name) @@ -23,12 +24,16 @@ def layer_bytes_for_contents(contents, mode='|gz'): tar_file_info.size = len(contents) tar_file_info.mtime = 1 - tar_file = tarfile.open(fileobj=layer_data, mode='w' + mode) tar_file.addfile(tar_file_info, StringIO(contents)) - tar_file.close() add_file('contents', contents) + if other_files is not None: + for file_name, file_contents in other_files.iteritems(): + add_file(file_name, file_contents) + + tar_file.close() + layer_bytes = layer_data.getvalue() layer_data.close() return layer_bytes diff --git a/test/registry/registry_tests.py b/test/registry/registry_tests.py index 71f0e5c38..877a02f0d 100644 --- a/test/registry/registry_tests.py +++ b/test/registry/registry_tests.py @@ -930,6 +930,58 @@ def test_squashed_image_disabled_user(pusher, sized_images, liveserver_session, assert response.status_code == 403 +@pytest.mark.parametrize('use_estimates', [ + False, + True, +]) +def test_multilayer_squashed_images(use_estimates, pusher, multi_layer_images, liveserver_session, + liveserver, registry_server_executor, app_reloader): + """ Test: Pulling of multilayer, complex squashed images. """ + credentials = ('devtable', 'password') + + # Push an image to download. + pusher.push(liveserver_session, 'devtable', 'newrepo', 'latest', multi_layer_images, + credentials=credentials) + + if use_estimates: + # Clear the uncompressed size stored for the images, to ensure that we estimate instead. + for image in multi_layer_images: + registry_server_executor.on(liveserver).clear_uncompressed_size(image.id) + + # Pull the squashed version. + response = liveserver_session.get('/c1/squash/devtable/newrepo/latest', auth=credentials) + tar = tarfile.open(fileobj=StringIO(response.content)) + + # Verify the squashed image. + expected_image_id = 'cdc6d6c0d07d2cbacfc579e49ce0c256c5084b9b2b16c1b1b0c45f26a12a4ba5' + expected_names = ['repositories', + expected_image_id, + '%s/json' % expected_image_id, + '%s/VERSION' % expected_image_id, + '%s/layer.tar' % expected_image_id] + + assert tar.getnames() == expected_names + + # Verify the JSON image data. + json_data = (tar.extractfile(tar.getmember('%s/json' % expected_image_id)).read()) + + # Ensure the JSON loads and parses. + result = json.loads(json_data) + assert result['id'] == expected_image_id + assert result['config']['internal_id'] == 'layer5' + + # Ensure that squashed layer tar can be opened. + tar = tarfile.open(fileobj=tar.extractfile(tar.getmember('%s/layer.tar' % expected_image_id))) + assert set(tar.getnames()) == {'contents', 'file1', 'file2', 'file3', 'file4'} + + # Check the contents of various files. + assert tar.extractfile('contents').read() == 'layer 5 contents' + assert tar.extractfile('file1').read() == 'from-layer-3' + assert tar.extractfile('file2').read() == 'from-layer-2' + assert tar.extractfile('file3').read() == 'from-layer-4' + assert tar.extractfile('file4').read() == 'from-layer-5' + + @pytest.mark.parametrize('use_estimates', [ False, True,