import json from cachetools import lru_cache from jsonschema import validate as validate_schema, ValidationError from image.docker.schema1 import DOCKER_SCHEMA1_MANIFEST_CONTENT_TYPE from image.docker.schema1 import DockerSchema1Manifest from image.docker.schema2 import (DOCKER_SCHEMA2_MANIFESTLIST_CONTENT_TYPE, DOCKER_SCHEMA2_MANIFEST_CONTENT_TYPE) from image.docker.schema2.manifest import DockerSchema2Manifest # Keys. DOCKER_SCHEMA2_MANIFESTLIST_VERSION_KEY = 'schemaVersion' DOCKER_SCHEMA2_MANIFESTLIST_MEDIATYPE_KEY = 'mediaType' DOCKER_SCHEMA2_MANIFESTLIST_SIZE_KEY = 'size' DOCKER_SCHEMA2_MANIFESTLIST_DIGEST_KEY = 'digest' DOCKER_SCHEMA2_MANIFESTLIST_MANIFESTS_KEY = 'manifests' DOCKER_SCHEMA2_MANIFESTLIST_PLATFORM_KEY = 'platform' DOCKER_SCHEMA2_MANIFESTLIST_ARCHITECTURE_KEY = 'architecture' DOCKER_SCHEMA2_MANIFESTLIST_OS_KEY = 'os' DOCKER_SCHEMA2_MANIFESTLIST_OS_VERSION_KEY = 'os.version' DOCKER_SCHEMA2_MANIFESTLIST_OS_FEATURES_KEY = 'os.features' DOCKER_SCHEMA2_MANIFESTLIST_FEATURES_KEY = 'features' DOCKER_SCHEMA2_MANIFESTLIST_VARIANT_KEY = 'variant' class MalformedSchema2ManifestList(Exception): """ Raised when a manifest list fails an assertion that should be true according to the Docker Manifest v2.2 Specification. """ pass class LazyManifestLoader(object): def __init__(self, manifest_data, lookup_manifest_fn): self._manifest_data = manifest_data self._lookup_manifest_fn = lookup_manifest_fn self._loaded_manifest = None @property def manifest_obj(self): if self._loaded_manifest is not None: return self._loaded_manifest self._loaded_manifest = self._load_manifest() return self._loaded_manifest def _load_manifest(self): digest = self._manifest_data[DOCKER_SCHEMA2_MANIFESTLIST_DIGEST_KEY] size = self._manifest_data[DOCKER_SCHEMA2_MANIFESTLIST_SIZE_KEY] manifest_bytes = self._lookup_manifest_fn(digest) if len(manifest_bytes) != size: raise MalformedSchema2ManifestList('Size of manifest does not match that retrieved: %s vs %s', len(manifest_bytes), size) content_type = self._manifest_data[DOCKER_SCHEMA2_MANIFESTLIST_MEDIATYPE_KEY] if content_type == DOCKER_SCHEMA2_MANIFEST_CONTENT_TYPE: return DockerSchema2Manifest(manifest_bytes) if content_type == DOCKER_SCHEMA1_MANIFEST_CONTENT_TYPE: return DockerSchema1Manifest(manifest_bytes, validate=False) raise MalformedSchema2ManifestList('Unknown manifest content type') class DockerSchema2ManifestList(object): METASCHEMA = { 'type': 'object', 'properties': { DOCKER_SCHEMA2_MANIFESTLIST_VERSION_KEY: { 'type': 'number', 'description': 'The version of the manifest list. Must always be `2`.', 'minimum': 2, 'maximum': 2, }, DOCKER_SCHEMA2_MANIFESTLIST_MEDIATYPE_KEY: { 'type': 'string', 'description': 'The media type of the manifest list.', 'enum': [DOCKER_SCHEMA2_MANIFESTLIST_CONTENT_TYPE], }, DOCKER_SCHEMA2_MANIFESTLIST_MANIFESTS_KEY: { 'type': 'array', 'description': 'The manifests field contains a list of manifests for specific platforms', 'items': { 'type': 'object', 'properties': { DOCKER_SCHEMA2_MANIFESTLIST_MEDIATYPE_KEY: { 'type': 'string', 'description': 'The MIME type of the referenced object. This will generally be ' + 'application/vnd.docker.distribution.manifest.v2+json, but it ' + 'could also be application/vnd.docker.distribution.manifest.v1+json ' + 'if the manifest list references a legacy schema-1 manifest.', 'enum': [DOCKER_SCHEMA2_MANIFEST_CONTENT_TYPE, DOCKER_SCHEMA1_MANIFEST_CONTENT_TYPE], }, DOCKER_SCHEMA2_MANIFESTLIST_SIZE_KEY: { 'type': 'number', 'description': 'The size in bytes of the object. This field exists so that a ' + 'client will have an expected size for the content before ' + 'validating. If the length of the retrieved content does not ' + 'match the specified length, the content should not be trusted.', }, DOCKER_SCHEMA2_MANIFESTLIST_DIGEST_KEY: { 'type': 'string', 'description': 'The content addressable digest of the manifest in the blob store', }, DOCKER_SCHEMA2_MANIFESTLIST_PLATFORM_KEY: { 'type': 'object', 'description': 'The platform object describes the platform which the image in ' + 'the manifest runs on', 'properties': { DOCKER_SCHEMA2_MANIFESTLIST_ARCHITECTURE_KEY: { 'type': 'string', 'description': 'Specifies the CPU architecture, for example amd64 or ppc64le.', }, DOCKER_SCHEMA2_MANIFESTLIST_OS_KEY: { 'type': 'string', 'description': 'Specifies the operating system, for example linux or windows', }, DOCKER_SCHEMA2_MANIFESTLIST_OS_VERSION_KEY: { 'type': 'string', 'description': 'Specifies the operating system version, for example 10.0.10586', }, DOCKER_SCHEMA2_MANIFESTLIST_OS_FEATURES_KEY: { 'type': 'array', 'description': 'specifies an array of strings, each listing a required OS ' + 'feature (for example on Windows win32k)', 'items': { 'type': 'string', }, }, DOCKER_SCHEMA2_MANIFESTLIST_VARIANT_KEY: { 'type': 'string', 'description': 'Specifies a variant of the CPU, for example armv6l to specify ' + 'a particular CPU variant of the ARM CPU', }, DOCKER_SCHEMA2_MANIFESTLIST_FEATURES_KEY: { 'type': 'array', 'description': 'specifies an array of strings, each listing a required CPU ' + 'feature (for example sse4 or aes).', 'items': { 'type': 'string', }, }, }, 'required': [DOCKER_SCHEMA2_MANIFESTLIST_ARCHITECTURE_KEY, DOCKER_SCHEMA2_MANIFESTLIST_OS_KEY], }, }, 'required': [DOCKER_SCHEMA2_MANIFESTLIST_MEDIATYPE_KEY, DOCKER_SCHEMA2_MANIFESTLIST_SIZE_KEY, DOCKER_SCHEMA2_MANIFESTLIST_DIGEST_KEY, DOCKER_SCHEMA2_MANIFESTLIST_PLATFORM_KEY], }, }, }, 'required': [DOCKER_SCHEMA2_MANIFESTLIST_VERSION_KEY, DOCKER_SCHEMA2_MANIFESTLIST_MEDIATYPE_KEY, DOCKER_SCHEMA2_MANIFESTLIST_MANIFESTS_KEY], } def __init__(self, manifest_bytes): self._layers = None try: self._parsed = json.loads(manifest_bytes) except ValueError as ve: raise MalformedSchema2ManifestList('malformed manifest data: %s' % ve) try: validate_schema(self._parsed, DockerSchema2ManifestList.METASCHEMA) except ValidationError as ve: raise MalformedSchema2ManifestList('manifest data does not match schema: %s' % ve) @lru_cache(maxsize=1) def manifests(self, lookup_manifest_fn): """ Returns the manifests in the list. The `lookup_manifest_fn` is a function that returns the manifest bytes for the specified digest. """ manifests = self._parsed[DOCKER_SCHEMA2_MANIFESTLIST_MANIFESTS_KEY] return [LazyManifestLoader(m, lookup_manifest_fn) for m in manifests] def get_v1_compatible_manifest(self, lookup_manifest_fn): """ Returns the manifest that is compatible with V1, by virtue of being `amd64` and `linux`. If none, returns None. """ for manifest in self.manifests(lookup_manifest_fn): platform = manifest._manifest_data[DOCKER_SCHEMA2_MANIFESTLIST_PLATFORM_KEY] architecture = platform[DOCKER_SCHEMA2_MANIFESTLIST_ARCHITECTURE_KEY] os = platform[DOCKER_SCHEMA2_MANIFESTLIST_OS_KEY] if architecture == 'amd64' and os == 'linux': return manifest return None