Make command optional in schema 2 manifests (as per OCI spec) and pull out additional information

Also updates the manifest view page to show the comment or digest if there is no command defined
This commit is contained in:
Joseph Schorr 2018-12-11 17:23:39 -05:00
parent fc691cefb4
commit 71b7a2b3a2
7 changed files with 53 additions and 16 deletions

View file

@ -37,10 +37,12 @@ def _label_dict(label):
def _layer_dict(manifest_layer, index): def _layer_dict(manifest_layer, index):
# NOTE: The `command` in the layer is either a JSON string of an array (schema 1) or # NOTE: The `command` in the layer is either a JSON string of an array (schema 1) or
# a single string (schema 2). The block below normalizes it to have the same format. # a single string (schema 2). The block below normalizes it to have the same format.
try: command = None
command = json.loads(manifest_layer.command) if manifest_layer.command:
except (TypeError, ValueError): try:
command = [manifest_layer.command] command = json.loads(manifest_layer.command)
except (TypeError, ValueError):
command = [manifest_layer.command]
return { return {
'index': index, 'index': index,
@ -48,6 +50,8 @@ def _layer_dict(manifest_layer, index):
'is_remote': manifest_layer.is_remote, 'is_remote': manifest_layer.is_remote,
'urls': manifest_layer.urls, 'urls': manifest_layer.urls,
'command': command, 'command': command,
'comment': manifest_layer.comment,
'author': manifest_layer.author,
'blob_digest': str(manifest_layer.blob_digest), 'blob_digest': str(manifest_layer.blob_digest),
'created_datetime': format_date(manifest_layer.created_datetime), 'created_datetime': format_date(manifest_layer.created_datetime),
} }

View file

