Fix pulling of squashed versions of the legacy image in a manifest lists
This commit is contained in:
parent
001768c043
commit
1f03fdb27e
10 changed files with 198 additions and 31 deletions
|
@ -197,6 +197,13 @@ class RegistryDataInterface(object):
|
||||||
Returns None if the manifest could not be parsed and validated.
|
Returns None if the manifest could not be parsed and validated.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def list_parsed_manifest_layers(self, repository_ref, parsed_manifest, include_placements=False):
|
||||||
|
""" Returns an *ordered list* of the layers found in the parsed manifest, starting at the base
|
||||||
|
and working towards the leaf, including the associated Blob and its placements
|
||||||
|
(if specified).
|
||||||
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def lookup_derived_image(self, manifest, verb, varying_metadata=None, include_placements=False):
|
def lookup_derived_image(self, manifest, verb, varying_metadata=None, include_placements=False):
|
||||||
"""
|
"""
|
||||||
|
@ -205,8 +212,8 @@ class RegistryDataInterface(object):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def lookup_or_create_derived_image(self, manifest, verb, storage_location, varying_metadata=None,
|
def lookup_or_create_derived_image(self, manifest, verb, storage_location, storage,
|
||||||
include_placements=False):
|
varying_metadata=None, include_placements=False):
|
||||||
"""
|
"""
|
||||||
Looks up the derived image for the given maniest, verb and optional varying metadata
|
Looks up the derived image for the given maniest, verb and optional varying metadata
|
||||||
and returns it. If none exists, a new derived image is created.
|
and returns it. If none exists, a new derived image is created.
|
||||||
|
|
|
@ -54,6 +54,30 @@ class OCIModel(SharedModel, RegistryDataInterface):
|
||||||
|
|
||||||
return tags_map
|
return tags_map
|
||||||
|
|
||||||
|
def _get_legacy_compatible_image_for_manifest(self, manifest, storage):
|
||||||
|
# Check for a legacy image directly on the manifest.
|
||||||
|
if manifest.media_type != DOCKER_SCHEMA2_MANIFESTLIST_CONTENT_TYPE:
|
||||||
|
return oci.shared.get_legacy_image_for_manifest(manifest._db_id)
|
||||||
|
|
||||||
|
# Otherwise, lookup a legacy image associated with the v1-compatible manifest
|
||||||
|
# in the list.
|
||||||
|
try:
|
||||||
|
manifest_obj = database.Manifest.get(id=manifest._db_id)
|
||||||
|
except database.Manifest.DoesNotExist:
|
||||||
|
logger.exception('Could not find manifest for manifest `%s`', manifest._db_id)
|
||||||
|
return None
|
||||||
|
|
||||||
|
# See if we can lookup a schema1 legacy image.
|
||||||
|
v1_compatible = self.get_schema1_parsed_manifest(manifest, '', '', '', storage)
|
||||||
|
if v1_compatible is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
v1_id = v1_compatible.leaf_layer_v1_image_id
|
||||||
|
if v1_id is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return model.image.get_image(manifest_obj.repository_id, v1_id)
|
||||||
|
|
||||||
def find_matching_tag(self, repository_ref, tag_names):
|
def find_matching_tag(self, repository_ref, tag_names):
|
||||||
""" Finds an alive tag in the repository matching one of the given tag names and returns it
|
""" Finds an alive tag in the repository matching one of the given tag names and returns it
|
||||||
or None if none.
|
or None if none.
|
||||||
|
@ -400,7 +424,13 @@ class OCIModel(SharedModel, RegistryDataInterface):
|
||||||
logger.exception('Could not find manifest for manifest `%s`', manifest._db_id)
|
logger.exception('Could not find manifest for manifest `%s`', manifest._db_id)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return self._list_manifest_layers(manifest, manifest_obj.repository_id, include_placements)
|
try:
|
||||||
|
parsed = manifest.get_parsed_manifest()
|
||||||
|
except ManifestException:
|
||||||
|
logger.exception('Could not parse and validate manifest `%s`', manifest._db_id)
|
||||||
|
return None
|
||||||
|
|
||||||
|
return self._list_manifest_layers(manifest_obj.repository_id, parsed, include_placements)
|
||||||
|
|
||||||
def lookup_derived_image(self, manifest, verb, varying_metadata=None, include_placements=False):
|
def lookup_derived_image(self, manifest, verb, varying_metadata=None, include_placements=False):
|
||||||
"""
|
"""
|
||||||
|
@ -414,13 +444,14 @@ class OCIModel(SharedModel, RegistryDataInterface):
|
||||||
derived = model.image.find_derived_storage_for_image(legacy_image, verb, varying_metadata)
|
derived = model.image.find_derived_storage_for_image(legacy_image, verb, varying_metadata)
|
||||||
return self._build_derived(derived, verb, varying_metadata, include_placements)
|
return self._build_derived(derived, verb, varying_metadata, include_placements)
|
||||||
|
|
||||||
def lookup_or_create_derived_image(self, manifest, verb, storage_location, varying_metadata=None,
|
def lookup_or_create_derived_image(self, manifest, verb, storage_location, storage,
|
||||||
|
varying_metadata=None,
|
||||||
include_placements=False):
|
include_placements=False):
|
||||||
"""
|
"""
|
||||||
Looks up the derived image for the given maniest, verb and optional varying metadata
|
Looks up the derived image for the given maniest, verb and optional varying metadata
|
||||||
and returns it. If none exists, a new derived image is created.
|
and returns it. If none exists, a new derived image is created.
|
||||||
"""
|
"""
|
||||||
legacy_image = oci.shared.get_legacy_image_for_manifest(manifest._db_id)
|
legacy_image = self._get_legacy_compatible_image_for_manifest(manifest, storage)
|
||||||
if legacy_image is None:
|
if legacy_image is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,8 @@ from data import model
|
||||||
from data.database import db_transaction
|
from data.database import db_transaction
|
||||||
from data.registry_model.interface import RegistryDataInterface
|
from data.registry_model.interface import RegistryDataInterface
|
||||||
from data.registry_model.datatypes import (Tag, Manifest, LegacyImage, Label,
|
from data.registry_model.datatypes import (Tag, Manifest, LegacyImage, Label,
|
||||||
SecurityScanStatus, ManifestLayer, Blob, DerivedImage)
|
SecurityScanStatus, ManifestLayer, Blob, DerivedImage,
|
||||||
|
RepositoryReference)
|
||||||
from data.registry_model.shared import SharedModel
|
from data.registry_model.shared import SharedModel
|
||||||
from data.registry_model.label_handlers import apply_label_to_manifest
|
from data.registry_model.label_handlers import apply_label_to_manifest
|
||||||
from image.docker.schema1 import (DockerSchema1ManifestBuilder, ManifestException,
|
from image.docker.schema1 import (DockerSchema1ManifestBuilder, ManifestException,
|
||||||
|
@ -489,7 +490,14 @@ class PreOCIModel(SharedModel, RegistryDataInterface):
|
||||||
logger.exception('Could not find tag manifest for manifest `%s`', manifest._db_id)
|
logger.exception('Could not find tag manifest for manifest `%s`', manifest._db_id)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return self._list_manifest_layers(manifest, tag_manifest.tag.repository_id, include_placements)
|
try:
|
||||||
|
parsed = manifest.get_parsed_manifest()
|
||||||
|
except ManifestException:
|
||||||
|
logger.exception('Could not parse and validate manifest `%s`', manifest._db_id)
|
||||||
|
return None
|
||||||
|
|
||||||
|
repo_ref = RepositoryReference.for_id(tag_manifest.tag.repository_id)
|
||||||
|
return self.list_parsed_manifest_layers(repo_ref, parsed, include_placements)
|
||||||
|
|
||||||
def lookup_derived_image(self, manifest, verb, varying_metadata=None, include_placements=False):
|
def lookup_derived_image(self, manifest, verb, varying_metadata=None, include_placements=False):
|
||||||
"""
|
"""
|
||||||
|
@ -506,8 +514,8 @@ class PreOCIModel(SharedModel, RegistryDataInterface):
|
||||||
derived = model.image.find_derived_storage_for_image(repo_image, verb, varying_metadata)
|
derived = model.image.find_derived_storage_for_image(repo_image, verb, varying_metadata)
|
||||||
return self._build_derived(derived, verb, varying_metadata, include_placements)
|
return self._build_derived(derived, verb, varying_metadata, include_placements)
|
||||||
|
|
||||||
def lookup_or_create_derived_image(self, manifest, verb, storage_location, varying_metadata=None,
|
def lookup_or_create_derived_image(self, manifest, verb, storage_location, storage,
|
||||||
include_placements=False):
|
varying_metadata=None, include_placements=False):
|
||||||
"""
|
"""
|
||||||
Looks up the derived image for the given maniest, verb and optional varying metadata
|
Looks up the derived image for the given maniest, verb and optional varying metadata
|
||||||
and returns it. If none exists, a new derived image is created.
|
and returns it. If none exists, a new derived image is created.
|
||||||
|
|
|
@ -306,17 +306,18 @@ class SharedModel:
|
||||||
|
|
||||||
return LegacyImage.for_image(image, images_map=parent_images_map, blob=blob)
|
return LegacyImage.for_image(image, images_map=parent_images_map, blob=blob)
|
||||||
|
|
||||||
def _list_manifest_layers(self, manifest, repo_id, include_placements=False):
|
def list_parsed_manifest_layers(self, repository_ref, parsed_manifest, include_placements=False):
|
||||||
|
""" Returns an *ordered list* of the layers found in the parsed manifest, starting at the base
|
||||||
|
and working towards the leaf, including the associated Blob and its placements
|
||||||
|
(if specified).
|
||||||
|
"""
|
||||||
|
return self._list_manifest_layers(repository_ref._db_id, parsed_manifest, include_placements)
|
||||||
|
|
||||||
|
def _list_manifest_layers(self, repo_id, parsed, include_placements=False):
|
||||||
""" Returns an *ordered list* of the layers found in the manifest, starting at the base and
|
""" Returns an *ordered list* of the layers found in the manifest, starting at the base and
|
||||||
working towards the leaf, including the associated Blob and its placements (if specified).
|
working towards the leaf, including the associated Blob and its placements (if specified).
|
||||||
Returns None if the manifest could not be parsed and validated.
|
Returns None if the manifest could not be parsed and validated.
|
||||||
"""
|
"""
|
||||||
try:
|
|
||||||
parsed = manifest.get_parsed_manifest()
|
|
||||||
except ManifestException:
|
|
||||||
logger.exception('Could not parse and validate manifest `%s`', manifest._db_id)
|
|
||||||
return None
|
|
||||||
|
|
||||||
storage_map = {}
|
storage_map = {}
|
||||||
if parsed.local_blob_digests:
|
if parsed.local_blob_digests:
|
||||||
blob_query = model.storage.lookup_repo_storages_by_content_checksum(repo_id,
|
blob_query = model.storage.lookup_repo_storages_by_content_checksum(repo_id,
|
||||||
|
@ -331,11 +332,12 @@ class SharedModel:
|
||||||
|
|
||||||
digest_str = str(layer.digest)
|
digest_str = str(layer.digest)
|
||||||
if digest_str not in storage_map:
|
if digest_str not in storage_map:
|
||||||
logger.error('Missing digest `%s` for manifest `%s`', layer.digest, manifest._db_id)
|
logger.error('Missing digest `%s` for manifest `%s`', layer.digest, parsed.digest)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
image_storage = storage_map[digest_str]
|
image_storage = storage_map[digest_str]
|
||||||
assert image_storage.cas_path is not None
|
assert image_storage.cas_path is not None
|
||||||
|
assert image_storage.image_size is not None
|
||||||
|
|
||||||
placements = None
|
placements = None
|
||||||
if include_placements:
|
if include_placements:
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import hashlib
|
import hashlib
|
||||||
import os
|
import os
|
||||||
|
import tarfile
|
||||||
|
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
|
@ -113,3 +114,36 @@ def test_extra_blob_stream_handlers(pre_oci_model):
|
||||||
|
|
||||||
assert ''.join(handler1_result) == 'hello world'
|
assert ''.join(handler1_result) == 'hello world'
|
||||||
assert ''.join(handler2_result) == 'hello world'
|
assert ''.join(handler2_result) == 'hello world'
|
||||||
|
|
||||||
|
|
||||||
|
def valid_tar_gz(contents):
|
||||||
|
layer_data = BytesIO()
|
||||||
|
tar_file = tarfile.open(fileobj=layer_data, mode='w|gz')
|
||||||
|
|
||||||
|
tar_file_info = tarfile.TarInfo(name='somefile')
|
||||||
|
tar_file_info.type = tarfile.REGTYPE
|
||||||
|
tar_file_info.size = len(contents)
|
||||||
|
tar_file_info.mtime = 1
|
||||||
|
|
||||||
|
tar_file.addfile(tar_file_info, BytesIO(contents))
|
||||||
|
tar_file.close()
|
||||||
|
|
||||||
|
layer_bytes = layer_data.getvalue()
|
||||||
|
layer_data.close()
|
||||||
|
return layer_bytes
|
||||||
|
|
||||||
|
|
||||||
|
def test_uncompressed_size(pre_oci_model):
|
||||||
|
repository_ref = pre_oci_model.lookup_repository('devtable', 'complex')
|
||||||
|
storage = DistributedStorage({'local_us': FakeStorage(None)}, ['local_us'])
|
||||||
|
settings = BlobUploadSettings('1K', 512 * 1024, 3600)
|
||||||
|
app_config = {'TESTING': True}
|
||||||
|
|
||||||
|
with upload_blob(repository_ref, storage, settings) as manager:
|
||||||
|
manager.upload_chunk(app_config, BytesIO(valid_tar_gz('hello world')))
|
||||||
|
|
||||||
|
blob = manager.commit_to_blob(app_config)
|
||||||
|
|
||||||
|
assert blob.compressed_size is not None
|
||||||
|
assert blob.uncompressed_size is not None
|
||||||
|
|
||||||
|
|
|
@ -543,8 +543,10 @@ def test_derived_image(registry_model):
|
||||||
assert registry_model.lookup_derived_image(manifest, 'squash', {}) is None
|
assert registry_model.lookup_derived_image(manifest, 'squash', {}) is None
|
||||||
|
|
||||||
# Create a new one.
|
# Create a new one.
|
||||||
squashed = registry_model.lookup_or_create_derived_image(manifest, 'squash', 'local_us', {})
|
squashed = registry_model.lookup_or_create_derived_image(manifest, 'squash',
|
||||||
assert registry_model.lookup_or_create_derived_image(manifest, 'squash', 'local_us', {}) == squashed
|
'local_us', storage, {})
|
||||||
|
assert registry_model.lookup_or_create_derived_image(manifest, 'squash',
|
||||||
|
'local_us', storage, {}) == squashed
|
||||||
assert squashed.unique_id
|
assert squashed.unique_id
|
||||||
|
|
||||||
# Check and set the size.
|
# Check and set the size.
|
||||||
|
@ -560,15 +562,15 @@ def test_derived_image(registry_model):
|
||||||
assert registry_model.lookup_derived_image(manifest, 'squash', {'foo': 'bar'}) is None
|
assert registry_model.lookup_derived_image(manifest, 'squash', {'foo': 'bar'}) is None
|
||||||
|
|
||||||
squashed_foo = registry_model.lookup_or_create_derived_image(manifest, 'squash', 'local_us',
|
squashed_foo = registry_model.lookup_or_create_derived_image(manifest, 'squash', 'local_us',
|
||||||
{'foo': 'bar'})
|
storage, {'foo': 'bar'})
|
||||||
assert squashed_foo != squashed
|
assert squashed_foo != squashed
|
||||||
assert registry_model.lookup_derived_image(manifest, 'squash', {'foo': 'bar'}) == squashed_foo
|
assert registry_model.lookup_derived_image(manifest, 'squash', {'foo': 'bar'}) == squashed_foo
|
||||||
|
|
||||||
assert squashed.unique_id != squashed_foo.unique_id
|
assert squashed.unique_id != squashed_foo.unique_id
|
||||||
|
|
||||||
# Lookup with placements.
|
# Lookup with placements.
|
||||||
squashed = registry_model.lookup_or_create_derived_image(manifest, 'squash', 'local_us', {},
|
squashed = registry_model.lookup_or_create_derived_image(manifest, 'squash', 'local_us',
|
||||||
include_placements=True)
|
storage, {}, include_placements=True)
|
||||||
assert squashed.blob.placements
|
assert squashed.blob.placements
|
||||||
|
|
||||||
# Delete the derived image.
|
# Delete the derived image.
|
||||||
|
|
|
@ -43,7 +43,7 @@ class VerbReporter(TarLayerFormatterReporter):
|
||||||
metric_queue.verb_action_passes.Inc(labelvalues=[self.kind, pass_count])
|
metric_queue.verb_action_passes.Inc(labelvalues=[self.kind, pass_count])
|
||||||
|
|
||||||
|
|
||||||
def _open_stream(formatter, tag, manifest, schema1_manifest, derived_image_id, handlers, reporter):
|
def _open_stream(formatter, tag, schema1_manifest, derived_image_id, handlers, reporter):
|
||||||
"""
|
"""
|
||||||
This method generates a stream of data which will be replicated and read from the queue files.
|
This method generates a stream of data which will be replicated and read from the queue files.
|
||||||
This method runs in a separate process.
|
This method runs in a separate process.
|
||||||
|
@ -51,7 +51,8 @@ def _open_stream(formatter, tag, manifest, schema1_manifest, derived_image_id, h
|
||||||
# For performance reasons, we load the full image list here, cache it, then disconnect from
|
# For performance reasons, we load the full image list here, cache it, then disconnect from
|
||||||
# the database.
|
# the database.
|
||||||
with database.UseThenDisconnect(app.config):
|
with database.UseThenDisconnect(app.config):
|
||||||
layers = registry_model.list_manifest_layers(manifest, include_placements=True)
|
layers = registry_model.list_parsed_manifest_layers(tag.repository, schema1_manifest,
|
||||||
|
include_placements=True)
|
||||||
|
|
||||||
def image_stream_getter(store, blob):
|
def image_stream_getter(store, blob):
|
||||||
def get_stream_for_storage():
|
def get_stream_for_storage():
|
||||||
|
@ -209,6 +210,9 @@ def _verify_repo_verb(_, namespace, repo_name, tag_name, verb, checker=None):
|
||||||
|
|
||||||
# Lookup the requested tag.
|
# Lookup the requested tag.
|
||||||
repo_ref = registry_model.lookup_repository(namespace, repo_name)
|
repo_ref = registry_model.lookup_repository(namespace, repo_name)
|
||||||
|
if repo_ref is None:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
tag = registry_model.get_repo_tag(repo_ref, tag_name)
|
tag = registry_model.get_repo_tag(repo_ref, tag_name)
|
||||||
if tag is None:
|
if tag is None:
|
||||||
logger.debug('Tag %s does not exist in repository %s/%s for user %s', tag, namespace, repo_name,
|
logger.debug('Tag %s does not exist in repository %s/%s for user %s', tag, namespace, repo_name,
|
||||||
|
@ -221,7 +225,7 @@ def _verify_repo_verb(_, namespace, repo_name, tag_name, verb, checker=None):
|
||||||
logger.debug('Could not get manifest on %s/%s:%s::%s', namespace, repo_name, tag.name, verb)
|
logger.debug('Could not get manifest on %s/%s:%s::%s', namespace, repo_name, tag.name, verb)
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
# Ensure the manifest is not a list.
|
# Retrieve the schema1-compatible version of the manifest.
|
||||||
try:
|
try:
|
||||||
schema1_manifest = registry_model.get_schema1_parsed_manifest(manifest, namespace,
|
schema1_manifest = registry_model.get_schema1_parsed_manifest(manifest, namespace,
|
||||||
repo_name, tag.name,
|
repo_name, tag.name,
|
||||||
|
@ -294,8 +298,13 @@ def _repo_verb(namespace, repository, tag_name, verb, formatter, sign=False, che
|
||||||
|
|
||||||
# Lookup/create the derived image for the verb and repo image.
|
# Lookup/create the derived image for the verb and repo image.
|
||||||
derived_image = registry_model.lookup_or_create_derived_image(
|
derived_image = registry_model.lookup_or_create_derived_image(
|
||||||
manifest, verb, storage.preferred_locations[0], varying_metadata={'tag': tag.name},
|
manifest, verb, storage.preferred_locations[0], storage,
|
||||||
|
varying_metadata={'tag': tag.name},
|
||||||
include_placements=True)
|
include_placements=True)
|
||||||
|
if derived_image is None:
|
||||||
|
logger.error('Could not create or lookup a derived image for manifest %s', manifest)
|
||||||
|
abort(400)
|
||||||
|
|
||||||
if not derived_image.blob.uploading:
|
if not derived_image.blob.uploading:
|
||||||
logger.debug('Derived %s image %s exists in storage', verb, derived_image)
|
logger.debug('Derived %s image %s exists in storage', verb, derived_image)
|
||||||
is_head_request = request.method == 'HEAD'
|
is_head_request = request.method == 'HEAD'
|
||||||
|
@ -337,7 +346,7 @@ def _repo_verb(namespace, repository, tag_name, verb, formatter, sign=False, che
|
||||||
# and send the results to the client and storage.
|
# and send the results to the client and storage.
|
||||||
handlers = [hasher.update]
|
handlers = [hasher.update]
|
||||||
reporter = VerbReporter(verb)
|
reporter = VerbReporter(verb)
|
||||||
args = (formatter, tag, manifest, schema1_manifest, derived_image.unique_id, handlers, reporter)
|
args = (formatter, tag, schema1_manifest, derived_image.unique_id, handlers, reporter)
|
||||||
queue_process = QueueProcess(
|
queue_process = QueueProcess(
|
||||||
_open_stream,
|
_open_stream,
|
||||||
8 * 1024,
|
8 * 1024,
|
||||||
|
|
|
@ -213,7 +213,7 @@ class DockerSchema2Config(object):
|
||||||
command=history_entry[DOCKER_SCHEMA2_CONFIG_CREATED_BY_KEY],
|
command=history_entry[DOCKER_SCHEMA2_CONFIG_CREATED_BY_KEY],
|
||||||
is_empty=history_entry.get(DOCKER_SCHEMA2_CONFIG_EMPTY_LAYER_KEY, False))
|
is_empty=history_entry.get(DOCKER_SCHEMA2_CONFIG_EMPTY_LAYER_KEY, False))
|
||||||
|
|
||||||
def build_v1_compatibility(self, layer_index, v1_id, v1_parent_id):
|
def build_v1_compatibility(self, layer_index, v1_id, v1_parent_id, compressed_size=None):
|
||||||
""" Builds the V1 compatibility block for the given layer.
|
""" Builds the V1 compatibility block for the given layer.
|
||||||
|
|
||||||
Note that the layer_index is 0-indexed, with the *base* layer being 0, and the leaf
|
Note that the layer_index is 0-indexed, with the *base* layer being 0, and the leaf
|
||||||
|
@ -237,6 +237,9 @@ class DockerSchema2Config(object):
|
||||||
'Cmd': history[layer_index].command,
|
'Cmd': history[layer_index].command,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if compressed_size is not None:
|
||||||
|
v1_compatibility['Size'] = compressed_size
|
||||||
|
|
||||||
# The history and rootfs keys are schema2-config specific.
|
# The history and rootfs keys are schema2-config specific.
|
||||||
v1_compatibility.pop(DOCKER_SCHEMA2_CONFIG_HISTORY_KEY, None)
|
v1_compatibility.pop(DOCKER_SCHEMA2_CONFIG_HISTORY_KEY, None)
|
||||||
v1_compatibility.pop(DOCKER_SCHEMA2_CONFIG_ROOTFS_KEY, None)
|
v1_compatibility.pop(DOCKER_SCHEMA2_CONFIG_ROOTFS_KEY, None)
|
||||||
|
|
|
@ -30,7 +30,7 @@ DockerV2ManifestLayer = namedtuple('DockerV2ManifestLayer', ['index', 'digest',
|
||||||
'is_remote', 'urls',
|
'is_remote', 'urls',
|
||||||
'compressed_size'])
|
'compressed_size'])
|
||||||
|
|
||||||
LayerWithV1ID = namedtuple('LayerWithV1ID', ['layer', 'v1_id', 'v1_parent_id'])
|
LayerWithV1ID = namedtuple('LayerWithV1ID', ['layer', 'v1_id', 'v1_parent_id', 'compressed_size'])
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -270,7 +270,8 @@ class DockerSchema2Manifest(ManifestInterface):
|
||||||
digest_history.update(str(layer.index))
|
digest_history.update(str(layer.index))
|
||||||
digest_history.update("|")
|
digest_history.update("|")
|
||||||
v1_layer_id = digest_history.hexdigest()
|
v1_layer_id = digest_history.hexdigest()
|
||||||
yield LayerWithV1ID(layer=layer, v1_id=v1_layer_id, v1_parent_id=v1_layer_parent_id)
|
yield LayerWithV1ID(layer=layer, v1_id=v1_layer_id, v1_parent_id=v1_layer_parent_id,
|
||||||
|
compressed_size=layer.compressed_size)
|
||||||
|
|
||||||
def populate_schema1_builder(self, v1_builder, content_retriever):
|
def populate_schema1_builder(self, v1_builder, content_retriever):
|
||||||
""" Populates a DockerSchema1ManifestBuilder with the layers and config from
|
""" Populates a DockerSchema1ManifestBuilder with the layers and config from
|
||||||
|
@ -284,7 +285,8 @@ class DockerSchema2Manifest(ManifestInterface):
|
||||||
for layer_with_ids in reversed(layers): # Schema1 has layers in reverse order
|
for layer_with_ids in reversed(layers): # Schema1 has layers in reverse order
|
||||||
v1_compatibility = schema2_config.build_v1_compatibility(layer_with_ids.layer.index,
|
v1_compatibility = schema2_config.build_v1_compatibility(layer_with_ids.layer.index,
|
||||||
layer_with_ids.v1_id,
|
layer_with_ids.v1_id,
|
||||||
layer_with_ids.v1_parent_id)
|
layer_with_ids.v1_parent_id,
|
||||||
|
layer_with_ids.compressed_size)
|
||||||
v1_builder.add_layer(str(layer_with_ids.layer.digest), json.dumps(v1_compatibility))
|
v1_builder.add_layer(str(layer_with_ids.layer.digest), json.dumps(v1_compatibility))
|
||||||
|
|
||||||
return v1_builder
|
return v1_builder
|
||||||
|
|
|
@ -1560,3 +1560,72 @@ def test_push_pull_manifest_list_duplicate_manifest(v22_protocol, basic_images,
|
||||||
# Pull and verify the manifest list.
|
# Pull and verify the manifest list.
|
||||||
v22_protocol.pull_list(liveserver_session, 'devtable', 'newrepo', 'latest', manifestlist,
|
v22_protocol.pull_list(liveserver_session, 'devtable', 'newrepo', 'latest', manifestlist,
|
||||||
credentials=credentials, options=options)
|
credentials=credentials, options=options)
|
||||||
|
|
||||||
|
|
||||||
|
def test_squashed_image_unsupported(v22_protocol, basic_images, liveserver_session, liveserver,
|
||||||
|
app_reloader, data_model):
|
||||||
|
""" Test: Attempting to pull a squashed image for a manifest list without an amd64+linux entry.
|
||||||
|
"""
|
||||||
|
credentials = ('devtable', 'password')
|
||||||
|
if data_model != 'oci_model':
|
||||||
|
return
|
||||||
|
|
||||||
|
credentials = ('devtable', 'password')
|
||||||
|
options = ProtocolOptions()
|
||||||
|
|
||||||
|
# Build the manifest that will go in the list.
|
||||||
|
blobs = {}
|
||||||
|
manifest = v22_protocol.build_schema2(basic_images, blobs, options)
|
||||||
|
|
||||||
|
# Create and push the manifest list.
|
||||||
|
builder = DockerSchema2ManifestListBuilder()
|
||||||
|
builder.add_manifest(manifest, 'foobar', 'someos')
|
||||||
|
manifestlist = builder.build()
|
||||||
|
|
||||||
|
v22_protocol.push_list(liveserver_session, 'devtable', 'newrepo', 'latest', manifestlist,
|
||||||
|
[manifest], blobs,
|
||||||
|
credentials=credentials, options=options)
|
||||||
|
|
||||||
|
# Attempt to pull the squashed version.
|
||||||
|
response = liveserver_session.get('/c1/squash/devtable/newrepo/latest', auth=credentials)
|
||||||
|
assert response.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
def test_squashed_image_manifest_list(v22_protocol, basic_images, liveserver_session, liveserver,
|
||||||
|
app_reloader, data_model):
|
||||||
|
""" Test: Pull a squashed image for a manifest list with an amd64+linux entry.
|
||||||
|
"""
|
||||||
|
credentials = ('devtable', 'password')
|
||||||
|
if data_model != 'oci_model':
|
||||||
|
return
|
||||||
|
|
||||||
|
credentials = ('devtable', 'password')
|
||||||
|
options = ProtocolOptions()
|
||||||
|
|
||||||
|
# Build the manifest that will go in the list.
|
||||||
|
blobs = {}
|
||||||
|
manifest = v22_protocol.build_schema2(basic_images, blobs, options)
|
||||||
|
|
||||||
|
# Create and push the manifest list.
|
||||||
|
builder = DockerSchema2ManifestListBuilder()
|
||||||
|
builder.add_manifest(manifest, 'amd64', 'linux')
|
||||||
|
manifestlist = builder.build()
|
||||||
|
|
||||||
|
v22_protocol.push_list(liveserver_session, 'devtable', 'newrepo', 'latest', manifestlist,
|
||||||
|
[manifest], blobs,
|
||||||
|
credentials=credentials, options=options)
|
||||||
|
|
||||||
|
# Pull the squashed version.
|
||||||
|
response = liveserver_session.get('/c1/squash/devtable/newrepo/latest', auth=credentials)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
# Verify the squashed image.
|
||||||
|
tar = tarfile.open(fileobj=StringIO(response.content))
|
||||||
|
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
|
||||||
|
|
Reference in a new issue