This repository has been archived on 2020-03-24. You can view files and clone it, but cannot push or open issues or pull requests.
quay/image/docker/schema2/config.py
Joseph Schorr e344d4a5cf Enhancements for Docker schema implementations in preparing for supporting schema 2 in the OCI model
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
2018-11-12 23:27:01 +02:00

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