@ -82,7 +82,8 @@ class Schema1Layer(namedtuple('Schema1Layer', ['digest', 'v1_metadata', 'raw_v1_
class Schema1V1Metadata(namedtuple('Schema1V1Metadata', ['image_id', 'parent_image_id', 'created', class Schema1V1Metadata(namedtuple('Schema1V1Metadata', ['image_id', 'parent_image_id', 'created',
'comment', 'command', 'labels'])): 'comment', 'command', 'author',
'labels'])):
""" """
Represents the necessary data extracted from the v1 compatibility string in a given layer of a Represents the necessary data extracted from the v1 compatibility string in a given layer of a
Manifest. Manifest.
@ -315,6 +316,8 @@ class DockerSchema1Manifest(ManifestInterface):
is_remote=False, is_remote=False,
urls=None, urls=None,
command=layer.v1_metadata.command, command=layer.v1_metadata.command,
comment=layer.v1_metadata.comment,
author=layer.v1_metadata.author,
blob_digest=layer.digest, blob_digest=layer.digest,
created_datetime=created_datetime, created_datetime=created_datetime,
internal_layer=layer) internal_layer=layer)
@ -372,9 +375,13 @@ class DockerSchema1Manifest(ManifestInterface):
raise MalformedSchema1Manifest('id field missing from v1Compatibility JSON') raise MalformedSchema1Manifest('id field missing from v1Compatibility JSON')
labels = v1_metadata.get('config', {}).get('Labels', {}) or {} labels = v1_metadata.get('config', {}).get('Labels', {}) or {}
extracted = Schema1V1Metadata(v1_metadata['id'], v1_metadata.get('parent'), extracted = Schema1V1Metadata(image_id=v1_metadata['id'],
v1_metadata.get('created'), v1_metadata.get('comment'), parent_image_id=v1_metadata.get('parent'),
command, labels) created=v1_metadata.get('created'),
comment=v1_metadata.get('comment'),
author=v1_metadata.get('author'),
command=command,
labels=labels)
compressed_size = v1_metadata.get('Size') compressed_size = v1_metadata.get('Size')
yield Schema1Layer(image_digest, extracted, metadata_string, compressed_size, False, None) yield Schema1Layer(image_digest, extracted, metadata_string, compressed_size, False, None)
@ -483,6 +490,7 @@ class DockerSchema1Manifest(ManifestInterface):
image_id=working_image_id, image_id=working_image_id,
created=extracted_v1_metadata.created, created=extracted_v1_metadata.created,
comment=extracted_v1_metadata.comment, comment=extracted_v1_metadata.comment,
author=extracted_v1_metadata.author,
command=extracted_v1_metadata.command, command=extracted_v1_metadata.command,
compat_json=v1_metadata_json, compat_json=v1_metadata_json,
parent_image_id=parent_image_id, parent_image_id=parent_image_id,

View file

@ -108,12 +108,14 @@ DOCKER_SCHEMA2_CONFIG_HISTORY_KEY = "history"
DOCKER_SCHEMA2_CONFIG_ROOTFS_KEY = "rootfs" DOCKER_SCHEMA2_CONFIG_ROOTFS_KEY = "rootfs"
DOCKER_SCHEMA2_CONFIG_CREATED_KEY = "created" DOCKER_SCHEMA2_CONFIG_CREATED_KEY = "created"
DOCKER_SCHEMA2_CONFIG_CREATED_BY_KEY = "created_by" DOCKER_SCHEMA2_CONFIG_CREATED_BY_KEY = "created_by"
DOCKER_SCHEMA2_CONFIG_COMMENT_KEY = "comment"
DOCKER_SCHEMA2_CONFIG_AUTHOR_KEY = "author"
DOCKER_SCHEMA2_CONFIG_EMPTY_LAYER_KEY = "empty_layer" DOCKER_SCHEMA2_CONFIG_EMPTY_LAYER_KEY = "empty_layer"
DOCKER_SCHEMA2_CONFIG_TYPE_KEY = "type" DOCKER_SCHEMA2_CONFIG_TYPE_KEY = "type"
LayerHistory = namedtuple('LayerHistory', ['created', 'created_datetime', 'command', 'is_empty', LayerHistory = namedtuple('LayerHistory', ['created', 'created_datetime', 'command', 'is_empty',
'raw_entry']) 'author', 'comment', 'raw_entry'])
class MalformedSchema2Config(ManifestException): class MalformedSchema2Config(ManifestException):
@ -151,8 +153,15 @@ class DockerSchema2Config(object):
'description': 'The command used to create the layer', 'description': 'The command used to create the layer',
'x-example': '\/bin\/sh -c #(nop) ADD file:somesha in /', 'x-example': '\/bin\/sh -c #(nop) ADD file:somesha in /',
}, },
DOCKER_SCHEMA2_CONFIG_COMMENT_KEY: {
'type': 'string',
'description': 'Comment describing the layer',
},
DOCKER_SCHEMA2_CONFIG_AUTHOR_KEY: {
'type': 'string',
'description': 'The author of the layer',
},
}, },
'required': [DOCKER_SCHEMA2_CONFIG_CREATED_KEY, DOCKER_SCHEMA2_CONFIG_CREATED_BY_KEY],
'additionalProperties': True, 'additionalProperties': True,
}, },
}, },
@ -211,7 +220,7 @@ class DockerSchema2Config(object):
for history_entry in self._parsed[DOCKER_SCHEMA2_CONFIG_HISTORY_KEY]: for history_entry in self._parsed[DOCKER_SCHEMA2_CONFIG_HISTORY_KEY]:
if history_entry.get(DOCKER_SCHEMA2_CONFIG_EMPTY_LAYER_KEY, False): if history_entry.get(DOCKER_SCHEMA2_CONFIG_EMPTY_LAYER_KEY, False):
return True return True
return False return False
@property @property
@ -220,8 +229,10 @@ class DockerSchema2Config(object):
for history_entry in self._parsed[DOCKER_SCHEMA2_CONFIG_HISTORY_KEY]: for history_entry in self._parsed[DOCKER_SCHEMA2_CONFIG_HISTORY_KEY]:
created_datetime = parse_date(history_entry[DOCKER_SCHEMA2_CONFIG_CREATED_KEY]) created_datetime = parse_date(history_entry[DOCKER_SCHEMA2_CONFIG_CREATED_KEY])
yield LayerHistory(created_datetime=created_datetime, yield LayerHistory(created_datetime=created_datetime,
created=history_entry[DOCKER_SCHEMA2_CONFIG_CREATED_KEY], created=history_entry.get(DOCKER_SCHEMA2_CONFIG_CREATED_KEY),
command=history_entry[DOCKER_SCHEMA2_CONFIG_CREATED_BY_KEY], command=history_entry.get(DOCKER_SCHEMA2_CONFIG_CREATED_BY_KEY),
author=history_entry.get(DOCKER_SCHEMA2_CONFIG_AUTHOR_KEY),
comment=history_entry.get(DOCKER_SCHEMA2_CONFIG_COMMENT_KEY),
is_empty=history_entry.get(DOCKER_SCHEMA2_CONFIG_EMPTY_LAYER_KEY, False), is_empty=history_entry.get(DOCKER_SCHEMA2_CONFIG_EMPTY_LAYER_KEY, False),
raw_entry=history_entry) raw_entry=history_entry)
@ -235,9 +246,18 @@ class DockerSchema2Config(object):
if v1_parent_id is not None: if v1_parent_id is not None:
v1_compatibility['parent'] = v1_parent_id v1_compatibility['parent'] = v1_parent_id
if 'created' not in v1_compatibility: if 'created' not in v1_compatibility and history.created:
v1_compatibility['created'] = history.created v1_compatibility['created'] = history.created
if 'author' not in v1_compatibility and history.author:
v1_compatibility['author'] = history.author
if 'comment' not in v1_compatibility and history.comment:
v1_compatibility['comment'] = history.comment
if 'throwaway' not in v1_compatibility and history.is_empty:
v1_compatibility['throwaway'] = True
if 'container_config' not in v1_compatibility: if 'container_config' not in v1_compatibility:
v1_compatibility['container_config'] = { v1_compatibility['container_config'] = {
'Cmd': [history.command], 'Cmd': [history.command],

View file

@ -221,6 +221,8 @@ class DockerSchema2Manifest(ManifestInterface):
command=image_layer.history.command, command=image_layer.history.command,
blob_digest=image_layer.blob_digest, blob_digest=image_layer.blob_digest,
created_datetime=image_layer.history.created_datetime, created_datetime=image_layer.history.created_datetime,
author=image_layer.history.author,
comment=image_layer.history.comment,
internal_layer=image_layer) internal_layer=image_layer)
@property @property

