""" 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) assert layer_index < len(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