Support pulling of schema2 manifests directly via a manifest list tag

This change ensures that if a manifest list is requested with an accepts header for a *schema 2* manifest, the legacy manifest (if any) is returned as schema 2 if it was pushed as a schema 2 manifest (rather than being auto-converted to schema 1)
This commit is contained in:
Joseph Schorr 2018-12-06 12:40:34 -05:00
parent a35982f2be
commit 3c2e050593
14 changed files with 215 additions and 15 deletions

View file

@ -115,6 +115,13 @@ class ManifestInterface(object):
If none, returns None.
"""
@abstractmethod
def convert_manifest(self, allowed_mediatypes, namespace_name, repo_name, tag_name,
content_retriever):
""" Returns a version of this schema that has a media type found in the given media type set.
If not possible, or an error occurs, returns None.
"""
@add_metaclass(ABCMeta)
class ContentRetriever(object):

View file

@ -419,6 +419,17 @@ class DockerSchema1Manifest(ManifestInterface):
# used, so to ensure full backwards compatibility, we just always return the schema.
return self
def convert_manifest(self, allowed_mediatypes, namespace_name, repo_name, tag_name,
content_retriever):
if self.media_type in allowed_mediatypes:
return self
unsigned = self.unsigned()
if unsigned.media_type in allowed_mediatypes:
return unsigned
return None
def rewrite_invalid_image_ids(self, images_map):
"""
Rewrites Docker v1 image IDs and returns a generator of DockerV1Metadata.

View file

@ -263,6 +263,29 @@ class DockerSchema2ManifestList(ManifestInterface):
""" Returns the manifest that is compatible with V1, by virtue of being `amd64` and `linux`.
If none, returns None.
"""
legacy_manifest = self._get_legacy_manifest(content_retriever)
if legacy_manifest is None:
return None
return legacy_manifest.get_schema1_manifest(namespace_name, repo_name, tag_name,
content_retriever)
def convert_manifest(self, allowed_mediatypes, namespace_name, repo_name, tag_name,
content_retriever):
if self.media_type in allowed_mediatypes:
return self
legacy_manifest = self._get_legacy_manifest(content_retriever)
if legacy_manifest is None:
return None
return legacy_manifest.convert_manifest(allowed_mediatypes, namespace_name, repo_name,
tag_name, content_retriever)
def _get_legacy_manifest(self, content_retriever):
""" Returns the manifest under this list with architecture amd64 and os linux, if any, or None
if none or error.
"""
for manifest_ref in self.manifests(content_retriever):
platform = manifest_ref._manifest_data[DOCKER_SCHEMA2_MANIFESTLIST_PLATFORM_KEY]
architecture = platform[DOCKER_SCHEMA2_MANIFESTLIST_ARCHITECTURE_KEY]
@ -271,13 +294,11 @@ class DockerSchema2ManifestList(ManifestInterface):
continue
try:
manifest = manifest_ref.manifest_obj
return manifest_ref.manifest_obj
except (ManifestException, IOError):
logger.exception('Could not load child manifest')
return None
return manifest.get_schema1_manifest(namespace_name, repo_name, tag_name, content_retriever)
return None
def unsigned(self):

View file

@ -299,6 +299,19 @@ class DockerSchema2Manifest(ManifestInterface):
return [l.v1_id for l in self._manifest_image_layers(content_retriever)]
def convert_manifest(self, allowed_mediatypes, namespace_name, repo_name, tag_name,
content_retriever):
if self.media_type in allowed_mediatypes:
return self
# If this manifest is not on the allowed list, try to convert the schema 1 version (if any)
schema1 = self.get_schema1_manifest(namespace_name, repo_name, tag_name, content_retriever)
if schema1 is None:
return None
return schema1.convert_manifest(allowed_mediatypes, namespace_name, repo_name, tag_name,
content_retriever)
def get_schema1_manifest(self, namespace_name, repo_name, tag_name, content_retriever):
if self.has_remote_layer:
return None

View file