View file

@ -3,4 +3,5 @@ from collections import namedtuple
ManifestImageLayer = namedtuple('ManifestImageLayer', ['layer_id', 'compressed_size', ManifestImageLayer = namedtuple('ManifestImageLayer', ['layer_id', 'compressed_size',
'is_remote', 'urls', 'command', 'is_remote', 'urls', 'command',
'blob_digest', 'created_datetime', 'blob_digest', 'created_datetime',
'author', 'comment',
'internal_layer']) 'internal_layer'])

View file

@ -9,7 +9,7 @@ from collections import namedtuple
class DockerV1Metadata(namedtuple('DockerV1Metadata', class DockerV1Metadata(namedtuple('DockerV1Metadata',
['namespace_name', 'repo_name', 'image_id', 'checksum', ['namespace_name', 'repo_name', 'image_id', 'checksum',
'content_checksum', 'created', 'comment', 'command', 'content_checksum', 'created', 'comment', 'command',
'parent_image_id', 'compat_json'])): 'author', 'parent_image_id', 'compat_json'])):
""" """
DockerV1Metadata represents all of the metadata for a given Docker v1 Image. DockerV1Metadata represents all of the metadata for a given Docker v1 Image.
The original form of the metadata is stored in the compat_json field. The original form of the metadata is stored in the compat_json field.

View file

@ -1,6 +1,8 @@
<div class="manifest-view-layer-element" ng-class="getClass()"> <div class="manifest-view-layer-element" ng-class="getClass()">
<div class="image-command"> <div class="image-command">
<image-command command="layer.command"></image-command> <image-command command="layer.command" ng-if="layer.command"></image-command>
<i ng-if="!layer.command && layer.comment">{{ layer.comment }}</i>
<code ng-if="!layer.command && !layer.comment">{{ layer.blob_digest }}</code>
</div> </div>
<div class="image-layer-dot"></div> <div class="image-layer-dot"></div>
<div class="image-layer-line"></div> <div class="image-layer-line"></div>