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 digest import digest_tools
|
||||||
from image.docker import ManifestException
|
from image.docker import ManifestException
|
||||||
|
from image.docker.interfaces import ManifestInterface
|
||||||
from image.docker.v1 import DockerV1Metadata
|
from image.docker.v1 import DockerV1Metadata
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@ -85,7 +85,7 @@ class Schema1V1Metadata(namedtuple('Schema1V1Metadata', ['image_id', 'parent_ima
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class DockerSchema1Manifest(object):
|
class DockerSchema1Manifest(ManifestInterface):
|
||||||
METASCHEMA = {
|
METASCHEMA = {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
'properties': {
|
'properties': {
|
||||||
|
@ -235,6 +235,10 @@ class DockerSchema1Manifest(object):
|
||||||
def manifest_json(self):
|
def manifest_json(self):
|
||||||
return self._parsed
|
return self._parsed
|
||||||
|
|
||||||
|
@property
|
||||||
|
def manifest_dict(self):
|
||||||
|
return self._parsed
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def digest(self):
|
def digest(self):
|
||||||
return digest_tools.sha256_digest(self.payload)
|
return digest_tools.sha256_digest(self.payload)
|
||||||
|
@ -252,6 +256,10 @@ class DockerSchema1Manifest(object):
|
||||||
def checksums(self):
|
def checksums(self):
|
||||||
return list({str(mdata.digest) for mdata in self.layers})
|
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
|
@property
|
||||||
def leaf_layer(self):
|
def leaf_layer(self):
|
||||||
return self.layers[-1]
|
return self.layers[-1]
|
||||||
|
@ -262,6 +270,10 @@ class DockerSchema1Manifest(object):
|
||||||
self._layers = list(self._generate_layers())
|
self._layers = list(self._generate_layers())
|
||||||
return self._layers
|
return self._layers
|
||||||
|
|
||||||
|
@property
|
||||||
|
def blob_digests(self):
|
||||||
|
return [str(layer.digest) for layer in self.layers]
|
||||||
|
|
||||||
def _generate_layers(self):
|
def _generate_layers(self):
|
||||||
"""
|
"""
|
||||||
Returns a generator of objects that have the blobSum and v1Compatibility keys in them,
|
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 digest import digest_tools
|
||||||
from image.docker import ManifestException
|
from image.docker import ManifestException
|
||||||
|
from image.docker.interfaces import ManifestInterface
|
||||||
from image.docker.schema2 import (DOCKER_SCHEMA2_MANIFEST_CONTENT_TYPE,
|
from image.docker.schema2 import (DOCKER_SCHEMA2_MANIFEST_CONTENT_TYPE,
|
||||||
DOCKER_SCHEMA2_CONFIG_CONTENT_TYPE,
|
DOCKER_SCHEMA2_CONFIG_CONTENT_TYPE,
|
||||||
DOCKER_SCHEMA2_LAYER_CONTENT_TYPE,
|
DOCKER_SCHEMA2_LAYER_CONTENT_TYPE,
|
||||||
|
@ -39,7 +40,7 @@ class MalformedSchema2Manifest(ManifestException):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class DockerSchema2Manifest(object):
|
class DockerSchema2Manifest(ManifestInterface):
|
||||||
METASCHEMA = {
|
METASCHEMA = {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
'properties': {
|
'properties': {
|
||||||
|
@ -121,6 +122,7 @@ class DockerSchema2Manifest(object):
|
||||||
|
|
||||||
def __init__(self, manifest_bytes):
|
def __init__(self, manifest_bytes):
|
||||||
self._layers = None
|
self._layers = None
|
||||||
|
self._payload = manifest_bytes
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._parsed = json.loads(manifest_bytes)
|
self._parsed = json.loads(manifest_bytes)
|
||||||
|
@ -136,6 +138,18 @@ class DockerSchema2Manifest(object):
|
||||||
def schema_version(self):
|
def schema_version(self):
|
||||||
return 2
|
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
|
@property
|
||||||
def config(self):
|
def config(self):
|
||||||
config = self._parsed[DOCKER_SCHEMA2_MANIFEST_CONFIG_KEY]
|
config = self._parsed[DOCKER_SCHEMA2_MANIFEST_CONFIG_KEY]
|
||||||
|
@ -153,6 +167,18 @@ class DockerSchema2Manifest(object):
|
||||||
def leaf_layer(self):
|
def leaf_layer(self):
|
||||||
return self.layers[-1]
|
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):
|
def _generate_layers(self):
|
||||||
for index, layer in enumerate(self._parsed[DOCKER_SCHEMA2_MANIFEST_LAYERS_KEY]):
|
for index, layer in enumerate(self._parsed[DOCKER_SCHEMA2_MANIFEST_LAYERS_KEY]):
|
||||||
content_type = layer[DOCKER_SCHEMA2_MANIFEST_MEDIATYPE_KEY]
|
content_type = layer[DOCKER_SCHEMA2_MANIFEST_MEDIATYPE_KEY]
|
||||||
|
|
|
@ -2,7 +2,8 @@ import json
|
||||||
import pytest
|
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)
|
||||||
from image.docker.schema2.manifest import MalformedSchema2Manifest, DockerSchema2Manifest
|
from image.docker.schema2.manifest import MalformedSchema2Manifest, DockerSchema2Manifest
|
||||||
from image.docker.schema2.test.test_config import CONFIG_BYTES
|
from image.docker.schema2.test.test_config import CONFIG_BYTES
|
||||||
|
|
||||||
|
@ -57,6 +58,7 @@ def test_valid_manifest():
|
||||||
manifest = DockerSchema2Manifest(MANIFEST_BYTES)
|
manifest = DockerSchema2Manifest(MANIFEST_BYTES)
|
||||||
assert manifest.config.size == 1885
|
assert manifest.config.size == 1885
|
||||||
assert str(manifest.config.digest) == 'sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7'
|
assert str(manifest.config.digest) == 'sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7'
|
||||||
|
assert manifest.media_type == "application/vnd.docker.distribution.manifest.v2+json"
|
||||||
|
|
||||||
assert len(manifest.layers) == 4
|
assert len(manifest.layers) == 4
|
||||||
assert manifest.layers[0].is_remote
|
assert manifest.layers[0].is_remote
|
||||||
|
@ -68,6 +70,10 @@ def test_valid_manifest():
|
||||||
assert not manifest.leaf_layer.is_remote
|
assert not manifest.leaf_layer.is_remote
|
||||||
assert manifest.leaf_layer.size == 73109
|
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():
|
def test_build_schema1():
|
||||||
manifest = DockerSchema2Manifest(MANIFEST_BYTES)
|
manifest = DockerSchema2Manifest(MANIFEST_BYTES)
|
||||||
|
@ -76,6 +82,7 @@ def test_build_schema1():
|
||||||
manifest.populate_schema1_builder(builder, lambda digest: CONFIG_BYTES)
|
manifest.populate_schema1_builder(builder, lambda digest: CONFIG_BYTES)
|
||||||
schema1 = builder.build(docker_v2_signing_key)
|
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 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.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])
|
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.digest == manifest_layers[index].layer.digest
|
||||||
assert layer.v1_metadata.image_id == manifest_layers[index].v1_id
|
assert layer.v1_metadata.image_id == manifest_layers[index].v1_id
|
||||||
assert layer.v1_metadata.parent_image_id == manifest_layers[index].v1_parent_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