Define a formal manifest interface and implement in the schema1 and schema2 manifests
This will allow us to pass arbitrary manifests to the model
This commit is contained in:
parent
cf5a6e1adc
commit
36c7482385
4 changed files with 94 additions and 5 deletions
41
image/docker/interfaces.py
Normal file
41
image/docker/interfaces.py
Normal file
|
@ -0,0 +1,41 @@
|
|||
from abc import ABCMeta, abstractproperty
|
||||
from six import add_metaclass
|
||||
|
||||
@add_metaclass(ABCMeta)
|
||||
class ManifestInterface(object):
|
||||
""" Defines the interface for the various manifests types supported. """
|
||||
@abstractproperty
|
||||
def digest(self):
|
||||
""" The digest of the manifest, including type prefix. """
|
||||
pass
|
||||
|
||||
@abstractproperty
|
||||
def media_type(self):
|
||||
""" The media type of the schema. """
|
||||
pass
|
||||
|
||||
@abstractproperty
|
||||
def manifest_dict(self):
|
||||
""" Returns the manifest as a dictionary ready to be serialized to JSON. """
|
||||
pass
|
||||
|
||||
@abstractproperty
|
||||
def bytes(self):
|
||||
""" Returns the bytes of the manifest. """
|
||||
pass
|
||||
|
||||
@abstractproperty
|
||||
def layers(self):
|
||||
""" Returns the layers of this manifest, from base to leaf. """
|
||||
pass
|
||||
|
||||
@abstractproperty
|
||||
def leaf_layer_v1_image_id(self):
|
||||
""" Returns the Docker V1 image ID for the leaf (top) layer, if any, or None if none. """
|
||||
pass
|
||||
|
||||
@abstractproperty
|
||||
def blob_digests(self):
|
||||
""" Returns an iterator over all the blob digests referenced by this manifest,
|
||||
from base to leaf. The blob digests are strings with prefixes.
|
||||
"""
|
|
@ -18,9 +18,9 @@ from jwt.utils import base64url_encode, base64url_decode
|
|||
|
||||
from digest import digest_tools
|
||||
from image.docker import ManifestException
|
||||
from image.docker.interfaces import ManifestInterface
|
||||
from image.docker.v1 import DockerV1Metadata
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@ -85,7 +85,7 @@ class Schema1V1Metadata(namedtuple('Schema1V1Metadata', ['image_id', 'parent_ima
|
|||
"""
|
||||
|
||||
|
||||
class DockerSchema1Manifest(object):
|
||||
class DockerSchema1Manifest(ManifestInterface):
|
||||
METASCHEMA = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
|
@ -235,6 +235,10 @@ class DockerSchema1Manifest(object):
|
|||
def manifest_json(self):
|
||||
return self._parsed
|
||||
|
||||
@property
|
||||
def manifest_dict(self):
|
||||
return self._parsed
|
||||
|
||||
@property
|
||||
def digest(self):
|
||||
return digest_tools.sha256_digest(self.payload)
|
||||
|
@ -252,6 +256,10 @@ class DockerSchema1Manifest(object):
|
|||
def checksums(self):
|
||||
return list({str(mdata.digest) for mdata in self.layers})
|
||||
|
||||
@property
|
||||
def leaf_layer_v1_image_id(self):
|
||||
return self.layers[-1].v1_metadata.image_id
|
||||
|
||||
@property
|
||||
def leaf_layer(self):
|
||||
return self.layers[-1]
|
||||
|
@ -262,6 +270,10 @@ class DockerSchema1Manifest(object):
|
|||
self._layers = list(self._generate_layers())
|
||||
return self._layers
|
||||
|
||||
@property
|
||||
def blob_digests(self):
|
||||
return [str(layer.digest) for layer in self.layers]
|
||||
|
||||
def _generate_layers(self):
|
||||
"""
|
||||
Returns a generator of objects that have the blobSum and v1Compatibility keys in them,
|
||||
|
|
|
@ -7,6 +7,7 @@ from jsonschema import validate as validate_schema, ValidationError
|
|||
|
||||
from digest import digest_tools
|
||||
from image.docker import ManifestException
|
||||
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,
|
||||
|
@ -39,7 +40,7 @@ class MalformedSchema2Manifest(ManifestException):
|
|||
pass
|
||||
|
||||
|
||||
class DockerSchema2Manifest(object):
|
||||
class DockerSchema2Manifest(ManifestInterface):
|
||||
METASCHEMA = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
|
@ -121,6 +122,7 @@ class DockerSchema2Manifest(object):
|
|||
|
||||
def __init__(self, manifest_bytes):
|
||||
self._layers = None
|
||||
self._payload = manifest_bytes
|
||||
|
||||
try:
|
||||
self._parsed = json.loads(manifest_bytes)
|
||||
|
@ -136,6 +138,18 @@ class DockerSchema2Manifest(object):
|
|||
def schema_version(self):
|
||||
return 2
|
||||
|
||||
@property
|
||||
def manifest_dict(self):
|
||||
return self._parsed
|
||||
|
||||
@property
|
||||
def media_type(self):
|
||||
return self._parsed[DOCKER_SCHEMA2_MANIFEST_MEDIATYPE_KEY]
|
||||
|
||||
@property
|
||||
def digest(self):
|
||||
return digest_tools.sha256_digest(self._payload)
|
||||
|
||||
@property
|
||||
def config(self):
|
||||
config = self._parsed[DOCKER_SCHEMA2_MANIFEST_CONFIG_KEY]
|
||||
|
@ -153,6 +167,18 @@ class DockerSchema2Manifest(object):
|
|||
def leaf_layer(self):
|
||||
return self.layers[-1]
|
||||
|
||||
@property
|
||||
def leaf_layer_v1_image_id(self):
|
||||
return list(self.layers_with_v1_ids)[-1].v1_id
|
||||
|
||||
@property
|
||||
def blob_digests(self):
|
||||
return [str(layer.digest) for layer in self.layers]
|
||||
|
||||
@property
|
||||
def bytes(self):
|
||||
return self._payload
|
||||
|
||||
def _generate_layers(self):
|
||||
for index, layer in enumerate(self._parsed[DOCKER_SCHEMA2_MANIFEST_LAYERS_KEY]):
|
||||
content_type = layer[DOCKER_SCHEMA2_MANIFEST_MEDIATYPE_KEY]
|
||||
|
|
|
@ -2,7 +2,8 @@ import json
|
|||
import pytest
|
||||
|
||||
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)
|
||||
from image.docker.schema2.manifest import MalformedSchema2Manifest, DockerSchema2Manifest
|
||||
from image.docker.schema2.test.test_config import CONFIG_BYTES
|
||||
|
||||
|
@ -57,7 +58,8 @@ def test_valid_manifest():
|
|||
manifest = DockerSchema2Manifest(MANIFEST_BYTES)
|
||||
assert manifest.config.size == 1885
|
||||
assert str(manifest.config.digest) == 'sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7'
|
||||
|
||||
assert manifest.media_type == "application/vnd.docker.distribution.manifest.v2+json"
|
||||
|
||||
assert len(manifest.layers) == 4
|
||||
assert manifest.layers[0].is_remote
|
||||
assert manifest.layers[0].size == 1234
|
||||
|
@ -68,6 +70,10 @@ def test_valid_manifest():
|
|||
assert not manifest.leaf_layer.is_remote
|
||||
assert manifest.leaf_layer.size == 73109
|
||||
|
||||
blob_digests = list(manifest.blob_digests)
|
||||
assert len(blob_digests) == len(manifest.layers)
|
||||
assert blob_digests == [str(layer.digest) for layer in manifest.layers]
|
||||
|
||||
|
||||
def test_build_schema1():
|
||||
manifest = DockerSchema2Manifest(MANIFEST_BYTES)
|
||||
|
@ -76,6 +82,7 @@ def test_build_schema1():
|
|||
manifest.populate_schema1_builder(builder, lambda digest: CONFIG_BYTES)
|
||||
schema1 = builder.build(docker_v2_signing_key)
|
||||
|
||||
assert schema1.media_type == DOCKER_SCHEMA1_SIGNED_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])
|
||||
|
@ -85,3 +92,6 @@ def test_build_schema1():
|
|||
assert layer.digest == manifest_layers[index].layer.digest
|
||||
assert layer.v1_metadata.image_id == manifest_layers[index].v1_id
|
||||
assert layer.v1_metadata.parent_image_id == manifest_layers[index].v1_parent_id
|
||||
|
||||
for index, digest in enumerate(schema1.blob_digests):
|
||||
assert digest == str(list(manifest.blob_digests)[index])
|
||||
|
|
Reference in a new issue