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:
Joseph Schorr 2018-11-25 16:16:59 +02:00
parent 947c029afa
commit 4985040d31
13 changed files with 173 additions and 25 deletions

View file

@ -82,6 +82,11 @@ class ManifestInterface(object):
of manifest does not support labels. """
pass
@abstractmethod
def get_requires_empty_layer_blob(self, content_retriever):
""" Whether this schema requires the special empty layer blob. """
pass
@abstractmethod
def unsigned(self):
""" Returns an unsigned version of this manifest. """

View file

@ -312,6 +312,9 @@ class DockerSchema1Manifest(ManifestInterface):
def get_manifest_labels(self, content_retriever):
return self.layers[-1].v1_metadata.labels
def get_requires_empty_layer_blob(self, content_retriever):
return False
def unsigned(self):
if self.media_type == DOCKER_SCHEMA1_MANIFEST_CONTENT_TYPE:
return self

View file

@ -19,3 +19,12 @@ OCI_MANIFESTLIST_CONTENT_TYPE = 'application/vnd.oci.image.index.v1+json'
DOCKER_SCHEMA2_CONTENT_TYPES = {DOCKER_SCHEMA2_MANIFEST_CONTENT_TYPE,
DOCKER_SCHEMA2_MANIFESTLIST_CONTENT_TYPE}
OCI_CONTENT_TYPES = {OCI_MANIFEST_CONTENT_TYPE, OCI_MANIFESTLIST_CONTENT_TYPE}
# The magical digest to be used for "empty" layers.
# https://github.com/docker/distribution/blob/749f6afb4572201e3c37325d0ffedb6f32be8950/manifest/schema1/config_builder.go#L22
EMPTY_LAYER_BLOB_DIGEST = 'sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4'
EMPTY_LAYER_SIZE = 32
EMPTY_LAYER_BYTES = "".join(map(chr, [
31, 139, 8, 0, 0, 9, 110, 136, 0, 255, 98, 24, 5, 163, 96, 20, 140, 88,
0, 8, 0, 0, 255, 255, 46, 175, 181, 239, 0, 4, 0, 0,
]))

View file

@ -205,6 +205,15 @@ class DockerSchema2Config(object):
""" Returns a dictionary of all the labels defined in this configuration. """
return self._parsed.get('config', {}).get('Labels', {}) or {}
@property
def has_empty_layer(self):
""" Returns whether this config contains an empty layer. """
for history_entry in self._parsed[DOCKER_SCHEMA2_CONFIG_HISTORY_KEY]:
if history_entry.get(DOCKER_SCHEMA2_CONFIG_EMPTY_LAYER_KEY, False):
return True
return False
@property
def history(self):
""" Returns the history of the image, started at the base layer. """

View file

@ -255,6 +255,9 @@ class DockerSchema2ManifestList(ManifestInterface):
def has_legacy_image(self):
return False
def get_requires_empty_layer_blob(self, content_retriever):
return False
def get_schema1_manifest(self, namespace_name, repo_name, tag_name, content_retriever):
""" Returns the manifest that is compatible with V1, by virtue of being `amd64` and `linux`.
If none, returns None.

View file

@ -11,7 +11,8 @@ from image.docker.interfaces import ManifestInterface
from image.docker.schema2 import (DOCKER_SCHEMA2_MANIFEST_CONTENT_TYPE,
DOCKER_SCHEMA2_CONFIG_CONTENT_TYPE,
DOCKER_SCHEMA2_LAYER_CONTENT_TYPE,
DOCKER_SCHEMA2_REMOTE_LAYER_CONTENT_TYPE)
DOCKER_SCHEMA2_REMOTE_LAYER_CONTENT_TYPE,
EMPTY_LAYER_BLOB_DIGEST, EMPTY_LAYER_SIZE)
from image.docker.schema1 import DockerSchema1ManifestBuilder
from image.docker.schema2.config import DockerSchema2Config
@ -34,8 +35,6 @@ ManifestImageLayer = namedtuple('ManifestImageLayer', ['history', 'blob_layer',
'v1_parent_id', 'compressed_size',
'blob_digest'])
EMPTY_BLOB_DIGEST = 'sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4'
logger = logging.getLogger(__name__)
class MalformedSchema2Manifest(ManifestException):
@ -233,8 +232,8 @@ class DockerSchema2Manifest(ManifestInterface):
v1_layer_parent_id = v1_layer_id
blob_layer = None if history_entry.is_empty else self.layers[blob_index]
blob_digest = EMPTY_BLOB_DIGEST if blob_layer is None else str(blob_layer.digest)
compressed_size = 0 if blob_layer is None else blob_layer.compressed_size
blob_digest = EMPTY_LAYER_BLOB_DIGEST if blob_layer is None else str(blob_layer.digest)
compressed_size = EMPTY_LAYER_SIZE if blob_layer is None else blob_layer.compressed_size
# Create a new synthesized V1 ID for the history layer by hashing its content and
# the blob associated withn it.
@ -295,6 +294,13 @@ class DockerSchema2Manifest(ManifestInterface):
def unsigned(self):
return self
def get_requires_empty_layer_blob(self, content_retriever):
schema2_config = self._get_built_config(content_retriever)
if schema2_config is None:
return None
return schema2_config.has_empty_layer
def _populate_schema1_builder(self, v1_builder, content_retriever):
""" Populates a DockerSchema1ManifestBuilder with the layers and config from
this schema.

View file

@ -1,4 +1,8 @@
import json
import tarfile
from cachetools import lru_cache
from io import BytesIO
from image.docker.interfaces import ContentRetriever
@ -22,3 +26,12 @@ class ContentRetrieverForTesting(ContentRetriever):
digests = {}
digests[digest] = padded_string
return ContentRetrieverForTesting(digests)
@lru_cache(maxsize=1)
def generate_empty_layer_data():
""" Generates the layer data for an "empty" layer. """
with BytesIO() as f:
tar_file = tarfile.open(fileobj=f, mode='w|gw')
tar_file.close()
return f.getvalue()