e344d4a5cf
This adds additional required properties and methods to the Docker schema interface to allow us to treat both schema1 and schema2 manifests and lists logically equivalent from the OCI mode perspective
235 lines
7.3 KiB
Python
235 lines
7.3 KiB
Python
"""
|
|
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
|
|
|
|
from digest import digest_tools
|
|
|
|
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):
|
|
self._config_bytes = 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 digest(self):
|
|
""" Returns the digest of this config object. """
|
|
return digest_tools.sha256_digest(self._config_bytes)
|
|
|
|
@property
|
|
def size(self):
|
|
""" Returns the size of this config object. """
|
|
return len(self._config_bytes)
|
|
|
|
@property
|
|
def labels(self):
|
|
""" Returns a dictionary of all the labels defined in this configuration. """
|
|
return self._parsed.get('config', {}).get('Labels', {}) or {}
|
|
|
|
@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
|