Properly handle the empty layer when pushing schema 2 manifests
Docker doesn't send us the contents of this layer, so we are forced to synthesize it ourselves
This commit is contained in:
parent
947c029afa
commit
4985040d31
13 changed files with 173 additions and 25 deletions
|
@ -24,7 +24,6 @@ def basic_images():
|
|||
]
|
||||
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def different_images():
|
||||
""" Returns different basic images for push and pull testing. """
|
||||
|
@ -37,7 +36,6 @@ def different_images():
|
|||
]
|
||||
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def sized_images():
|
||||
""" Returns basic images (with sizes) for push and pull testing. """
|
||||
|
@ -106,6 +104,24 @@ def remote_images():
|
|||
]
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def images_with_empty_layer():
|
||||
""" Returns images for push and pull testing that contain an empty layer. """
|
||||
# Note: order is from base layer down to leaf.
|
||||
parent_bytes = layer_bytes_for_contents('parent contents')
|
||||
empty_bytes = layer_bytes_for_contents('', empty=True)
|
||||
image_bytes = layer_bytes_for_contents('some contents')
|
||||
middle_bytes = layer_bytes_for_contents('middle')
|
||||
|
||||
return [
|
||||
Image(id='parentid', bytes=parent_bytes, parent_id=None),
|
||||
Image(id='emptyid', bytes=empty_bytes, parent_id='parentid', is_empty=True),
|
||||
Image(id='middleid', bytes=middle_bytes, parent_id='emptyid'),
|
||||
Image(id='emptyid2', bytes=empty_bytes, parent_id='middleid', is_empty=True),
|
||||
Image(id='someid', bytes=image_bytes, parent_id='emptyid2'),
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def jwk():
|
||||
return RSAKey(key=RSA.generate(2048))
|
||||
|
@ -161,10 +177,10 @@ def legacy_pusher(request, data_model, jwk):
|
|||
|
||||
@pytest.fixture(params=['v1', 'v2_1', 'v2_2'])
|
||||
def puller(request, data_model, jwk):
|
||||
if request == 'v1':
|
||||
if request.param == 'v1':
|
||||
return V1Protocol(jwk)
|
||||
|
||||
if request == 'v2_2' and data_model == 'oci_model':
|
||||
if request.param == 'v2_2' and data_model == 'oci_model':
|
||||
return V2Protocol(jwk, schema2=True)
|
||||
|
||||
return V2Protocol(jwk)
|
||||
|
|
|
@ -249,7 +249,21 @@ class V2Protocol(RegistryProtocol):
|
|||
if options.manifest_invalid_blob_references:
|
||||
checksum = 'sha256:' + hashlib.sha256('notarealthing').hexdigest()
|
||||
|
||||
builder.add_layer(checksum, len(image.bytes), urls=image.urls)
|
||||
if not image.is_empty:
|
||||
builder.add_layer(checksum, len(image.bytes), urls=image.urls)
|
||||
|
||||
def history_for_image(image):
|
||||
history = {
|
||||
'created': '2018-04-03T18:37:09.284840891Z',
|
||||
'created_by': (('/bin/sh -c #(nop) ENTRYPOINT %s' % image.config['Entrypoint'])
|
||||
if image.config and image.config.get('Entrypoint')
|
||||
else '/bin/sh -c #(nop) %s' % image.id),
|
||||
}
|
||||
|
||||
if image.is_empty:
|
||||
history['empty_layer'] = True
|
||||
|
||||
return history
|
||||
|
||||
config = {
|
||||
"os": "linux",
|
||||
|
@ -257,12 +271,7 @@ class V2Protocol(RegistryProtocol):
|
|||
"type": "layers",
|
||||
"diff_ids": []
|
||||
},
|
||||
"history": [{
|
||||
'created': '2018-04-03T18:37:09.284840891Z',
|
||||
'created_by': (('/bin/sh -c #(nop) ENTRYPOINT %s' % image.config['Entrypoint'])
|
||||
if image.config and image.config.get('Entrypoint')
|
||||
else '/bin/sh -c #(nop) %s' % image.id),
|
||||
} for image in images],
|
||||
"history": [history_for_image(image) for image in images],
|
||||
}
|
||||
|
||||
if images[-1].config:
|
||||
|
@ -535,17 +544,28 @@ class V2Protocol(RegistryProtocol):
|
|||
image_ids[tag_name] = manifest.leaf_layer_v1_image_id
|
||||
|
||||
# Verify the layers.
|
||||
for index, layer in enumerate(manifest.layers):
|
||||
layer_index = 0
|
||||
empty_count = 0
|
||||
for image in images:
|
||||
if manifest.schema_version == 2 and image.is_empty:
|
||||
empty_count += 1
|
||||
continue
|
||||
|
||||
# If the layer is remote, then we expect the blob to *not* exist in the system.
|
||||
expected_status = 404 if images[index].urls else 200
|
||||
layer = manifest.layers[layer_index]
|
||||
expected_status = 404 if image.urls else 200
|
||||
result = self.conduct(session, 'GET',
|
||||
'/v2/%s/blobs/%s' % (self.repo_name(namespace, repo_name),
|
||||
layer.digest),
|
||||
expected_status=expected_status,
|
||||
headers=headers)
|
||||
|
||||
|
||||
if expected_status == 200:
|
||||
assert result.content == images[index].bytes
|
||||
assert result.content == image.bytes
|
||||
|
||||
layer_index += 1
|
||||
|
||||
assert (len(manifest.layers) + empty_count) == len(images)
|
||||
|
||||
return PullResult(manifests=manifests, image_ids=image_ids)
|
||||
|
||||
|
|
|
@ -7,14 +7,20 @@ from cStringIO import StringIO
|
|||
from enum import Enum, unique
|
||||
from six import add_metaclass
|
||||
|
||||
Image = namedtuple('Image', ['id', 'parent_id', 'bytes', 'size', 'config', 'created', 'urls'])
|
||||
Image.__new__.__defaults__ = (None, None, None, None)
|
||||
from image.docker.schema2 import EMPTY_LAYER_BYTES
|
||||
|
||||
Image = namedtuple('Image', ['id', 'parent_id', 'bytes', 'size', 'config', 'created', 'urls',
|
||||
'is_empty'])
|
||||
Image.__new__.__defaults__ = (None, None, None, None, False)
|
||||
|
||||
PushResult = namedtuple('PushResult', ['manifests', 'headers'])
|
||||
PullResult = namedtuple('PullResult', ['manifests', 'image_ids'])
|
||||
|
||||
|
||||
def layer_bytes_for_contents(contents, mode='|gz', other_files=None):
|
||||
def layer_bytes_for_contents(contents, mode='|gz', other_files=None, empty=False):
|
||||
if empty:
|
||||
return EMPTY_LAYER_BYTES
|
||||
|
||||
layer_data = StringIO()
|
||||
tar_file = tarfile.open(fileobj=layer_data, mode='w' + mode)
|
||||
|
||||
|
|
|
@ -39,6 +39,19 @@ def test_basic_push_pull(pusher, puller, basic_images, liveserver_session, app_r
|
|||
credentials=credentials)
|
||||
|
||||
|
||||
def test_empty_layer(pusher, puller, images_with_empty_layer, liveserver_session, app_reloader):
|
||||
""" Test: Push and pull of an image with an empty layer to a new repository. """
|
||||
credentials = ('devtable', 'password')
|
||||
|
||||
# Push a new repository.
|
||||
pusher.push(liveserver_session, 'devtable', 'newrepo', 'latest', images_with_empty_layer,
|
||||
credentials=credentials)
|
||||
|
||||
# Pull the repository to verify.
|
||||
puller.pull(liveserver_session, 'devtable', 'newrepo', 'latest', images_with_empty_layer,
|
||||
credentials=credentials)
|
||||
|
||||
|
||||
def test_multi_layer_images_push_pull(pusher, puller, multi_layer_images, liveserver_session,
|
||||
app_reloader):
|
||||
""" Test: Basic push and pull of a multi-layered image to a new repository. """
|
||||
|
|
Reference in a new issue