Add method for retrieving a V1-compatible manifest for any manifest or manifest list
This is used to serve older clients that don't support the V2 schema format
This commit is contained in:
parent
d77d383e46
commit
1b3daac3c3
7 changed files with 119 additions and 7 deletions
|
@ -74,3 +74,9 @@ class ManifestInterface(object):
|
||||||
|
|
||||||
Returns None if there are no legacy images associated with the manifest.
|
Returns None if there are no legacy images associated with the manifest.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_v1_compatible_manifest(self, namespace_name, repo_name, tag_name, lookup_fn):
|
||||||
|
""" Returns the manifest that is compatible with V1, by virtue of being `amd64` and `linux`.
|
||||||
|
If none, returns None.
|
||||||
|
"""
|
||||||
|
|
|
@ -365,6 +365,14 @@ class DockerSchema1Manifest(ManifestInterface):
|
||||||
def generate_legacy_layers(self, images_map, lookup_config_fn):
|
def generate_legacy_layers(self, images_map, lookup_config_fn):
|
||||||
return self.rewrite_invalid_image_ids(images_map)
|
return self.rewrite_invalid_image_ids(images_map)
|
||||||
|
|
||||||
|
def get_v1_compatible_manifest(self, namespace_name, repo_name, tag_name, lookup_fn):
|
||||||
|
""" Returns the manifest that is compatible with V1, by virtue of being `amd64` and `linux`.
|
||||||
|
If none, returns None.
|
||||||
|
"""
|
||||||
|
# Note: schema1 *technically* supports non-amd64 architectures, but in practice these were never
|
||||||
|
# used, so to ensure full backwards compatibility, we just always return the schema.
|
||||||
|
return self
|
||||||
|
|
||||||
def rewrite_invalid_image_ids(self, images_map):
|
def rewrite_invalid_image_ids(self, images_map):
|
||||||
"""
|
"""
|
||||||
Rewrites Docker v1 image IDs and returns a generator of DockerV1Metadata.
|
Rewrites Docker v1 image IDs and returns a generator of DockerV1Metadata.
|
||||||
|
|
|
@ -213,6 +213,7 @@ class DockerSchema2Config(object):
|
||||||
layer being last.
|
layer being last.
|
||||||
"""
|
"""
|
||||||
history = list(self.history)
|
history = list(self.history)
|
||||||
|
assert layer_index < len(history)
|
||||||
|
|
||||||
# If the layer is the leaf, it gets the full config (minus 2 fields). Otherwise, it gets only
|
# If the layer is the leaf, it gets the full config (minus 2 fields). Otherwise, it gets only
|
||||||
# IDs.
|
# IDs.
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
|
import logging
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from cachetools import lru_cache
|
from cachetools import lru_cache
|
||||||
from jsonschema import validate as validate_schema, ValidationError
|
from jsonschema import validate as validate_schema, ValidationError
|
||||||
|
|
||||||
from digest import digest_tools
|
from digest import digest_tools
|
||||||
|
from image.docker import ManifestException
|
||||||
from image.docker.interfaces import ManifestInterface
|
from image.docker.interfaces import ManifestInterface
|
||||||
from image.docker.schema1 import DOCKER_SCHEMA1_MANIFEST_CONTENT_TYPE
|
from image.docker.schema1 import DOCKER_SCHEMA1_MANIFEST_CONTENT_TYPE
|
||||||
from image.docker.schema1 import DockerSchema1Manifest
|
from image.docker.schema1 import DockerSchema1Manifest
|
||||||
|
@ -11,6 +13,9 @@ from image.docker.schema2 import (DOCKER_SCHEMA2_MANIFESTLIST_CONTENT_TYPE,
|
||||||
DOCKER_SCHEMA2_MANIFEST_CONTENT_TYPE)
|
DOCKER_SCHEMA2_MANIFEST_CONTENT_TYPE)
|
||||||
from image.docker.schema2.manifest import DockerSchema2Manifest
|
from image.docker.schema2.manifest import DockerSchema2Manifest
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Keys.
|
# Keys.
|
||||||
DOCKER_SCHEMA2_MANIFESTLIST_VERSION_KEY = 'schemaVersion'
|
DOCKER_SCHEMA2_MANIFESTLIST_VERSION_KEY = 'schemaVersion'
|
||||||
DOCKER_SCHEMA2_MANIFESTLIST_MEDIATYPE_KEY = 'mediaType'
|
DOCKER_SCHEMA2_MANIFESTLIST_MEDIATYPE_KEY = 'mediaType'
|
||||||
|
@ -228,16 +233,28 @@ class DockerSchema2ManifestList(ManifestInterface):
|
||||||
def get_manifest_labels(self, lookup_config_fn):
|
def get_manifest_labels(self, lookup_config_fn):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_v1_compatible_manifest(self, lookup_manifest_fn):
|
def get_v1_compatible_manifest(self, namespace_name, repo_name, tag_name, lookup_fn):
|
||||||
""" Returns the manifest that is compatible with V1, by virtue of being `amd64` and `linux`.
|
""" Returns the manifest that is compatible with V1, by virtue of being `amd64` and `linux`.
|
||||||
If none, returns None.
|
If none, returns None.
|
||||||
"""
|
"""
|
||||||
for manifest in self.manifests(lookup_manifest_fn):
|
for manifest_ref in self.manifests(lookup_fn):
|
||||||
platform = manifest._manifest_data[DOCKER_SCHEMA2_MANIFESTLIST_PLATFORM_KEY]
|
platform = manifest_ref._manifest_data[DOCKER_SCHEMA2_MANIFESTLIST_PLATFORM_KEY]
|
||||||
architecture = platform[DOCKER_SCHEMA2_MANIFESTLIST_ARCHITECTURE_KEY]
|
architecture = platform[DOCKER_SCHEMA2_MANIFESTLIST_ARCHITECTURE_KEY]
|
||||||
os = platform[DOCKER_SCHEMA2_MANIFESTLIST_OS_KEY]
|
os = platform[DOCKER_SCHEMA2_MANIFESTLIST_OS_KEY]
|
||||||
if architecture == 'amd64' and os == 'linux':
|
|
||||||
return manifest
|
if architecture != 'amd64' or os != 'linux':
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
manifest = manifest_ref.manifest_obj
|
||||||
|
except ManifestException:
|
||||||
|
logger.exception('Could not load child manifest')
|
||||||
|
return None
|
||||||
|
except IOError:
|
||||||
|
logger.exception('Could not load child manifest')
|
||||||
|
return None
|
||||||
|
|
||||||
|
return manifest.get_v1_compatible_manifest(namespace_name, repo_name, tag_name, lookup_fn)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
|
@ -260,6 +260,11 @@ class DockerSchema2Manifest(ManifestInterface):
|
||||||
self.populate_schema1_builder(v1_builder, lookup_config_fn)
|
self.populate_schema1_builder(v1_builder, lookup_config_fn)
|
||||||
return v1_builder.build().generate_legacy_layers(images_map, lookup_config_fn)
|
return v1_builder.build().generate_legacy_layers(images_map, lookup_config_fn)
|
||||||
|
|
||||||
|
def get_v1_compatible_manifest(self, namespace_name, repo_name, tag_name, lookup_fn):
|
||||||
|
v1_builder = DockerSchema1ManifestBuilder(namespace_name, repo_name, tag_name)
|
||||||
|
self.populate_schema1_builder(v1_builder, lookup_fn)
|
||||||
|
return v1_builder.build()
|
||||||
|
|
||||||
def unsigned(self):
|
def unsigned(self):
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
|
@ -50,6 +50,22 @@ MANIFESTLIST_BYTES = json.dumps({
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
NO_AMD_MANIFESTLIST_BYTES = json.dumps({
|
||||||
|
"schemaVersion": 2,
|
||||||
|
"mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
|
||||||
|
"manifests": [
|
||||||
|
{
|
||||||
|
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||||
|
"size": 983,
|
||||||
|
"digest": "sha256:e6",
|
||||||
|
"platform": {
|
||||||
|
"architecture": "ppc64le",
|
||||||
|
"os": "linux",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
def test_valid_manifestlist():
|
def test_valid_manifestlist():
|
||||||
def _get_manifest(digest):
|
def _get_manifest(digest):
|
||||||
if digest == 'sha256:e6':
|
if digest == 'sha256:e6':
|
||||||
|
@ -75,13 +91,33 @@ def test_valid_manifestlist():
|
||||||
assert isinstance(manifest.manifest_obj, DockerSchema1Manifest)
|
assert isinstance(manifest.manifest_obj, DockerSchema1Manifest)
|
||||||
assert manifest.manifest_obj.schema_version == 1
|
assert manifest.manifest_obj.schema_version == 1
|
||||||
|
|
||||||
assert manifestlist.get_v1_compatible_manifest(_get_manifest).manifest_obj.schema_version == 1
|
compatible_manifest = manifestlist.get_v1_compatible_manifest('foo', 'bar', 'baz', _get_manifest)
|
||||||
|
assert compatible_manifest.schema_version == 1
|
||||||
|
|
||||||
assert manifestlist.layers is None
|
assert manifestlist.layers is None
|
||||||
assert manifestlist.leaf_layer_v1_image_id is None
|
assert manifestlist.leaf_layer_v1_image_id is None
|
||||||
assert manifestlist.legacy_image_ids is None
|
assert manifestlist.legacy_image_ids is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_v1_compatible_manifest_no_matching_list():
|
||||||
|
def _get_manifest(digest):
|
||||||
|
if digest == 'sha256:e6':
|
||||||
|
return v22_bytes
|
||||||
|
else:
|
||||||
|
return v21_bytes
|
||||||
|
|
||||||
|
manifestlist = DockerSchema2ManifestList(NO_AMD_MANIFESTLIST_BYTES)
|
||||||
|
assert len(manifestlist.manifests(_get_manifest)) == 1
|
||||||
|
assert (manifestlist.digest ==
|
||||||
|
'sha256:50150251101420a020ab4a3e77e9d167a18b09bd4eeb0cc65e0eafab95cf79cf')
|
||||||
|
|
||||||
|
assert manifestlist.media_type == 'application/vnd.docker.distribution.manifest.list.v2+json'
|
||||||
|
assert manifestlist.bytes == NO_AMD_MANIFESTLIST_BYTES
|
||||||
|
|
||||||
|
compatible_manifest = manifestlist.get_v1_compatible_manifest('foo', 'bar', 'baz', _get_manifest)
|
||||||
|
assert compatible_manifest is None
|
||||||
|
|
||||||
|
|
||||||
def test_builder():
|
def test_builder():
|
||||||
def _get_manifest(digest):
|
def _get_manifest(digest):
|
||||||
if digest == 'sha256:e6':
|
if digest == 'sha256:e6':
|
||||||
|
|
|
@ -3,7 +3,8 @@ import pytest
|
||||||
|
|
||||||
from app import docker_v2_signing_key
|
from app import docker_v2_signing_key
|
||||||
from image.docker.schema1 import (DockerSchema1ManifestBuilder,
|
from image.docker.schema1 import (DockerSchema1ManifestBuilder,
|
||||||
DOCKER_SCHEMA1_SIGNED_MANIFEST_CONTENT_TYPE)
|
DOCKER_SCHEMA1_SIGNED_MANIFEST_CONTENT_TYPE,
|
||||||
|
DOCKER_SCHEMA1_MANIFEST_CONTENT_TYPE)
|
||||||
from image.docker.schema2.manifest import (MalformedSchema2Manifest, DockerSchema2Manifest,
|
from image.docker.schema2.manifest import (MalformedSchema2Manifest, DockerSchema2Manifest,
|
||||||
DockerSchema2ManifestBuilder)
|
DockerSchema2ManifestBuilder)
|
||||||
from image.docker.schema2.test.test_config import CONFIG_BYTES
|
from image.docker.schema2.test.test_config import CONFIG_BYTES
|
||||||
|
@ -132,6 +133,44 @@ def test_build_schema1():
|
||||||
assert digest == str(list(manifest.blob_digests)[index])
|
assert digest == str(list(manifest.blob_digests)[index])
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_v1_compatible_manifest():
|
||||||
|
def _get_config(digest):
|
||||||
|
config_str = json.dumps({
|
||||||
|
"config": {
|
||||||
|
"Labels": {},
|
||||||
|
},
|
||||||
|
"rootfs": {"type": "layers", "diff_ids": []},
|
||||||
|
"history": [
|
||||||
|
{
|
||||||
|
"created": "2018-04-03T18:37:09.284840891Z",
|
||||||
|
"created_by": "foo"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"created": "2018-04-12T18:37:09.284840891Z",
|
||||||
|
"created_by": "bar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"created": "2018-04-03T18:37:09.284840891Z",
|
||||||
|
"created_by": "foo"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"created": "2018-04-12T18:37:09.284840891Z",
|
||||||
|
"created_by": "bar"
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
return config_str + ' ' * (1885 - len(config_str))
|
||||||
|
|
||||||
|
manifest = DockerSchema2Manifest(MANIFEST_BYTES)
|
||||||
|
schema1 = manifest.get_v1_compatible_manifest('somenamespace', 'somename', 'sometag', _get_config)
|
||||||
|
assert schema1 is not None
|
||||||
|
assert schema1.media_type == DOCKER_SCHEMA1_MANIFEST_CONTENT_TYPE
|
||||||
|
assert len(schema1.layers) == len(manifest.layers)
|
||||||
|
assert set(schema1.image_ids) == set([l.v1_id for l in manifest.layers_with_v1_ids])
|
||||||
|
assert set(schema1.parent_image_ids) == set([l.v1_parent_id for l in
|
||||||
|
manifest.layers_with_v1_ids if l.v1_parent_id])
|
||||||
|
|
||||||
|
|
||||||
def test_generate_legacy_layers():
|
def test_generate_legacy_layers():
|
||||||
builder = DockerSchema2ManifestBuilder()
|
builder = DockerSchema2ManifestBuilder()
|
||||||
builder.add_layer('sha256:abc123', 123)
|
builder.add_layer('sha256:abc123', 123)
|
||||||
|
|
Reference in a new issue