@ -3,7 +3,7 @@ import json
import pytest
from image.docker.schema1 import DockerSchema1Manifest
from image.docker.schema1 import DockerSchema1Manifest, DOCKER_SCHEMA1_CONTENT_TYPES
from image.docker.schema2.manifest import DockerSchema2Manifest
from image.docker.schemautil import ContentRetrieverForTesting
@ -53,6 +53,36 @@ def test_conversion(name, config_sha):
schema2 = DockerSchema2Manifest(_get_test_file_contents(name, 'schema2'))
schema1 = DockerSchema1Manifest(_get_test_file_contents(name, 'schema1'), validate=False)
s2to2 = schema2.convert_manifest([schema2.media_type], 'devtable', 'somerepo', 'latest',
retriever)
assert s2to2 == schema2
s1to1 = schema1.convert_manifest([schema1.media_type], 'devtable', 'somerepo', 'latest',
retriever)
assert s1to1 == schema1
s2to1 = schema2.convert_manifest(DOCKER_SCHEMA1_CONTENT_TYPES, 'devtable', 'somerepo', 'latest',
retriever)
assert s2to1.media_type in DOCKER_SCHEMA1_CONTENT_TYPES
assert len(s2to1.layers) == len(schema1.layers)
s2toempty = schema2.convert_manifest([], 'devtable', 'somerepo', 'latest', retriever)
assert s2toempty is None
@pytest.mark.parametrize('name, config_sha', [
('simple', 'sha256:e7a06c2e5b7afb1bbfa9124812e87f1138c4c10d77e0a217f0b8c8c9694dc5cf'),
('complex', 'sha256:ae6b78bedf88330a5e5392164f40d28ed8a38120b142905d30b652ebffece10e'),
('ubuntu', 'sha256:93fd78260bd1495afb484371928661f63e64be306b7ac48e2d13ce9422dfee26'),
])
def test_2to1_conversion(name, config_sha):
cr = {}
cr[config_sha] = _get_test_file_contents(name, 'config')
retriever = ContentRetrieverForTesting(cr)
schema2 = DockerSchema2Manifest(_get_test_file_contents(name, 'schema2'))
schema1 = DockerSchema1Manifest(_get_test_file_contents(name, 'schema1'), validate=False)
converted = schema2.get_schema1_manifest('devtable', 'somerepo', 'latest', retriever)
assert len(converted.layers) == len(schema1.layers)

View file

@ -1,7 +1,8 @@
import json
import pytest
from image.docker.schema1 import DockerSchema1Manifest
from image.docker.schema1 import DockerSchema1Manifest, DOCKER_SCHEMA1_CONTENT_TYPES
from image.docker.schema2 import DOCKER_SCHEMA2_MANIFEST_CONTENT_TYPE
from image.docker.schema2.manifest import DockerSchema2Manifest
from image.docker.schema2.list import (MalformedSchema2ManifestList, DockerSchema2ManifestList,
DockerSchema2ManifestListBuilder)
@ -90,9 +91,21 @@ def test_valid_manifestlist():
assert isinstance(manifest.manifest_obj, DockerSchema1Manifest)
assert manifest.manifest_obj.schema_version == 1
# Check retrieval of a schema 2 manifest. This should return None, because the schema 2 manifest
# is not amd64-compatible.
schema2_manifest = manifestlist.convert_manifest([DOCKER_SCHEMA2_MANIFEST_CONTENT_TYPE], 'foo',
'bar', 'baz', retriever)
assert schema2_manifest is None
# Check retrieval of a schema 1 manifest.
compatible_manifest = manifestlist.get_schema1_manifest('foo', 'bar', 'baz', retriever)
assert compatible_manifest.schema_version == 1
schema1_manifest = manifestlist.convert_manifest(DOCKER_SCHEMA1_CONTENT_TYPES, 'foo',
'bar', 'baz', retriever)
assert schema1_manifest.schema_version == 1
assert schema1_manifest.digest == compatible_manifest.digest
def test_get_schema1_manifest_no_matching_list():
manifestlist = DockerSchema2ManifestList(NO_AMD_MANIFESTLIST_BYTES)

View file

@ -278,6 +278,10 @@ def test_get_schema1_manifest():
assert schema1 is not None
assert schema1.media_type == DOCKER_SCHEMA1_MANIFEST_CONTENT_TYPE
via_convert = manifest.convert_manifest([schema1.media_type], 'somenamespace', 'somename',
'sometag', retriever)
assert via_convert.digest == schema1.digest
def test_generate_legacy_layers():
builder = DockerSchema2ManifestBuilder()