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:
parent
fc691cefb4
commit
71b7a2b3a2
7 changed files with 53 additions and 16 deletions
|
@ -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),
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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],
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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'])
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Reference in a new issue