Fix parent rewrite bug in schema1 manifest code and add a bunch more tests

Before this change, if you ended up writing a middle layer whose parent is not in the database, the manifest would fail to rewrite. We now just lookup the parent image in the manifest given to us, ignoring whether it is in the database or not (as it doesn't actually matter if not present; it'll be created if necessary).
This commit is contained in:
Joseph Schorr 2016-10-04 14:03:39 +03:00 committed by Evan Cordell
parent f3091c6424
commit f4e1748bb7
2 changed files with 191 additions and 30 deletions

View file

@ -259,13 +259,9 @@ class DockerSchema1Manifest(object):
updated_id_map[extracted_v1_metadata.image_id] = working_image_id updated_id_map[extracted_v1_metadata.image_id] = working_image_id
# Lookup the parent image for the layer, if any. # Lookup the parent image for the layer, if any.
parent_image_id = None parent_image_id = extracted_v1_metadata.parent_image_id
if extracted_v1_metadata.parent_image_id is not None: if parent_image_id is not None:
parent_image = images_map.get(extracted_v1_metadata.parent_image_id, None) parent_image_id = updated_id_map.get(parent_image_id, parent_image_id)
if parent_image is None:
raise MalformedSchema1Manifest('parent not found with image ID: %s' %
extracted_v1_metadata.parent_image_id)
parent_image_id = updated_id_map.get(parent_image.image_id, parent_image.image_id)
# Synthesize and store the v1 metadata in the db. # Synthesize and store the v1 metadata in the db.
v1_metadata_json = layer.raw_v1_metadata v1_metadata_json = layer.raw_v1_metadata
@ -285,7 +281,6 @@ class DockerSchema1Manifest(object):
content_checksum=digest_str, content_checksum=digest_str,
) )
images_map[updated_image.image_id] = updated_image
yield updated_image yield updated_image

View file

