Merge pull request #3054 from quay/v2-manifest
V2 Schema 2 manifest support
This commit is contained in:
commit
b8b2c75822
11 changed files with 997 additions and 1 deletions
|
@ -114,7 +114,7 @@ class DockerSchema1Manifest(object):
|
|||
},
|
||||
},
|
||||
'required': [DOCKER_SCHEMA1_PROTECTED_KEY, DOCKER_SCHEMA1_HEADER_KEY,
|
||||
DOCKER_SCHEMA1_SIGNATURE_KEY],
|
||||
DOCKER_SCHEMA1_SIGNATURE_KEY],
|
||||
},
|
||||
},
|
||||
DOCKER_SCHEMA1_REPO_TAG_KEY: {
|
||||
|
@ -195,6 +195,10 @@ class DockerSchema1Manifest(object):
|
|||
if not verified:
|
||||
raise InvalidSchema1Signature()
|
||||
|
||||
@property
|
||||
def schema_version(self):
|
||||
return 1
|
||||
|
||||
@property
|
||||
def content_type(self):
|
||||
return DOCKER_SCHEMA1_SIGNED_MANIFEST_CONTENT_TYPE
|
||||
|
|
|
@ -8,6 +8,11 @@ https://github.com/docker/distribution/blob/master/docs/spec/manifest-v2-2.md
|
|||
DOCKER_SCHEMA2_MANIFEST_CONTENT_TYPE = 'application/vnd.docker.distribution.manifest.v2+json'
|
||||
DOCKER_SCHEMA2_MANIFESTLIST_CONTENT_TYPE = 'application/vnd.docker.distribution.manifest.list.v2+json'
|
||||
|
||||
DOCKER_SCHEMA2_LAYER_CONTENT_TYPE = 'application/vnd.docker.image.rootfs.diff.tar.gzip'
|
||||
DOCKER_SCHEMA2_REMOTE_LAYER_CONTENT_TYPE = 'application/vnd.docker.image.rootfs.foreign.diff.tar.gzip'
|
||||
|
||||
DOCKER_SCHEMA2_CONFIG_CONTENT_TYPE = 'application/vnd.docker.container.image.v1+json'
|
||||
|
||||
OCI_MANIFEST_CONTENT_TYPE = 'application/vnd.oci.image.manifest.v1+json'
|
||||
OCI_MANIFESTLIST_CONTENT_TYPE = 'application/vnd.oci.image.index.v1+json'
|
||||
|
216
image/docker/schema2/config.py
Normal file
216
image/docker/schema2/config.py
Normal file
|
@ -0,0 +1,216 @@
|
|||
"""
|
||||
Implements validation and conversion for the Schema2 config JSON.
|
||||
|
||||
Example:
|
||||
{
|
||||
"architecture": "amd64",
|
||||
"config": {
|
||||
"Hostname": "",
|
||||
"Domainname": "",
|
||||
"User": "",
|
||||
"AttachStdin": false,
|
||||
"AttachStdout": false,
|
||||
"AttachStderr": false,
|
||||
"Tty": false,
|
||||
"OpenStdin": false,
|
||||
"StdinOnce": false,
|
||||
"Env": [
|
||||
"HTTP_PROXY=http:\/\/localhost:8080",
|
||||
"http_proxy=http:\/\/localhost:8080",
|
||||
"PATH=\/usr\/local\/sbin:\/usr\/local\/bin:\/usr\/sbin:\/usr\/bin:\/sbin:\/bin"
|
||||
],
|
||||
"Cmd": [
|
||||
"sh"
|
||||
],
|
||||
"Image": "",
|
||||
"Volumes": null,
|
||||
"WorkingDir": "",
|
||||
"Entrypoint": null,
|
||||
"OnBuild": null,
|
||||
"Labels": {
|
||||
|
||||
}
|
||||
},
|
||||
"container": "b7a43694b435c8e9932615643f61f975a9213e453b15cd6c2a386f144a2d2de9",
|
||||
"container_config": {
|
||||
"Hostname": "b7a43694b435",
|
||||
"Domainname": "",
|
||||
"User": "",
|
||||
"AttachStdin": true,
|
||||
"AttachStdout": true,
|
||||
"AttachStderr": true,
|
||||
"Tty": true,
|
||||
"OpenStdin": true,
|
||||
"StdinOnce": true,
|
||||
"Env": [
|
||||
"HTTP_PROXY=http:\/\/localhost:8080",
|
||||
"http_proxy=http:\/\/localhost:8080",
|
||||
"PATH=\/usr\/local\/sbin:\/usr\/local\/bin:\/usr\/sbin:\/usr\/bin:\/sbin:\/bin"
|
||||
],
|
||||
"Cmd": [
|
||||
"sh"
|
||||
],
|
||||
"Image": "somenamespace\/somerepo",
|
||||
"Volumes": null,
|
||||
"WorkingDir": "",
|
||||
"Entrypoint": null,
|
||||
"OnBuild": null,
|
||||
"Labels": {
|
||||
|
||||
}
|
||||
},
|
||||
"created": "2018-04-16T10:41:19.079522722Z",
|
||||
"docker_version": "17.09.0-ce",
|
||||
"history": [
|
||||
{
|
||||
"created": "2018-04-03T18:37:09.284840891Z",
|
||||
"created_by": "\/bin\/sh -c #(nop) ADD file:9e4ca21cbd24dc05b454b6be21c7c639216ae66559b21ba24af0d665c62620dc in \/ "
|
||||
},
|
||||
{
|
||||
"created": "2018-04-03T18:37:09.613317719Z",
|
||||
"created_by": "\/bin\/sh -c #(nop) CMD [\"sh\"]",
|
||||
"empty_layer": true
|
||||
},
|
||||
{
|
||||
"created": "2018-04-16T10:37:44.418262777Z",
|
||||
"created_by": "sh"
|
||||
},
|
||||
{
|
||||
"created": "2018-04-16T10:41:19.079522722Z",
|
||||
"created_by": "sh"
|
||||
}
|
||||
],
|
||||
"os": "linux",
|
||||
"rootfs": {
|
||||
"type": "layers",
|
||||
"diff_ids": [
|
||||
"sha256:3e596351c689c8827a3c9635bc1083cff17fa4a174f84f0584bd0ae6f384195b",
|
||||
"sha256:4552be273c71275a88de0b8c8853dcac18cb74d5790f5383d9b38d4ac55062d5",
|
||||
"sha256:1319c76152ca37fbeb7fb71e0ffa7239bc19ffbe3b95c00417ece39d89d06e6e"
|
||||
]
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
import copy
|
||||
import json
|
||||
|
||||
from collections import namedtuple
|
||||
from jsonschema import validate as validate_schema, ValidationError
|
||||
from dateutil.parser import parse as parse_date
|
||||
|
||||
DOCKER_SCHEMA2_CONFIG_HISTORY_KEY = "history"
|
||||
DOCKER_SCHEMA2_CONFIG_ROOTFS_KEY = "rootfs"
|
||||
DOCKER_SCHEMA2_CONFIG_CREATED_KEY = "created"
|
||||
DOCKER_SCHEMA2_CONFIG_CREATED_BY_KEY = "created_by"
|
||||
DOCKER_SCHEMA2_CONFIG_EMPTY_LAYER_KEY = "empty_layer"
|
||||
DOCKER_SCHEMA2_CONFIG_TYPE_KEY = "type"
|
||||
|
||||
|
||||
LayerHistory = namedtuple('LayerHistory', ['created', 'created_datetime', 'command', 'is_empty'])
|
||||
|
||||
|
||||
class MalformedSchema2Config(Exception):
|
||||
"""
|
||||
Raised when a config fails an assertion that should be true according to the Docker Manifest
|
||||
v2.2 Config Specification.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class DockerSchema2Config(object):
|
||||
METASCHEMA = {
|
||||
'type': 'object',
|
||||
'description': 'The container configuration found in a schema 2 manifest',
|
||||
'required': [DOCKER_SCHEMA2_CONFIG_HISTORY_KEY, DOCKER_SCHEMA2_CONFIG_ROOTFS_KEY],
|
||||
'properties': {
|
||||
DOCKER_SCHEMA2_CONFIG_HISTORY_KEY: {
|
||||
'type': 'array',
|
||||
'description': 'The history used to create the container image',
|
||||
'items': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
DOCKER_SCHEMA2_CONFIG_EMPTY_LAYER_KEY: {
|
||||
'type': 'boolean',
|
||||
'description': 'If present, this layer is empty',
|
||||
},
|
||||
DOCKER_SCHEMA2_CONFIG_CREATED_KEY: {
|
||||
'type': 'string',
|
||||
'description': 'The date/time that the layer was created',
|
||||
'format': 'date-time',
|
||||
'x-example': '2018-04-03T18:37:09.284840891Z',
|
||||
},
|
||||
DOCKER_SCHEMA2_CONFIG_CREATED_BY_KEY: {
|
||||
'type': 'string',
|
||||
'description': 'The command used to create the layer',
|
||||
'x-example': '\/bin\/sh -c #(nop) ADD file:somesha in /',
|
||||
},
|
||||
},
|
||||
'required': [DOCKER_SCHEMA2_CONFIG_CREATED_KEY, DOCKER_SCHEMA2_CONFIG_CREATED_BY_KEY],
|
||||
'additionalProperties': True,
|
||||
},
|
||||
},
|
||||
DOCKER_SCHEMA2_CONFIG_ROOTFS_KEY: {
|
||||
'type': 'object',
|
||||
'description': 'Describes the root filesystem for this image',
|
||||
'properties': {
|
||||
DOCKER_SCHEMA2_CONFIG_TYPE_KEY: {
|
||||
'type': 'string',
|
||||
'description': 'The type of the root file system entries',
|
||||
},
|
||||
},
|
||||
'required': [DOCKER_SCHEMA2_CONFIG_TYPE_KEY],
|
||||
'additionalProperties': True,
|
||||
},
|
||||
},
|
||||
'additionalProperties': True,
|
||||
}
|
||||
|
||||
def __init__(self, config_bytes):
|
||||
try:
|
||||
self._parsed = json.loads(config_bytes)
|
||||
except ValueError as ve:
|
||||
raise MalformedSchema2Config('malformed config data: %s' % ve)
|
||||
|
||||
try:
|
||||
validate_schema(self._parsed, DockerSchema2Config.METASCHEMA)
|
||||
except ValidationError as ve:
|
||||
raise MalformedSchema2Config('config data does not match schema: %s' % ve)
|
||||
|
||||
@property
|
||||
def history(self):
|
||||
""" Returns the history of the image, started at the base layer. """
|
||||
for history_entry in self._parsed[DOCKER_SCHEMA2_CONFIG_HISTORY_KEY]:
|
||||
created_datetime = parse_date(history_entry[DOCKER_SCHEMA2_CONFIG_CREATED_KEY])
|
||||
yield LayerHistory(created_datetime=created_datetime,
|
||||
created=history_entry[DOCKER_SCHEMA2_CONFIG_CREATED_KEY],
|
||||
command=history_entry[DOCKER_SCHEMA2_CONFIG_CREATED_BY_KEY],
|
||||
is_empty=history_entry.get(DOCKER_SCHEMA2_CONFIG_EMPTY_LAYER_KEY, False))
|
||||
|
||||
def build_v1_compatibility(self, layer_index, v1_id, v1_parent_id):
|
||||
""" Builds the V1 compatibility block for the given layer.
|
||||
|
||||
Note that the layer_index is 0-indexed, with the *base* layer being 0, and the leaf
|
||||
layer being last.
|
||||
"""
|
||||
history = list(self.history)
|
||||
|
||||
# If the layer is the leaf, it gets the full config (minus 2 fields). Otherwise, it gets only
|
||||
# IDs.
|
||||
v1_compatibility = copy.deepcopy(self._parsed) if layer_index == len(history) - 1 else {}
|
||||
v1_compatibility['id'] = v1_id
|
||||
if v1_parent_id is not None:
|
||||
v1_compatibility['parent'] = v1_parent_id
|
||||
|
||||
if 'created' not in v1_compatibility:
|
||||
v1_compatibility['created'] = history[layer_index].created
|
||||
|
||||
if 'container_config' not in v1_compatibility:
|
||||
v1_compatibility['container_config'] = {
|
||||
'Cmd': history[layer_index].command,
|
||||
}
|
||||
|
||||
# The history and rootfs keys are schema2-config specific.
|
||||
v1_compatibility.pop(DOCKER_SCHEMA2_CONFIG_HISTORY_KEY, None)
|
||||
v1_compatibility.pop(DOCKER_SCHEMA2_CONFIG_ROOTFS_KEY, None)
|
||||
return v1_compatibility
|
194
image/docker/schema2/list.py
Normal file
194
image/docker/schema2/list.py
Normal file
|
@ -0,0 +1,194 @@
|
|||
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
|
212
image/docker/schema2/manifest.py
Normal file
212
image/docker/schema2/manifest.py
Normal file
|
@ -0,0 +1,212 @@
|
|||
import json
|
||||
import logging
|
||||
import hashlib
|
||||
|
||||
from collections import namedtuple
|
||||
from jsonschema import validate as validate_schema, ValidationError
|
||||
|
||||
from digest import digest_tools
|
||||
from image.docker import ManifestException
|
||||
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)
|
||||
from image.docker.schema2.config import DockerSchema2Config
|
||||
|
||||
# Keys.
|
||||
DOCKER_SCHEMA2_MANIFEST_VERSION_KEY = 'schemaVersion'
|
||||
DOCKER_SCHEMA2_MANIFEST_MEDIATYPE_KEY = 'mediaType'
|
||||
DOCKER_SCHEMA2_MANIFEST_CONFIG_KEY = 'config'
|
||||
DOCKER_SCHEMA2_MANIFEST_SIZE_KEY = 'size'
|
||||
DOCKER_SCHEMA2_MANIFEST_DIGEST_KEY = 'digest'
|
||||
DOCKER_SCHEMA2_MANIFEST_LAYERS_KEY = 'layers'
|
||||
DOCKER_SCHEMA2_MANIFEST_URLS_KEY = 'urls'
|
||||
|
||||
# Named tuples.
|
||||
DockerV2ManifestConfig = namedtuple('DockerV2ManifestConfig', ['size', 'digest'])
|
||||
DockerV2ManifestLayer = namedtuple('DockerV2ManifestLayer', ['index', 'size', 'digest',
|
||||
'is_remote', 'urls'])
|
||||
|
||||
LayerWithV1ID = namedtuple('LayerWithV1ID', ['layer', 'v1_id', 'v1_parent_id'])
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class MalformedSchema2Manifest(ManifestException):
|
||||
"""
|
||||
Raised when a manifest fails an assertion that should be true according to the Docker Manifest
|
||||
v2.2 Specification.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class DockerSchema2Manifest(object):
|
||||
METASCHEMA = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
DOCKER_SCHEMA2_MANIFEST_VERSION_KEY: {
|
||||
'type': 'number',
|
||||
'description': 'The version of the schema. Must always be `2`.',
|
||||
'minimum': 2,
|
||||
'maximum': 2,
|
||||
},
|
||||
DOCKER_SCHEMA2_MANIFEST_MEDIATYPE_KEY: {
|
||||
'type': 'string',
|
||||
'description': 'The media type of the schema.',
|
||||
'enum': [DOCKER_SCHEMA2_MANIFEST_CONTENT_TYPE],
|
||||
},
|
||||
DOCKER_SCHEMA2_MANIFEST_CONFIG_KEY: {
|
||||
'type': 'object',
|
||||
'description': 'The config field references a configuration object for a container, ' +
|
||||
'by digest. This configuration item is a JSON blob that the runtime ' +
|
||||
'uses to set up the container.',
|
||||
'properties': {
|
||||
DOCKER_SCHEMA2_MANIFEST_MEDIATYPE_KEY: {
|
||||
'type': 'string',
|
||||
'description': 'The MIME type of the referenced object. This should generally be ' +
|
||||
'application/vnd.docker.container.image.v1+json',
|
||||
'enum': [DOCKER_SCHEMA2_CONFIG_CONTENT_TYPE],
|
||||
},
|
||||
DOCKER_SCHEMA2_MANIFEST_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_MANIFEST_DIGEST_KEY: {
|
||||
'type': 'string',
|
||||
'description': 'The content addressable digest of the config in the blob store',
|
||||
},
|
||||
},
|
||||
'required': [DOCKER_SCHEMA2_MANIFEST_MEDIATYPE_KEY, DOCKER_SCHEMA2_MANIFEST_SIZE_KEY,
|
||||
DOCKER_SCHEMA2_MANIFEST_DIGEST_KEY],
|
||||
},
|
||||
DOCKER_SCHEMA2_MANIFEST_LAYERS_KEY: {
|
||||
'type': 'array',
|
||||
'description': 'The layer list is ordered starting from the base ' +
|
||||
'image (opposite order of schema1).',
|
||||
'items': {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
DOCKER_SCHEMA2_MANIFEST_MEDIATYPE_KEY: {
|
||||
'type': 'string',
|
||||
'description': 'The MIME type of the referenced object. This should generally be ' +
|
||||
'application/vnd.docker.image.rootfs.diff.tar.gzip. Layers of type ' +
|
||||
'application/vnd.docker.image.rootfs.foreign.diff.tar.gzip may be ' +
|
||||
'pulled from a remote location but they should never be pushed.',
|
||||
'enum': [DOCKER_SCHEMA2_LAYER_CONTENT_TYPE, DOCKER_SCHEMA2_REMOTE_LAYER_CONTENT_TYPE],
|
||||
},
|
||||
DOCKER_SCHEMA2_MANIFEST_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_MANIFEST_DIGEST_KEY: {
|
||||
'type': 'string',
|
||||
'description': 'The content addressable digest of the layer in the blob store',
|
||||
},
|
||||
},
|
||||
'required': [
|
||||
DOCKER_SCHEMA2_MANIFEST_MEDIATYPE_KEY, DOCKER_SCHEMA2_MANIFEST_SIZE_KEY,
|
||||
DOCKER_SCHEMA2_MANIFEST_DIGEST_KEY,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
'required': [DOCKER_SCHEMA2_MANIFEST_VERSION_KEY, DOCKER_SCHEMA2_MANIFEST_MEDIATYPE_KEY,
|
||||
DOCKER_SCHEMA2_MANIFEST_CONFIG_KEY, DOCKER_SCHEMA2_MANIFEST_LAYERS_KEY],
|
||||
}
|
||||
|
||||
def __init__(self, manifest_bytes):
|
||||
self._layers = None
|
||||
|
||||
try:
|
||||
self._parsed = json.loads(manifest_bytes)
|
||||
except ValueError as ve:
|
||||
raise MalformedSchema2Manifest('malformed manifest data: %s' % ve)
|
||||
|
||||
try:
|
||||
validate_schema(self._parsed, DockerSchema2Manifest.METASCHEMA)
|
||||
except ValidationError as ve:
|
||||
raise MalformedSchema2Manifest('manifest data does not match schema: %s' % ve)
|
||||
|
||||
@property
|
||||
def schema_version(self):
|
||||
return 2
|
||||
|
||||
@property
|
||||
def config(self):
|
||||
config = self._parsed[DOCKER_SCHEMA2_MANIFEST_CONFIG_KEY]
|
||||
return DockerV2ManifestConfig(size=config[DOCKER_SCHEMA2_MANIFEST_SIZE_KEY],
|
||||
digest=config[DOCKER_SCHEMA2_MANIFEST_DIGEST_KEY])
|
||||
|
||||
@property
|
||||
def layers(self):
|
||||
""" Returns the layers of this manifest, from base to leaf. """
|
||||
if self._layers is None:
|
||||
self._layers = list(self._generate_layers())
|
||||
return self._layers
|
||||
|
||||
@property
|
||||
def leaf_layer(self):
|
||||
return self.layers[-1]
|
||||
|
||||
def _generate_layers(self):
|
||||
for index, layer in enumerate(self._parsed[DOCKER_SCHEMA2_MANIFEST_LAYERS_KEY]):
|
||||
content_type = layer[DOCKER_SCHEMA2_MANIFEST_MEDIATYPE_KEY]
|
||||
is_remote = content_type == DOCKER_SCHEMA2_REMOTE_LAYER_CONTENT_TYPE
|
||||
|
||||
try:
|
||||
digest = digest_tools.Digest.parse_digest(layer[DOCKER_SCHEMA2_MANIFEST_DIGEST_KEY])
|
||||
except digest_tools.InvalidDigestException:
|
||||
raise MalformedSchema2Manifest('could not parse manifest digest: %s' %
|
||||
layer[DOCKER_SCHEMA2_MANIFEST_DIGEST_KEY])
|
||||
|
||||
yield DockerV2ManifestLayer(index=index,
|
||||
size=layer[DOCKER_SCHEMA2_MANIFEST_SIZE_KEY],
|
||||
digest=digest,
|
||||
is_remote=is_remote,
|
||||
urls=layer.get(DOCKER_SCHEMA2_MANIFEST_URLS_KEY))
|
||||
|
||||
@property
|
||||
def layers_with_v1_ids(self):
|
||||
digest_history = hashlib.sha256()
|
||||
v1_layer_parent_id = None
|
||||
v1_layer_id = None
|
||||
|
||||
for layer in self.layers:
|
||||
v1_layer_parent_id = v1_layer_id
|
||||
|
||||
# Create a new synthesized V1 ID for the layer by adding its digest and index to the
|
||||
# existing digest history hash builder. This will ensure unique V1s across *all* schemas in
|
||||
# a repository.
|
||||
digest_history.update(str(layer.digest))
|
||||
digest_history.update("#")
|
||||
digest_history.update(str(layer.index))
|
||||
digest_history.update("|")
|
||||
v1_layer_id = digest_history.hexdigest()
|
||||
yield LayerWithV1ID(layer=layer, v1_id=v1_layer_id, v1_parent_id=v1_layer_parent_id)
|
||||
|
||||
def populate_schema1_builder(self, v1_builder, lookup_config_fn):
|
||||
""" Populates a DockerSchema1ManifestBuilder with the layers and config from
|
||||
this schema. The `lookup_config_fn` is a function that, when given the config
|
||||
digest SHA, returns the associated configuration JSON bytes for this schema.
|
||||
"""
|
||||
config_bytes = lookup_config_fn(self.config.digest)
|
||||
if len(config_bytes) != self.config.size:
|
||||
raise MalformedSchema2Manifest('Size of config does not match that retrieved: %s vs %s',
|
||||
len(config_bytes), self.config.size)
|
||||
|
||||
schema2_config = DockerSchema2Config(config_bytes)
|
||||
|
||||
# Build the V1 IDs for the layers.
|
||||
layers = list(self.layers_with_v1_ids)
|
||||
for layer_with_ids in reversed(layers): # Schema1 has layers in reverse order
|
||||
v1_compatibility = schema2_config.build_v1_compatibility(layer_with_ids.layer.index,
|
||||
layer_with_ids.v1_id,
|
||||
layer_with_ids.v1_parent_id)
|
||||
v1_builder.add_layer(str(layer_with_ids.layer.digest), json.dumps(v1_compatibility))
|
||||
|
||||
return v1_builder
|
0
image/docker/schema2/test/__init__.py
Normal file
0
image/docker/schema2/test/__init__.py
Normal file
129
image/docker/schema2/test/test_config.py
Normal file
129
image/docker/schema2/test/test_config.py
Normal file
|
@ -0,0 +1,129 @@
|
|||
import json
|
||||
import pytest
|
||||
|
||||
from image.docker.schema2.config import MalformedSchema2Config, DockerSchema2Config
|
||||
|
||||
@pytest.mark.parametrize('json_data', [
|
||||
'',
|
||||
'{}',
|
||||
"""
|
||||
{
|
||||
"unknown": "key"
|
||||
}
|
||||
""",
|
||||
])
|
||||
def test_malformed_configs(json_data):
|
||||
with pytest.raises(MalformedSchema2Config):
|
||||
DockerSchema2Config(json_data)
|
||||
|
||||
CONFIG_BYTES = json.dumps({
|
||||
"architecture": "amd64",
|
||||
"config": {
|
||||
"Hostname": "",
|
||||
"Domainname": "",
|
||||
"User": "",
|
||||
"AttachStdin": False,
|
||||
"AttachStdout": False,
|
||||
"AttachStderr": False,
|
||||
"Tty": False,
|
||||
"OpenStdin": False,
|
||||
"StdinOnce": False,
|
||||
"Env": [
|
||||
"HTTP_PROXY=http:\/\/localhost:8080",
|
||||
"http_proxy=http:\/\/localhost:8080",
|
||||
"PATH=\/usr\/local\/sbin:\/usr\/local\/bin:\/usr\/sbin:\/usr\/bin:\/sbin:\/bin"
|
||||
],
|
||||
"Cmd": [
|
||||
"sh"
|
||||
],
|
||||
"Image": "",
|
||||
"Volumes": None,
|
||||
"WorkingDir": "",
|
||||
"Entrypoint": None,
|
||||
"OnBuild": None,
|
||||
"Labels": {
|
||||
|
||||
}
|
||||
},
|
||||
"container": "b7a43694b435c8e9932615643f61f975a9213e453b15cd6c2a386f144a2d2de9",
|
||||
"container_config": {
|
||||
"Hostname": "b7a43694b435",
|
||||
"Domainname": "",
|
||||
"User": "",
|
||||
"AttachStdin": True,
|
||||
"AttachStdout": True,
|
||||
"AttachStderr": True,
|
||||
"Tty": True,
|
||||
"OpenStdin": True,
|
||||
"StdinOnce": True,
|
||||
"Env": [
|
||||
"HTTP_PROXY=http:\/\/localhost:8080",
|
||||
"http_proxy=http:\/\/localhost:8080",
|
||||
"PATH=\/usr\/local\/sbin:\/usr\/local\/bin:\/usr\/sbin:\/usr\/bin:\/sbin:\/bin"
|
||||
],
|
||||
"Cmd": [
|
||||
"sh"
|
||||
],
|
||||
"Image": "jschorr\/somerepo",
|
||||
"Volumes": None,
|
||||
"WorkingDir": "",
|
||||
"Entrypoint": None,
|
||||
"OnBuild": None,
|
||||
"Labels": {
|
||||
|
||||
}
|
||||
},
|
||||
"created": "2018-04-16T10:41:19.079522722Z",
|
||||
"docker_version": "17.09.0-ce",
|
||||
"history": [
|
||||
{
|
||||
"created": "2018-04-03T18:37:09.284840891Z",
|
||||
"created_by": "\/bin\/sh -c #(nop) ADD file:9e4ca21cbd24dc05b454b6be21c7c639216ae66559b21ba24af0d665c62620dc in \/ "
|
||||
},
|
||||
{
|
||||
"created": "2018-04-03T18:37:09.613317719Z",
|
||||
"created_by": "/bin/sh -c #(nop) CMD [\"sh\"]",
|
||||
"empty_layer": True
|
||||
},
|
||||
{
|
||||
"created": "2018-04-16T10:37:44.418262777Z",
|
||||
"created_by": "sh"
|
||||
},
|
||||
{
|
||||
"created": "2018-04-16T10:41:19.079522722Z",
|
||||
"created_by": "sh"
|
||||
}
|
||||
],
|
||||
"os": "linux",
|
||||
"rootfs": {
|
||||
"type": "layers",
|
||||
"diff_ids": [
|
||||
"sha256:3e596351c689c8827a3c9635bc1083cff17fa4a174f84f0584bd0ae6f384195b",
|
||||
"sha256:4552be273c71275a88de0b8c8853dcac18cb74d5790f5383d9b38d4ac55062d5",
|
||||
"sha256:1319c76152ca37fbeb7fb71e0ffa7239bc19ffbe3b95c00417ece39d89d06e6e"
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
def test_valid_config():
|
||||
config = DockerSchema2Config(CONFIG_BYTES)
|
||||
history = list(config.history)
|
||||
assert len(history) == 4
|
||||
|
||||
assert not history[0].is_empty
|
||||
assert history[1].is_empty
|
||||
|
||||
assert history[0].created_datetime.year == 2018
|
||||
assert history[1].command == '/bin/sh -c #(nop) CMD ["sh"]'
|
||||
assert history[2].command == 'sh'
|
||||
|
||||
for index, history_entry in enumerate(history):
|
||||
v1_compat = config.build_v1_compatibility(index, 'somev1id', 'someparentid')
|
||||
assert v1_compat['id'] == 'somev1id'
|
||||
assert v1_compat['parent'] == 'someparentid'
|
||||
|
||||
if index == 3:
|
||||
assert v1_compat['container_config'] == config._parsed['container_config']
|
||||
else:
|
||||
assert 'Hostname' not in v1_compat['container_config']
|
||||
assert v1_compat['container_config']['Cmd'] == history_entry.command
|
70
image/docker/schema2/test/test_list.py
Normal file
70
image/docker/schema2/test/test_list.py
Normal file
|
@ -0,0 +1,70 @@
|
|||
import json
|
||||
import pytest
|
||||
|
||||
from image.docker.schema1 import DockerSchema1Manifest
|
||||
from image.docker.schema2.manifest import DockerSchema2Manifest
|
||||
from image.docker.schema2.list import MalformedSchema2ManifestList, DockerSchema2ManifestList
|
||||
from image.docker.schema2.test.test_manifest import MANIFEST_BYTES as v22_bytes
|
||||
from image.docker.test.test_schema1 import MANIFEST_BYTES as v21_bytes
|
||||
|
||||
@pytest.mark.parametrize('json_data', [
|
||||
'',
|
||||
'{}',
|
||||
"""
|
||||
{
|
||||
"unknown": "key"
|
||||
}
|
||||
""",
|
||||
])
|
||||
def test_malformed_manifest_lists(json_data):
|
||||
with pytest.raises(MalformedSchema2ManifestList):
|
||||
DockerSchema2ManifestList(json_data)
|
||||
|
||||
|
||||
MANIFESTLIST_BYTES = json.dumps({
|
||||
"schemaVersion": 2,
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
|
||||
"manifests": [
|
||||
{
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"size": 983,
|
||||
"digest": "sha256:e6",
|
||||
"platform": {
|
||||
"architecture": "ppc64le",
|
||||
"os": "linux",
|
||||
}
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v1+json",
|
||||
"size": 878,
|
||||
"digest": "sha256:5b",
|
||||
"platform": {
|
||||
"architecture": "amd64",
|
||||
"os": "linux",
|
||||
"features": [
|
||||
"sse4"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
def test_valid_manifestlist():
|
||||
def _get_manifest(digest):
|
||||
if digest == 'sha256:e6':
|
||||
return v22_bytes
|
||||
else:
|
||||
return v21_bytes
|
||||
|
||||
manifestlist = DockerSchema2ManifestList(MANIFESTLIST_BYTES)
|
||||
assert len(manifestlist.manifests(_get_manifest)) == 2
|
||||
|
||||
for index, manifest in enumerate(manifestlist.manifests(_get_manifest)):
|
||||
if index == 0:
|
||||
assert isinstance(manifest.manifest_obj, DockerSchema2Manifest)
|
||||
assert manifest.manifest_obj.schema_version == 2
|
||||
else:
|
||||
assert isinstance(manifest.manifest_obj, DockerSchema1Manifest)
|
||||
assert manifest.manifest_obj.schema_version == 1
|
||||
|
||||
assert manifestlist.get_v1_compatible_manifest(_get_manifest).manifest_obj.schema_version == 1
|
87
image/docker/schema2/test/test_manifest.py
Normal file
87
image/docker/schema2/test/test_manifest.py
Normal file
|
@ -0,0 +1,87 @@
|
|||
import json
|
||||
import pytest
|
||||
|
||||
from app import docker_v2_signing_key
|
||||
from image.docker.schema1 import DockerSchema1ManifestBuilder
|
||||
from image.docker.schema2.manifest import MalformedSchema2Manifest, DockerSchema2Manifest
|
||||
from image.docker.schema2.test.test_config import CONFIG_BYTES
|
||||
|
||||
@pytest.mark.parametrize('json_data', [
|
||||
'',
|
||||
'{}',
|
||||
"""
|
||||
{
|
||||
"unknown": "key"
|
||||
}
|
||||
""",
|
||||
])
|
||||
def test_malformed_manifests(json_data):
|
||||
with pytest.raises(MalformedSchema2Manifest):
|
||||
DockerSchema2Manifest(json_data)
|
||||
|
||||
|
||||
MANIFEST_BYTES = json.dumps({
|
||||
"schemaVersion": 2,
|
||||
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||
"config": {
|
||||
"mediaType": "application/vnd.docker.container.image.v1+json",
|
||||
"size": 1885,
|
||||
"digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7"
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"mediaType": "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip",
|
||||
"size": 1234,
|
||||
"digest": "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736",
|
||||
"urls": ['http://some/url'],
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||
"size": 32654,
|
||||
"digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f"
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||
"size": 16724,
|
||||
"digest": "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b"
|
||||
},
|
||||
{
|
||||
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||
"size": 73109,
|
||||
"digest": "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736"
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
def test_valid_manifest():
|
||||
manifest = DockerSchema2Manifest(MANIFEST_BYTES)
|
||||
assert manifest.config.size == 1885
|
||||
assert str(manifest.config.digest) == 'sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7'
|
||||
|
||||
assert len(manifest.layers) == 4
|
||||
assert manifest.layers[0].is_remote
|
||||
assert manifest.layers[0].size == 1234
|
||||
assert str(manifest.layers[0].digest) == 'sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736'
|
||||
assert manifest.layers[0].urls
|
||||
|
||||
assert manifest.leaf_layer == manifest.layers[3]
|
||||
assert not manifest.leaf_layer.is_remote
|
||||
assert manifest.leaf_layer.size == 73109
|
||||
|
||||
|
||||
def test_build_schema1():
|
||||
manifest = DockerSchema2Manifest(MANIFEST_BYTES)
|
||||
|
||||
builder = DockerSchema1ManifestBuilder('somenamespace', 'somename', 'sometag')
|
||||
manifest.populate_schema1_builder(builder, lambda digest: CONFIG_BYTES)
|
||||
schema1 = builder.build(docker_v2_signing_key)
|
||||
|
||||
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])
|
||||
|
||||
manifest_layers = list(manifest.layers_with_v1_ids)
|
||||
for index, layer in enumerate(schema1.layers):
|
||||
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
|
0
image/docker/test/__init__.py
Normal file
0
image/docker/test/__init__.py
Normal file
79
image/docker/test/test_schema1.py
Normal file
79
image/docker/test/test_schema1.py
Normal file
|
@ -0,0 +1,79 @@
|
|||
import json
|
||||
import pytest
|
||||
|
||||
from image.docker.schema1 import MalformedSchema1Manifest, DockerSchema1Manifest
|
||||
|
||||
@pytest.mark.parametrize('json_data', [
|
||||
'',
|
||||
'{}',
|
||||
"""
|
||||
{
|
||||
"unknown": "key"
|
||||
}
|
||||
""",
|
||||
])
|
||||
def test_malformed_manifests(json_data):
|
||||
with pytest.raises(MalformedSchema1Manifest):
|
||||
DockerSchema1Manifest(json_data)
|
||||
|
||||
|
||||
MANIFEST_BYTES = json.dumps({
|
||||
"name": 'hello-world',
|
||||
"tag": "latest",
|
||||
"architecture": "amd64",
|
||||
"fsLayers": [
|
||||
{
|
||||
"blobSum": "sha256:cc8567d70002e957612902a8e985ea129d831ebe04057d88fb644857caa45d11"
|
||||
},
|
||||
{
|
||||
"blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
|
||||
}
|
||||
],
|
||||
"history": [
|
||||
{
|
||||
"v1Compatibility": "{\"id\":\"someid\", \"parent\": \"anotherid\"}"
|
||||
},
|
||||
{
|
||||
"v1Compatibility": "{\"id\":\"anotherid\"}"
|
||||
},
|
||||
],
|
||||
"schemaVersion": 1,
|
||||
"signatures": [
|
||||
{
|
||||
"header": {
|
||||
"jwk": {
|
||||
"crv": "P-256",
|
||||
"kid": "OD6I:6DRK:JXEJ:KBM4:255X:NSAA:MUSF:E4VM:ZI6W:CUN2:L4Z6:LSF4",
|
||||
"kty": "EC",
|
||||
"x": "3gAwX48IQ5oaYQAYSxor6rYYc_6yjuLCjtQ9LUakg4A",
|
||||
"y": "t72ge6kIA1XOjqjVoEOiPPAURltJFBMGDSQvEGVB010"
|
||||
},
|
||||
"alg": "ES256"
|
||||
},
|
||||
"signature": "XREm0L8WNn27Ga_iE_vRnTxVMhhYY0Zst_FfkKopg6gWSoTOZTuW4rK0fg_IqnKkEKlbD83tD46LKEGi5aIVFg",
|
||||
"protected": "eyJmb3JtYXRMZW5ndGgiOjY2MjgsImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAxNS0wNC0wOFQxODo1Mjo1OVoifQ"
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
def test_valid_manifest():
|
||||
manifest = DockerSchema1Manifest(MANIFEST_BYTES, validate=False)
|
||||
assert len(manifest.signatures) == 1
|
||||
assert manifest.namespace == ''
|
||||
assert manifest.repo_name == 'hello-world'
|
||||
assert manifest.tag == 'latest'
|
||||
assert manifest.image_ids == {'someid', 'anotherid'}
|
||||
assert manifest.parent_image_ids == {'anotherid'}
|
||||
|
||||
assert len(manifest.layers) == 2
|
||||
|
||||
assert manifest.layers[0].v1_metadata.image_id == 'anotherid'
|
||||
assert manifest.layers[0].v1_metadata.parent_image_id is None
|
||||
|
||||
assert manifest.layers[1].v1_metadata.image_id == 'someid'
|
||||
assert manifest.layers[1].v1_metadata.parent_image_id == 'anotherid'
|
||||
|
||||
assert manifest.leaf_layer == manifest.layers[1]
|
||||
|
||||
assert manifest.digest
|
Reference in a new issue