From 36c748238540b3914a1f6ffbd5089c17e2154f39 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Tue, 31 Jul 2018 15:41:30 -0400 Subject: [PATCH] Define a formal manifest interface and implement in the schema1 and schema2 manifests This will allow us to pass arbitrary manifests to the model --- image/docker/interfaces.py | 41 ++++++++++++++++++++++ image/docker/schema1.py | 16 +++++++-- image/docker/schema2/manifest.py | 28 ++++++++++++++- image/docker/schema2/test/test_manifest.py | 14 ++++++-- 4 files changed, 94 insertions(+), 5 deletions(-) create mode 100644 image/docker/interfaces.py diff --git a/image/docker/interfaces.py b/image/docker/interfaces.py new file mode 100644 index 000000000..5ce12de93 --- /dev/null +++ b/image/docker/interfaces.py @@ -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. + """ diff --git a/image/docker/schema1.py b/image/docker/schema1.py index 9b11470e1..47134e178 100644 --- a/image/docker/schema1.py +++ b/image/docker/schema1.py @@ -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, diff --git a/image/docker/schema2/manifest.py b/image/docker/schema2/manifest.py index 12b00956a..4e59d20a3 100644 --- a/image/docker/schema2/manifest.py +++ b/image/docker/schema2/manifest.py @@ -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] diff --git a/image/docker/schema2/test/test_manifest.py b/image/docker/schema2/test/test_manifest.py index b365ab5b3..6037e22e4 100644 --- a/image/docker/schema2/test/test_manifest.py +++ b/image/docker/schema2/test/test_manifest.py @@ -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])