@ -38,7 +38,7 @@ from endpoints.csrf import generate_csrf_token
from endpoints.v1 import v1_bp from endpoints.v1 import v1_bp
from endpoints.v2 import v2_bp from endpoints.v2 import v2_bp
from endpoints.verbs import verbs from endpoints.verbs import verbs
from image.docker.schema1 import DockerSchema1ManifestBuilder from image.docker.schema1 import DockerSchema1ManifestBuilder, DockerSchema1Manifest
from initdb import wipe_database, initialize_database, populate_database from initdb import wipe_database, initialize_database, populate_database
from jsonschema import validate as validate_schema from jsonschema import validate as validate_schema
from util.security.registry_jwt import decode_bearer_header from util.security.registry_jwt import decode_bearer_header
@ -72,6 +72,14 @@ def set_fakestorage_directdownload(enabled):
return 'OK' return 'OK'
@testbp.route('/deleteimage/<image_id>', methods=['POST'])
def delete_image(image_id):
image = Image.get(docker_image_id=image_id)
image.docker_image_id = 'DELETED'
image.save()
return 'OK'
@testbp.route('/storagerepentry/<image_id>', methods=['GET']) @testbp.route('/storagerepentry/<image_id>', methods=['GET'])
def get_storage_replication_entry(image_id): def get_storage_replication_entry(image_id):
image = Image.get(docker_image_id=image_id) image = Image.get(docker_image_id=image_id)
@ -342,7 +350,7 @@ class V1RegistryPushMixin(V1RegistryMixin):
push_version = 'v1' push_version = 'v1'
def do_push(self, namespace, repository, username, password, images=None, expect_failure=None, def do_push(self, namespace, repository, username, password, images=None, expect_failure=None,
munge_shas=False, tag_names=None): munge_shas=[], tag_names=None, head_check=True):
images = images or self._get_default_images() images = images or self._get_default_images()
auth = (username, password) auth = (username, password)
repo_name = _get_repo_name(namespace, repository) repo_name = _get_repo_name(namespace, repository)
@ -399,7 +407,7 @@ class V1RegistryPullMixin(V1RegistryMixin):
pull_version = 'v1' pull_version = 'v1'
def do_pull(self, namespace, repository, username=None, password='password', expect_failure=None, def do_pull(self, namespace, repository, username=None, password='password', expect_failure=None,
images=None, munge_shas=False): images=None, munge_shas=[]):
images = images or self._get_default_images() images = images or self._get_default_images()
repo_name = _get_repo_name(namespace, repository) repo_name = _get_repo_name(namespace, repository)
@ -422,10 +430,11 @@ class V1RegistryPullMixin(V1RegistryMixin):
tags_result = json.loads(self.conduct('GET', prefix + 'tags', auth='sig').text) tags_result = json.loads(self.conduct('GET', prefix + 'tags', auth='sig').text)
self.assertEquals(1, len(tags_result.values())) self.assertEquals(1, len(tags_result.values()))
# Ensure we do (or do not) have a matching image ID.
tag_image_id = tags_result['latest'] tag_image_id = tags_result['latest']
known_ids = [item['id'] for item in images] if not munge_shas:
self.assertEquals(not munge_shas, tag_image_id in known_ids) # Ensure we have a matching image ID.
known_ids = [item['id'] for item in images]
self.assertTrue(tag_image_id in known_ids)
# Retrieve the ancestry of the tag image. # Retrieve the ancestry of the tag image.
image_prefix = '/v1/images/%s/' % tag_image_id image_prefix = '/v1/images/%s/' % tag_image_id
@ -527,7 +536,8 @@ class V2RegistryPushMixin(V2RegistryMixin):
push_version = 'v2' push_version = 'v2'
def do_push(self, namespace, repository, username, password, images=None, tag_names=None, def do_push(self, namespace, repository, username, password, images=None, tag_names=None,
cancel=False, invalid=False, expect_failure=None, scopes=None, munge_shas=False): cancel=False, invalid=False, expect_failure=None, scopes=None, munge_shas=[],
head_check=True):
images = images or self._get_default_images() images = images or self._get_default_images()
repo_name = _get_repo_name(namespace, repository) repo_name = _get_repo_name(namespace, repository)
@ -551,8 +561,9 @@ class V2RegistryPushMixin(V2RegistryMixin):
manifests = {} manifests = {}
full_contents = {} full_contents = {}
for image_data in reversed(images): for image_data in reversed(images):
full_contents[image_data['id']] = _get_full_contents(image_data, image_id = image_data['id']
additional_fields=munge_shas) full_contents[image_id] = _get_full_contents(image_data,
additional_fields=image_id in munge_shas)
# Build a fake manifest. # Build a fake manifest.
for tag_name in tag_names: for tag_name in tag_names:
@ -577,8 +588,10 @@ class V2RegistryPushMixin(V2RegistryMixin):
# Layer data should not yet exist. # Layer data should not yet exist.
checksum = 'sha256:' + hashlib.sha256(layer_bytes).hexdigest() checksum = 'sha256:' + hashlib.sha256(layer_bytes).hexdigest()
self.conduct('HEAD', '/v2/%s/blobs/%s' % (repo_name, checksum),
expected_code=404, auth='jwt') if head_check:
self.conduct('HEAD', '/v2/%s/blobs/%s' % (repo_name, checksum),
expected_code=404, auth='jwt')
# If we expected a non-404 status code, then the HEAD operation has failed and we cannot # If we expected a non-404 status code, then the HEAD operation has failed and we cannot
# continue performing the push. # continue performing the push.
@ -657,7 +670,7 @@ class V2RegistryPullMixin(V2RegistryMixin):
pull_version = 'v2' pull_version = 'v2'
def do_pull(self, namespace, repository, username=None, password='password', expect_failure=None, def do_pull(self, namespace, repository, username=None, password='password', expect_failure=None,
manifest_id=None, images=None, munge_shas=False): manifest_id=None, images=None, munge_shas=[]):
images = images or self._get_default_images() images = images or self._get_default_images()
repo_name = _get_repo_name(namespace, repository) repo_name = _get_repo_name(namespace, repository)
@ -709,7 +722,7 @@ class V2RegistryPullMixin(V2RegistryMixin):
for image in images: for image in images:
self.assertIn(image['id'], found_v1_layers) self.assertIn(image['id'], found_v1_layers)
return blobs return blobs, manifest_data
class V1RegistryLoginMixin(object): class V1RegistryLoginMixin(object):
@ -749,6 +762,120 @@ class V2RegistryLoginMixin(object):
class RegistryTestsMixin(object): class RegistryTestsMixin(object):
def test_middle_layer_different_sha(self):
if self.push_version == 'v1':
# No SHAs to munge in V1.
return
images = [
{
'id': 'rootid',
'contents': 'The root image',
},
{
'id': 'baseid',
'contents': 'The base image',
},
]
# Push a new repository with two layers.
self.do_push('public', 'newrepo', 'public', 'password', images=images)
# Pull the repository to verify.
self.do_pull('public', 'newrepo', 'public', 'password', images=images)
# Push again, munging the middle layer to ensure it gets assigned a different ID.
images = [
{
'id': 'rootid',
'contents': 'The root image',
},
{
'id': 'baseid',
'contents': 'The base image',
},
{
'id': 'latestid',
'contents': 'the latest image',
'parent': 'baseid',
},
]
munged_shas = ['baseid']
# Push the repository.
self.do_push('public', 'newrepo', 'public', 'password', images=images, munge_shas=munged_shas,
head_check=False)
# Pull the repository to verify.
self.do_pull('public', 'newrepo', 'public', 'password', images=images, munge_shas=munged_shas)
# Ensures we don't hit weird tag overwrite issues.
time.sleep(1)
# Delete the baseid image.
self.conduct('POST', '/__test/deleteimage/baseid')
images = [
{
'id': 'rootid',
'contents': 'The root image',
},
{
'id': 'baseid',
'contents': 'The base image',
},
{
'id': 'latestid',
'contents': 'the latest image',
'parent': 'baseid',
},
]
# Push the repository again, this time munging the root layer. Since the baseid does not exist
# anymore (since we deleted it above), this will have to look in the layer metadata itself
# to work (which didn't before).
munged_shas = ['rootid']
self.do_push('public', 'newrepo', 'public', 'password', images=images, munge_shas=munged_shas,
head_check=False)
# Pull the repository to verify.
self.do_pull('public', 'newrepo', 'public', 'password', images=images, munge_shas=munged_shas)
def test_push_same_ids_different_base_sha(self):
if self.push_version == 'v1':
# No SHAs to munge in V1.
return
images = [
{
'id': 'baseid',
'contents': 'The base image',
},
{
'id': 'latestid',
'contents': 'the latest image',
'parent': 'baseid',
},
]
munged_shas = ['baseid']
# Push a new repository.
self.do_push('public', 'newrepo', 'public', 'password', images=images)
# Pull the repository.
self.do_pull('public', 'newrepo', 'public', 'password', images=images)
# Push a the repository again, but with different SHAs.
self.do_push('public', 'newrepo', 'public', 'password', images=images, munge_shas=munged_shas,
head_check=False)
# Pull the repository.
self.do_pull('public', 'newrepo', 'public', 'password', images=images, munge_shas=munged_shas)
def test_push_same_ids_different_sha(self): def test_push_same_ids_different_sha(self):
if self.push_version == 'v1': if self.push_version == 'v1':
# No SHAs to munge in V1. # No SHAs to munge in V1.
@ -766,6 +893,8 @@ class RegistryTestsMixin(object):
}, },
] ]
munged_shas = ['latestid']
# Push a new repository. # Push a new repository.
self.do_push('public', 'newrepo', 'public', 'password', images=images) self.do_push('public', 'newrepo', 'public', 'password', images=images)
@ -773,10 +902,44 @@ class RegistryTestsMixin(object):
self.do_pull('public', 'newrepo', 'public', 'password', images=images) self.do_pull('public', 'newrepo', 'public', 'password', images=images)
# Push a the repository again, but with different SHAs. # Push a the repository again, but with different SHAs.
self.do_push('public', 'newrepo', 'public', 'password', images=images, munge_shas=True) self.do_push('public', 'newrepo', 'public', 'password', images=images, munge_shas=munged_shas,
head_check=False)
# Pull the repository. # Pull the repository.
self.do_pull('public', 'newrepo', 'public', 'password', images=images, munge_shas=True) self.do_pull('public', 'newrepo', 'public', 'password', images=images, munge_shas=munged_shas)
def test_push_same_ids_different_sha_both_layers(self):
if self.push_version == 'v1':
# No SHAs to munge in V1.
return
images = [
{
'id': 'baseid',
'contents': 'The base image',
},
{
'id': 'latestid',
'contents': 'the latest image',
'parent': 'baseid',
},
]
munged_shas = ['baseid', 'latestid']
# Push a new repository.
self.do_push('public', 'newrepo', 'public', 'password', images=images)
# Pull the repository.
self.do_pull('public', 'newrepo', 'public', 'password', images=images)
# Push a the repository again, but with different SHAs.
self.do_push('public', 'newrepo', 'public', 'password', images=images, munge_shas=munged_shas,
head_check=False)
# Pull the repository.
self.do_pull('public', 'newrepo', 'public', 'password', images=images, munge_shas=munged_shas)
def test_push_same_ids_different_sha_with_unicode(self): def test_push_same_ids_different_sha_with_unicode(self):
@ -797,6 +960,8 @@ class RegistryTestsMixin(object):
}, },
] ]
munged_shas = ['latestid', 'baseid']
# Push a new repository. # Push a new repository.
self.do_push('public', 'newrepo', 'public', 'password', images=images) self.do_push('public', 'newrepo', 'public', 'password', images=images)
@ -804,10 +969,11 @@ class RegistryTestsMixin(object):
self.do_pull('public', 'newrepo', 'public', 'password', images=images) self.do_pull('public', 'newrepo', 'public', 'password', images=images)
# Push a the repository again, but with different SHAs. # Push a the repository again, but with different SHAs.
self.do_push('public', 'newrepo', 'public', 'password', images=images, munge_shas=True) self.do_push('public', 'newrepo', 'public', 'password', images=images, munge_shas=munged_shas,
head_check=False)
# Pull the repository. # Pull the repository.
self.do_pull('public', 'newrepo', 'public', 'password', images=images, munge_shas=True) self.do_pull('public', 'newrepo', 'public', 'password', images=images, munge_shas=munged_shas)
def test_push_pull_logging(self): def test_push_pull_logging(self):
@ -1363,7 +1529,7 @@ class V2RegistryTests(V2RegistryPullMixin, V2RegistryPushMixin, RegistryTestsMix
self.do_push('devtable', 'newrepo', 'devtable', 'password', images=images) self.do_push('devtable', 'newrepo', 'devtable', 'password', images=images)
# Pull the image back and verify the contents. # Pull the image back and verify the contents.
blobs = self.do_pull('devtable', 'newrepo', 'devtable', 'password', images=images) blobs, _ = self.do_pull('devtable', 'newrepo', 'devtable', 'password', images=images)
self.assertEquals(len(blobs.items()), 1) self.assertEquals(len(blobs.items()), 1)
self.assertEquals(blobs.items()[0][1], contents) self.assertEquals(blobs.items()[0][1], contents)
@ -1384,7 +1550,7 @@ class V2RegistryTests(V2RegistryPullMixin, V2RegistryPushMixin, RegistryTestsMix
self.do_push('devtable', 'newrepo', 'devtable', 'password', images=images) self.do_push('devtable', 'newrepo', 'devtable', 'password', images=images)
# Pull the image back and verify the contents. # Pull the image back and verify the contents.
blobs = self.do_pull('devtable', 'newrepo', 'devtable', 'password', images=images) blobs, _ = self.do_pull('devtable', 'newrepo', 'devtable', 'password', images=images)
self.assertEquals(len(blobs.items()), 1) self.assertEquals(len(blobs.items()), 1)
self.assertEquals(blobs.items()[0][1], contents) self.assertEquals(blobs.items()[0][1], contents)
@ -1406,7 +1572,7 @@ class V2RegistryTests(V2RegistryPullMixin, V2RegistryPushMixin, RegistryTestsMix
self.do_push('devtable', 'newrepo', 'devtable', 'password', images=images) self.do_push('devtable', 'newrepo', 'devtable', 'password', images=images)
# Pull the image back and verify the contents. # Pull the image back and verify the contents.
blobs = self.do_pull('devtable', 'newrepo', 'devtable', 'password', images=images) blobs, _ = self.do_pull('devtable', 'newrepo', 'devtable', 'password', images=images)
self.assertEquals(len(blobs.items()), 1) self.assertEquals(len(blobs.items()), 1)
self.assertEquals(blobs.items()[0][1], contents) self.assertEquals(blobs.items()[0][1], contents)
@ -1583,8 +1749,8 @@ class TorrentTestMixin(V2RegistryPullMixin):
self.do_push('devtable', 'newrepo', 'devtable', 'password', images=initial_images) self.do_push('devtable', 'newrepo', 'devtable', 'password', images=initial_images)
# Retrieve the manifest for the tag. # Retrieve the manifest for the tag.
blobs = self.do_pull('devtable', 'newrepo', 'devtable', 'password', manifest_id='latest', blobs, _ = self.do_pull('devtable', 'newrepo', 'devtable', 'password', manifest_id='latest',
images=initial_images) images=initial_images)
self.assertEquals(1, len(list(blobs.keys()))) self.assertEquals(1, len(list(blobs.keys())))
blobsum = list(blobs.keys())[0] blobsum = list(blobs.keys())[0]