Merge pull request #3322 from quay/further-unicode-fixes
Further fixes for unicode handling in manifests
This commit is contained in:
commit
d9da838df1
28 changed files with 275 additions and 106 deletions
|
@ -149,7 +149,7 @@ def _create_manifest(repository_id, manifest_interface_instance, storage):
|
||||||
manifest = Manifest.create(repository=repository_id,
|
manifest = Manifest.create(repository=repository_id,
|
||||||
digest=manifest_interface_instance.digest,
|
digest=manifest_interface_instance.digest,
|
||||||
media_type=media_type,
|
media_type=media_type,
|
||||||
manifest_bytes=manifest_interface_instance.bytes)
|
manifest_bytes=manifest_interface_instance.bytes.as_encoded_str())
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
manifest = Manifest.get(repository=repository_id, digest=manifest_interface_instance.digest)
|
manifest = Manifest.get(repository=repository_id, digest=manifest_interface_instance.digest)
|
||||||
return CreatedManifest(manifest=manifest, newly_created=False, labels_to_apply=None)
|
return CreatedManifest(manifest=manifest, newly_created=False, labels_to_apply=None)
|
||||||
|
|
|
@ -11,6 +11,7 @@ from data.model.oci.shared import get_legacy_image_for_manifest
|
||||||
from data.model import config
|
from data.model import config
|
||||||
from image.docker.schema1 import (DOCKER_SCHEMA1_CONTENT_TYPES, DockerSchema1Manifest,
|
from image.docker.schema1 import (DOCKER_SCHEMA1_CONTENT_TYPES, DockerSchema1Manifest,
|
||||||
MalformedSchema1Manifest)
|
MalformedSchema1Manifest)
|
||||||
|
from util.bytes import Bytes
|
||||||
from util.timedeltastring import convert_to_timedelta
|
from util.timedeltastring import convert_to_timedelta
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -215,7 +216,8 @@ def retarget_tag(tag_name, manifest_id, is_reversion=False, now_ms=None):
|
||||||
# name.
|
# name.
|
||||||
if manifest.media_type.name in DOCKER_SCHEMA1_CONTENT_TYPES:
|
if manifest.media_type.name in DOCKER_SCHEMA1_CONTENT_TYPES:
|
||||||
try:
|
try:
|
||||||
parsed = DockerSchema1Manifest(manifest.manifest_bytes, validate=False)
|
parsed = DockerSchema1Manifest(Bytes.for_string_or_unicode(manifest.manifest_bytes),
|
||||||
|
validate=False)
|
||||||
if parsed.tag != tag_name:
|
if parsed.tag != tag_name:
|
||||||
logger.error('Tried to re-target schema1 manifest with tag `%s` to tag `%s', parsed.tag,
|
logger.error('Tried to re-target schema1 manifest with tag `%s` to tag `%s', parsed.tag,
|
||||||
tag_name)
|
tag_name)
|
||||||
|
|
|
@ -18,6 +18,7 @@ from data.model.storage import get_layer_path
|
||||||
from image.docker.schema1 import DockerSchema1ManifestBuilder, DockerSchema1Manifest
|
from image.docker.schema1 import DockerSchema1ManifestBuilder, DockerSchema1Manifest
|
||||||
from image.docker.schema2.manifest import DockerSchema2ManifestBuilder
|
from image.docker.schema2.manifest import DockerSchema2ManifestBuilder
|
||||||
from image.docker.schema2.list import DockerSchema2ManifestListBuilder
|
from image.docker.schema2.list import DockerSchema2ManifestListBuilder
|
||||||
|
from util.bytes import Bytes
|
||||||
|
|
||||||
from test.fixtures import *
|
from test.fixtures import *
|
||||||
|
|
||||||
|
@ -163,7 +164,7 @@ def test_get_or_create_manifest(schema_version, initialized_db):
|
||||||
assert created is not None
|
assert created is not None
|
||||||
assert created.media_type.name == sample_manifest_instance.media_type
|
assert created.media_type.name == sample_manifest_instance.media_type
|
||||||
assert created.digest == sample_manifest_instance.digest
|
assert created.digest == sample_manifest_instance.digest
|
||||||
assert created.manifest_bytes == sample_manifest_instance.bytes
|
assert created.manifest_bytes == sample_manifest_instance.bytes.as_encoded_str()
|
||||||
assert created_manifest.labels_to_apply == expected_labels
|
assert created_manifest.labels_to_apply == expected_labels
|
||||||
|
|
||||||
# Verify the legacy image.
|
# Verify the legacy image.
|
||||||
|
@ -199,7 +200,8 @@ def test_get_or_create_manifest_invalid_image(initialized_db):
|
||||||
repository = get_repository('devtable', 'simple')
|
repository = get_repository('devtable', 'simple')
|
||||||
|
|
||||||
latest_tag = get_tag(repository, 'latest')
|
latest_tag = get_tag(repository, 'latest')
|
||||||
parsed = DockerSchema1Manifest(latest_tag.manifest.manifest_bytes, validate=False)
|
parsed = DockerSchema1Manifest(Bytes.for_string_or_unicode(latest_tag.manifest.manifest_bytes),
|
||||||
|
validate=False)
|
||||||
|
|
||||||
builder = DockerSchema1ManifestBuilder('devtable', 'simple', 'anothertag')
|
builder = DockerSchema1ManifestBuilder('devtable', 'simple', 'anothertag')
|
||||||
builder.add_layer(parsed.blob_digests[0], '{"id": "foo", "parent": "someinvalidimageid"}')
|
builder.add_layer(parsed.blob_digests[0], '{"id": "foo", "parent": "someinvalidimageid"}')
|
||||||
|
|
|
@ -793,7 +793,8 @@ def populate_manifest(repository, manifest, legacy_image, storage_ids):
|
||||||
with db_transaction():
|
with db_transaction():
|
||||||
try:
|
try:
|
||||||
manifest_row = Manifest.create(digest=manifest.digest, repository=repository,
|
manifest_row = Manifest.create(digest=manifest.digest, repository=repository,
|
||||||
manifest_bytes=manifest.bytes, media_type=media_type)
|
manifest_bytes=manifest.bytes.as_encoded_str(),
|
||||||
|
media_type=media_type)
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
return Manifest.get(repository=repository, digest=manifest.digest)
|
return Manifest.get(repository=repository, digest=manifest.digest)
|
||||||
|
|
||||||
|
|
|
@ -325,7 +325,7 @@ def test_store_tag_manifest(get_storages, initialized_db):
|
||||||
mapping_row = TagManifestToManifest.get(tag_manifest=tag_manifest)
|
mapping_row = TagManifestToManifest.get(tag_manifest=tag_manifest)
|
||||||
|
|
||||||
assert mapping_row.manifest is not None
|
assert mapping_row.manifest is not None
|
||||||
assert mapping_row.manifest.manifest_bytes == manifest.bytes
|
assert mapping_row.manifest.manifest_bytes == manifest.bytes.as_encoded_str()
|
||||||
assert mapping_row.manifest.digest == str(manifest.digest)
|
assert mapping_row.manifest.digest == str(manifest.digest)
|
||||||
|
|
||||||
blob_rows = {m.blob_id for m in
|
blob_rows = {m.blob_id for m in
|
||||||
|
|
|
@ -11,6 +11,7 @@ from image.docker import ManifestException
|
||||||
from image.docker.schemas import parse_manifest_from_bytes
|
from image.docker.schemas import parse_manifest_from_bytes
|
||||||
from image.docker.schema1 import DOCKER_SCHEMA1_SIGNED_MANIFEST_CONTENT_TYPE
|
from image.docker.schema1 import DOCKER_SCHEMA1_SIGNED_MANIFEST_CONTENT_TYPE
|
||||||
from image.docker.schema2 import DOCKER_SCHEMA2_MANIFESTLIST_CONTENT_TYPE
|
from image.docker.schema2 import DOCKER_SCHEMA2_MANIFESTLIST_CONTENT_TYPE
|
||||||
|
from util.bytes import Bytes
|
||||||
|
|
||||||
|
|
||||||
class RepositoryReference(datatype('Repository', [])):
|
class RepositoryReference(datatype('Repository', [])):
|
||||||
|
@ -176,7 +177,7 @@ class Tag(datatype('Tag', ['name', 'reversion', 'manifest_digest', 'lifetime_sta
|
||||||
return self._db_id
|
return self._db_id
|
||||||
|
|
||||||
|
|
||||||
class Manifest(datatype('Manifest', ['digest', 'media_type', 'manifest_bytes'])):
|
class Manifest(datatype('Manifest', ['digest', 'media_type', 'internal_manifest_bytes'])):
|
||||||
""" Manifest represents a manifest in a repository. """
|
""" Manifest represents a manifest in a repository. """
|
||||||
@classmethod
|
@classmethod
|
||||||
def for_tag_manifest(cls, tag_manifest, legacy_image=None):
|
def for_tag_manifest(cls, tag_manifest, legacy_image=None):
|
||||||
|
@ -184,7 +185,7 @@ class Manifest(datatype('Manifest', ['digest', 'media_type', 'manifest_bytes']))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return Manifest(db_id=tag_manifest.id, digest=tag_manifest.digest,
|
return Manifest(db_id=tag_manifest.id, digest=tag_manifest.digest,
|
||||||
manifest_bytes=tag_manifest.json_data,
|
internal_manifest_bytes=Bytes.for_string_or_unicode(tag_manifest.json_data),
|
||||||
media_type=DOCKER_SCHEMA1_SIGNED_MANIFEST_CONTENT_TYPE, # Always in legacy.
|
media_type=DOCKER_SCHEMA1_SIGNED_MANIFEST_CONTENT_TYPE, # Always in legacy.
|
||||||
inputs=dict(legacy_image=legacy_image, tag_manifest=True))
|
inputs=dict(legacy_image=legacy_image, tag_manifest=True))
|
||||||
|
|
||||||
|
@ -195,7 +196,7 @@ class Manifest(datatype('Manifest', ['digest', 'media_type', 'manifest_bytes']))
|
||||||
|
|
||||||
return Manifest(db_id=manifest.id,
|
return Manifest(db_id=manifest.id,
|
||||||
digest=manifest.digest,
|
digest=manifest.digest,
|
||||||
manifest_bytes=manifest.manifest_bytes,
|
internal_manifest_bytes=Bytes.for_string_or_unicode(manifest.manifest_bytes),
|
||||||
media_type=manifest.media_type.name,
|
media_type=manifest.media_type.name,
|
||||||
inputs=dict(legacy_image=legacy_image, tag_manifest=False))
|
inputs=dict(legacy_image=legacy_image, tag_manifest=False))
|
||||||
|
|
||||||
|
@ -221,8 +222,8 @@ class Manifest(datatype('Manifest', ['digest', 'media_type', 'manifest_bytes']))
|
||||||
|
|
||||||
def get_parsed_manifest(self, validate=True):
|
def get_parsed_manifest(self, validate=True):
|
||||||
""" Returns the parsed manifest for this manifest. """
|
""" Returns the parsed manifest for this manifest. """
|
||||||
validate = False # Temporarily disable.
|
return parse_manifest_from_bytes(self.internal_manifest_bytes, self.media_type,
|
||||||
return parse_manifest_from_bytes(self.manifest_bytes, self.media_type, validate=validate)
|
validate=validate)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def layers_compressed_size(self):
|
def layers_compressed_size(self):
|
||||||
|
|
|
@ -15,16 +15,20 @@ from data import model
|
||||||
from data.database import (TagManifestLabelMap, TagManifestToManifest, Manifest, ManifestBlob,
|
from data.database import (TagManifestLabelMap, TagManifestToManifest, Manifest, ManifestBlob,
|
||||||
ManifestLegacyImage, ManifestLabel, TagManifest, RepositoryTag, Image,
|
ManifestLegacyImage, ManifestLabel, TagManifest, RepositoryTag, Image,
|
||||||
TagManifestLabel, TagManifest, TagManifestLabel, DerivedStorageForImage,
|
TagManifestLabel, TagManifest, TagManifestLabel, DerivedStorageForImage,
|
||||||
TorrentInfo, Tag, TagToRepositoryTag, close_db_filter)
|
TorrentInfo, Tag, TagToRepositoryTag, close_db_filter,
|
||||||
|
ImageStorageLocation)
|
||||||
from data.cache.impl import InMemoryDataModelCache
|
from data.cache.impl import InMemoryDataModelCache
|
||||||
from data.registry_model.registry_pre_oci_model import PreOCIModel
|
from data.registry_model.registry_pre_oci_model import PreOCIModel
|
||||||
from data.registry_model.registry_oci_model import OCIModel
|
from data.registry_model.registry_oci_model import OCIModel
|
||||||
from data.registry_model.datatypes import RepositoryReference
|
from data.registry_model.datatypes import RepositoryReference
|
||||||
from data.registry_model.blobuploader import upload_blob, BlobUploadSettings
|
from data.registry_model.blobuploader import upload_blob, BlobUploadSettings
|
||||||
from data.registry_model.modelsplitter import SplitModel
|
from data.registry_model.modelsplitter import SplitModel
|
||||||
|
from data.model.blob import store_blob_record_and_temp_link
|
||||||
from image.docker.types import ManifestImageLayer
|
from image.docker.types import ManifestImageLayer
|
||||||
from image.docker.schema1 import DockerSchema1ManifestBuilder, DOCKER_SCHEMA1_CONTENT_TYPES
|
from image.docker.schema1 import (DockerSchema1ManifestBuilder, DOCKER_SCHEMA1_CONTENT_TYPES,
|
||||||
|
DockerSchema1Manifest)
|
||||||
from image.docker.schema2.manifest import DockerSchema2ManifestBuilder
|
from image.docker.schema2.manifest import DockerSchema2ManifestBuilder
|
||||||
|
from util.bytes import Bytes
|
||||||
|
|
||||||
from test.fixtures import *
|
from test.fixtures import *
|
||||||
|
|
||||||
|
@ -823,3 +827,40 @@ def test_create_manifest_and_retarget_tag_with_labels(registry_model):
|
||||||
|
|
||||||
# Ensure the labels were applied.
|
# Ensure the labels were applied.
|
||||||
assert tag.lifetime_end_ms is not None
|
assert tag.lifetime_end_ms is not None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def _populate_blob(digest):
|
||||||
|
location = ImageStorageLocation.get(name='local_us')
|
||||||
|
store_blob_record_and_temp_link('devtable', 'simple', digest, location, 1, 120)
|
||||||
|
|
||||||
|
|
||||||
|
def test_known_issue_schema1(registry_model):
|
||||||
|
test_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
path = os.path.join(test_dir, '../../../image/docker/test/validate_manifest_known_issue.json')
|
||||||
|
with open(path, 'r') as f:
|
||||||
|
manifest_bytes = f.read()
|
||||||
|
|
||||||
|
manifest = DockerSchema1Manifest(Bytes.for_string_or_unicode(manifest_bytes))
|
||||||
|
|
||||||
|
for blob_digest in manifest.local_blob_digests:
|
||||||
|
_populate_blob(blob_digest)
|
||||||
|
|
||||||
|
digest = manifest.digest
|
||||||
|
assert digest == 'sha256:44518f5a4d1cb5b7a6347763116fb6e10f6a8563b6c40bb389a0a982f0a9f47a'
|
||||||
|
|
||||||
|
# Create the manifest in the database.
|
||||||
|
repository_ref = registry_model.lookup_repository('devtable', 'simple')
|
||||||
|
created_manifest, _ = registry_model.create_manifest_and_retarget_tag(repository_ref, manifest,
|
||||||
|
'latest', storage)
|
||||||
|
assert created_manifest
|
||||||
|
assert created_manifest.digest == manifest.digest
|
||||||
|
assert (created_manifest.internal_manifest_bytes.as_encoded_str() ==
|
||||||
|
manifest.bytes.as_encoded_str())
|
||||||
|
|
||||||
|
# Look it up again and validate.
|
||||||
|
found = registry_model.lookup_manifest_by_digest(repository_ref, manifest.digest, allow_dead=True)
|
||||||
|
assert found
|
||||||
|
assert found.digest == digest
|
||||||
|
assert found.internal_manifest_bytes.as_encoded_str() == manifest.bytes.as_encoded_str()
|
||||||
|
assert found.get_parsed_manifest().digest == digest
|
||||||
|
|
|
@ -72,7 +72,7 @@ def _manifest_dict(manifest):
|
||||||
return {
|
return {
|
||||||
'digest': manifest.digest,
|
'digest': manifest.digest,
|
||||||
'is_manifest_list': manifest.is_manifest_list,
|
'is_manifest_list': manifest.is_manifest_list,
|
||||||
'manifest_data': manifest.manifest_bytes,
|
'manifest_data': manifest.internal_manifest_bytes.as_unicode(),
|
||||||
'image': image,
|
'image': image,
|
||||||
'layers': ([_layer_dict(lyr.layer_info, idx) for idx, lyr in enumerate(layers)]
|
'layers': ([_layer_dict(lyr.layer_info, idx) for idx, lyr in enumerate(layers)]
|
||||||
if layers else None),
|
if layers else None),
|
||||||
|
|
|
@ -39,7 +39,7 @@ def _tag_dict(tag):
|
||||||
tag_info['manifest_digest'] = tag.manifest_digest
|
tag_info['manifest_digest'] = tag.manifest_digest
|
||||||
if tag.manifest:
|
if tag.manifest:
|
||||||
try:
|
try:
|
||||||
tag_info['manifest'] = json.loads(tag.manifest.manifest_bytes)
|
tag_info['manifest'] = json.loads(tag.manifest.internal_manifest_bytes.as_unicode())
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ from image.docker.schema2 import DOCKER_SCHEMA2_CONTENT_TYPES, OCI_CONTENT_TYPES
|
||||||
from image.docker.schemas import parse_manifest_from_bytes
|
from image.docker.schemas import parse_manifest_from_bytes
|
||||||
from notifications import spawn_notification
|
from notifications import spawn_notification
|
||||||
from util.audit import track_and_log
|
from util.audit import track_and_log
|
||||||
|
from util.bytes import Bytes
|
||||||
from util.names import VALID_TAG_PATTERN
|
from util.names import VALID_TAG_PATTERN
|
||||||
from util.registry.replication import queue_replication_batch
|
from util.registry.replication import queue_replication_batch
|
||||||
|
|
||||||
|
@ -72,7 +73,7 @@ def fetch_manifest_by_tagname(namespace_name, repo_name, manifest_ref):
|
||||||
metric_queue.repository_pull.Inc(labelvalues=[namespace_name, repo_name, 'v2', True])
|
metric_queue.repository_pull.Inc(labelvalues=[namespace_name, repo_name, 'v2', True])
|
||||||
|
|
||||||
return Response(
|
return Response(
|
||||||
supported.bytes,
|
supported.bytes.as_unicode(),
|
||||||
status=200,
|
status=200,
|
||||||
headers={
|
headers={
|
||||||
'Content-Type': supported.media_type,
|
'Content-Type': supported.media_type,
|
||||||
|
@ -109,7 +110,7 @@ def fetch_manifest_by_digest(namespace_name, repo_name, manifest_ref):
|
||||||
track_and_log('pull_repo', repository_ref, manifest_digest=manifest_ref)
|
track_and_log('pull_repo', repository_ref, manifest_digest=manifest_ref)
|
||||||
metric_queue.repository_pull.Inc(labelvalues=[namespace_name, repo_name, 'v2', True])
|
metric_queue.repository_pull.Inc(labelvalues=[namespace_name, repo_name, 'v2', True])
|
||||||
|
|
||||||
return Response(supported.bytes, status=200, headers={
|
return Response(supported.bytes.as_unicode(), status=200, headers={
|
||||||
'Content-Type': supported.media_type,
|
'Content-Type': supported.media_type,
|
||||||
'Docker-Content-Digest': supported.digest,
|
'Docker-Content-Digest': supported.digest,
|
||||||
})
|
})
|
||||||
|
@ -214,7 +215,7 @@ def _parse_manifest():
|
||||||
content_type = DOCKER_SCHEMA1_MANIFEST_CONTENT_TYPE
|
content_type = DOCKER_SCHEMA1_MANIFEST_CONTENT_TYPE
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return parse_manifest_from_bytes(request.data, content_type)
|
return parse_manifest_from_bytes(Bytes.for_string_or_unicode(request.data), content_type)
|
||||||
except ManifestException as me:
|
except ManifestException as me:
|
||||||
logger.exception("failed to parse manifest when writing by tagname")
|
logger.exception("failed to parse manifest when writing by tagname")
|
||||||
raise ManifestInvalid(detail={'message': 'failed to parse manifest: %s' % me.message})
|
raise ManifestInvalid(detail={'message': 'failed to parse manifest: %s' % me.message})
|
||||||
|
|
|
@ -23,7 +23,8 @@ from image.docker import ManifestException
|
||||||
from image.docker.types import ManifestImageLayer
|
from image.docker.types import ManifestImageLayer
|
||||||
from image.docker.interfaces import ManifestInterface
|
from image.docker.interfaces import ManifestInterface
|
||||||
from image.docker.v1 import DockerV1Metadata
|
from image.docker.v1 import DockerV1Metadata
|
||||||
from image.docker.schemautil import ensure_utf8, to_canonical_json
|
from image.docker.schemautil import to_canonical_json
|
||||||
|
from util.bytes import Bytes
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -160,11 +161,13 @@ class DockerSchema1Manifest(ManifestInterface):
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, manifest_bytes, validate=True):
|
def __init__(self, manifest_bytes, validate=True):
|
||||||
|
assert isinstance(manifest_bytes, Bytes)
|
||||||
|
|
||||||
self._layers = None
|
self._layers = None
|
||||||
self._bytes = manifest_bytes
|
self._bytes = manifest_bytes
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._parsed = json.loads(manifest_bytes)
|
self._parsed = json.loads(manifest_bytes.as_encoded_str())
|
||||||
except ValueError as ve:
|
except ValueError as ve:
|
||||||
raise MalformedSchema1Manifest('malformed manifest data: %s' % ve)
|
raise MalformedSchema1Manifest('malformed manifest data: %s' % ve)
|
||||||
|
|
||||||
|
@ -193,13 +196,13 @@ class DockerSchema1Manifest(ManifestInterface):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def for_latin1_bytes(cls, encoded_bytes, validate=True):
|
def for_latin1_bytes(cls, encoded_bytes, validate=True):
|
||||||
return DockerSchema1Manifest(encoded_bytes.encode('utf-8'), validate)
|
return DockerSchema1Manifest(Bytes.for_string_or_unicode(encoded_bytes), validate)
|
||||||
|
|
||||||
def _validate(self):
|
def _validate(self):
|
||||||
if not self._signatures:
|
if not self._signatures:
|
||||||
return
|
return
|
||||||
|
|
||||||
payload_str = ensure_utf8(self._payload)
|
payload_str = self._payload
|
||||||
for signature in self._signatures:
|
for signature in self._signatures:
|
||||||
bytes_to_verify = '{0}.{1}'.format(signature['protected'], base64url_encode(payload_str))
|
bytes_to_verify = '{0}.{1}'.format(signature['protected'], base64url_encode(payload_str))
|
||||||
signer = SIGNER_ALGS[signature['header']['alg']]
|
signer = SIGNER_ALGS[signature['header']['alg']]
|
||||||
|
@ -248,10 +251,6 @@ class DockerSchema1Manifest(ManifestInterface):
|
||||||
def tag(self):
|
def tag(self):
|
||||||
return self._tag
|
return self._tag
|
||||||
|
|
||||||
@property
|
|
||||||
def json(self):
|
|
||||||
return self._bytes
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def bytes(self):
|
def bytes(self):
|
||||||
return self._bytes
|
return self._bytes
|
||||||
|
@ -270,7 +269,7 @@ class DockerSchema1Manifest(ManifestInterface):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def digest(self):
|
def digest(self):
|
||||||
return digest_tools.sha256_digest(ensure_utf8(self._payload))
|
return digest_tools.sha256_digest(self._payload)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def image_ids(self):
|
def image_ids(self):
|
||||||
|
@ -395,11 +394,12 @@ class DockerSchema1Manifest(ManifestInterface):
|
||||||
@property
|
@property
|
||||||
def _payload(self):
|
def _payload(self):
|
||||||
if self._signatures is None:
|
if self._signatures is None:
|
||||||
return self._bytes
|
return self._bytes.as_encoded_str()
|
||||||
|
|
||||||
|
byte_data = self._bytes.as_encoded_str()
|
||||||
protected = str(self._signatures[0][DOCKER_SCHEMA1_PROTECTED_KEY])
|
protected = str(self._signatures[0][DOCKER_SCHEMA1_PROTECTED_KEY])
|
||||||
parsed_protected = json.loads(base64url_decode(protected))
|
parsed_protected = json.loads(base64url_decode(protected))
|
||||||
signed_content_head = self._bytes[:parsed_protected[DOCKER_SCHEMA1_FORMAT_LENGTH_KEY]]
|
signed_content_head = byte_data[:parsed_protected[DOCKER_SCHEMA1_FORMAT_LENGTH_KEY]]
|
||||||
signed_content_tail = base64url_decode(str(parsed_protected[DOCKER_SCHEMA1_FORMAT_TAIL_KEY]))
|
signed_content_tail = base64url_decode(str(parsed_protected[DOCKER_SCHEMA1_FORMAT_TAIL_KEY]))
|
||||||
return signed_content_head + signed_content_tail
|
return signed_content_head + signed_content_tail
|
||||||
|
|
||||||
|
@ -548,8 +548,9 @@ class DockerSchema1ManifestBuilder(object):
|
||||||
|
|
||||||
payload_str = json.dumps(payload, indent=3, ensure_ascii=ensure_ascii)
|
payload_str = json.dumps(payload, indent=3, ensure_ascii=ensure_ascii)
|
||||||
if json_web_key is None:
|
if json_web_key is None:
|
||||||
return DockerSchema1Manifest(payload_str)
|
return DockerSchema1Manifest(Bytes.for_string_or_unicode(payload_str))
|
||||||
|
|
||||||
|
payload_str = Bytes.for_string_or_unicode(payload_str).as_encoded_str()
|
||||||
split_point = payload_str.rfind('\n}')
|
split_point = payload_str.rfind('\n}')
|
||||||
|
|
||||||
protected_payload = {
|
protected_payload = {
|
||||||
|
@ -560,7 +561,6 @@ class DockerSchema1ManifestBuilder(object):
|
||||||
protected = base64url_encode(json.dumps(protected_payload, ensure_ascii=ensure_ascii))
|
protected = base64url_encode(json.dumps(protected_payload, ensure_ascii=ensure_ascii))
|
||||||
logger.debug('Generated protected block: %s', protected)
|
logger.debug('Generated protected block: %s', protected)
|
||||||
|
|
||||||
payload_str = ensure_utf8(payload_str)
|
|
||||||
bytes_to_sign = '{0}.{1}'.format(protected, base64url_encode(payload_str))
|
bytes_to_sign = '{0}.{1}'.format(protected, base64url_encode(payload_str))
|
||||||
|
|
||||||
signer = SIGNER_ALGS[_JWS_SIGNING_ALGORITHM]
|
signer = SIGNER_ALGS[_JWS_SIGNING_ALGORITHM]
|
||||||
|
@ -579,7 +579,9 @@ class DockerSchema1ManifestBuilder(object):
|
||||||
|
|
||||||
logger.debug('Encoded signature block: %s', json.dumps(signature_block))
|
logger.debug('Encoded signature block: %s', json.dumps(signature_block))
|
||||||
payload.update({DOCKER_SCHEMA1_SIGNATURES_KEY: [signature_block]})
|
payload.update({DOCKER_SCHEMA1_SIGNATURES_KEY: [signature_block]})
|
||||||
return DockerSchema1Manifest(json.dumps(payload, indent=3, ensure_ascii=ensure_ascii))
|
|
||||||
|
json_str = json.dumps(payload, indent=3, ensure_ascii=ensure_ascii)
|
||||||
|
return DockerSchema1Manifest(Bytes.for_string_or_unicode(json_str))
|
||||||
|
|
||||||
|
|
||||||
def _updated_v1_metadata(v1_metadata_json, updated_id_map):
|
def _updated_v1_metadata(v1_metadata_json, updated_id_map):
|
||||||
|
|
|
@ -102,7 +102,7 @@ from dateutil.parser import parse as parse_date
|
||||||
|
|
||||||
from digest import digest_tools
|
from digest import digest_tools
|
||||||
from image.docker import ManifestException
|
from image.docker import ManifestException
|
||||||
from image.docker.schemautil import ensure_utf8
|
from util.bytes import Bytes
|
||||||
|
|
||||||
|
|
||||||
DOCKER_SCHEMA2_CONFIG_HISTORY_KEY = "history"
|
DOCKER_SCHEMA2_CONFIG_HISTORY_KEY = "history"
|
||||||
|
@ -183,10 +183,12 @@ class DockerSchema2Config(object):
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, config_bytes):
|
def __init__(self, config_bytes):
|
||||||
|
assert isinstance(config_bytes, Bytes)
|
||||||
|
|
||||||
self._config_bytes = config_bytes
|
self._config_bytes = config_bytes
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._parsed = json.loads(ensure_utf8(config_bytes))
|
self._parsed = json.loads(config_bytes.as_unicode())
|
||||||
except ValueError as ve:
|
except ValueError as ve:
|
||||||
raise MalformedSchema2Config('malformed config data: %s' % ve)
|
raise MalformedSchema2Config('malformed config data: %s' % ve)
|
||||||
|
|
||||||
|
@ -198,12 +200,12 @@ class DockerSchema2Config(object):
|
||||||
@property
|
@property
|
||||||
def digest(self):
|
def digest(self):
|
||||||
""" Returns the digest of this config object. """
|
""" Returns the digest of this config object. """
|
||||||
return digest_tools.sha256_digest(ensure_utf8(self._config_bytes))
|
return digest_tools.sha256_digest(self._config_bytes.as_encoded_str())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def size(self):
|
def size(self):
|
||||||
""" Returns the size of this config object. """
|
""" Returns the size of this config object. """
|
||||||
return len(ensure_utf8(self._config_bytes))
|
return len(self._config_bytes.as_encoded_str())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def bytes(self):
|
def bytes(self):
|
||||||
|
|
|
@ -12,7 +12,7 @@ from image.docker.schema1 import DockerSchema1Manifest
|
||||||
from image.docker.schema2 import (DOCKER_SCHEMA2_MANIFESTLIST_CONTENT_TYPE,
|
from image.docker.schema2 import (DOCKER_SCHEMA2_MANIFESTLIST_CONTENT_TYPE,
|
||||||
DOCKER_SCHEMA2_MANIFEST_CONTENT_TYPE)
|
DOCKER_SCHEMA2_MANIFEST_CONTENT_TYPE)
|
||||||
from image.docker.schema2.manifest import DockerSchema2Manifest
|
from image.docker.schema2.manifest import DockerSchema2Manifest
|
||||||
from image.docker.schemautil import ensure_utf8
|
from util.bytes import Bytes
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -67,10 +67,10 @@ class LazyManifestLoader(object):
|
||||||
|
|
||||||
content_type = self._manifest_data[DOCKER_SCHEMA2_MANIFESTLIST_MEDIATYPE_KEY]
|
content_type = self._manifest_data[DOCKER_SCHEMA2_MANIFESTLIST_MEDIATYPE_KEY]
|
||||||
if content_type == DOCKER_SCHEMA2_MANIFEST_CONTENT_TYPE:
|
if content_type == DOCKER_SCHEMA2_MANIFEST_CONTENT_TYPE:
|
||||||
return DockerSchema2Manifest(manifest_bytes)
|
return DockerSchema2Manifest(Bytes.for_string_or_unicode(manifest_bytes))
|
||||||
|
|
||||||
if content_type == DOCKER_SCHEMA1_MANIFEST_CONTENT_TYPE:
|
if content_type == DOCKER_SCHEMA1_MANIFEST_CONTENT_TYPE:
|
||||||
return DockerSchema1Manifest(manifest_bytes, validate=False)
|
return DockerSchema1Manifest(Bytes.for_string_or_unicode(manifest_bytes), validate=False)
|
||||||
|
|
||||||
raise MalformedSchema2ManifestList('Unknown manifest content type')
|
raise MalformedSchema2ManifestList('Unknown manifest content type')
|
||||||
|
|
||||||
|
@ -171,11 +171,13 @@ class DockerSchema2ManifestList(ManifestInterface):
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, manifest_bytes):
|
def __init__(self, manifest_bytes):
|
||||||
|
assert isinstance(manifest_bytes, Bytes)
|
||||||
|
|
||||||
self._layers = None
|
self._layers = None
|
||||||
self._manifest_bytes = manifest_bytes
|
self._manifest_bytes = manifest_bytes
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._parsed = json.loads(ensure_utf8(manifest_bytes))
|
self._parsed = json.loads(manifest_bytes.as_unicode())
|
||||||
except ValueError as ve:
|
except ValueError as ve:
|
||||||
raise MalformedSchema2ManifestList('malformed manifest data: %s' % ve)
|
raise MalformedSchema2ManifestList('malformed manifest data: %s' % ve)
|
||||||
|
|
||||||
|
@ -196,7 +198,7 @@ class DockerSchema2ManifestList(ManifestInterface):
|
||||||
@property
|
@property
|
||||||
def digest(self):
|
def digest(self):
|
||||||
""" The digest of the manifest, including type prefix. """
|
""" The digest of the manifest, including type prefix. """
|
||||||
return digest_tools.sha256_digest(ensure_utf8(self._manifest_bytes))
|
return digest_tools.sha256_digest(self._manifest_bytes.as_encoded_str())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_type(self):
|
def media_type(self):
|
||||||
|
@ -319,7 +321,9 @@ class DockerSchema2ManifestListBuilder(object):
|
||||||
def add_manifest(self, manifest, architecture, os):
|
def add_manifest(self, manifest, architecture, os):
|
||||||
""" Adds a manifest to the list. """
|
""" Adds a manifest to the list. """
|
||||||
manifest = manifest.unsigned() # Make sure we add the unsigned version to the list.
|
manifest = manifest.unsigned() # Make sure we add the unsigned version to the list.
|
||||||
self.add_manifest_digest(manifest.digest, len(manifest.bytes), manifest.media_type,
|
self.add_manifest_digest(manifest.digest,
|
||||||
|
len(manifest.bytes.as_encoded_str()),
|
||||||
|
manifest.media_type,
|
||||||
architecture, os)
|
architecture, os)
|
||||||
|
|
||||||
def add_manifest_digest(self, manifest_digest, manifest_size, media_type, architecture, os):
|
def add_manifest_digest(self, manifest_digest, manifest_size, media_type, architecture, os):
|
||||||
|
@ -345,4 +349,6 @@ class DockerSchema2ManifestListBuilder(object):
|
||||||
} for manifest in self.manifests
|
} for manifest in self.manifests
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
return DockerSchema2ManifestList(json.dumps(manifest_list_dict, indent=3))
|
|
||||||
|
json_str = Bytes.for_string_or_unicode(json.dumps(manifest_list_dict, indent=3))
|
||||||
|
return DockerSchema2ManifestList(json_str)
|
||||||
|
|
|
@ -16,7 +16,7 @@ from image.docker.schema2 import (DOCKER_SCHEMA2_MANIFEST_CONTENT_TYPE,
|
||||||
EMPTY_LAYER_BLOB_DIGEST, EMPTY_LAYER_SIZE)
|
EMPTY_LAYER_BLOB_DIGEST, EMPTY_LAYER_SIZE)
|
||||||
from image.docker.schema1 import DockerSchema1ManifestBuilder
|
from image.docker.schema1 import DockerSchema1ManifestBuilder
|
||||||
from image.docker.schema2.config import DockerSchema2Config
|
from image.docker.schema2.config import DockerSchema2Config
|
||||||
from image.docker.schemautil import ensure_utf8
|
from util.bytes import Bytes
|
||||||
|
|
||||||
# Keys.
|
# Keys.
|
||||||
DOCKER_SCHEMA2_MANIFEST_VERSION_KEY = 'schemaVersion'
|
DOCKER_SCHEMA2_MANIFEST_VERSION_KEY = 'schemaVersion'
|
||||||
|
@ -129,13 +129,15 @@ class DockerSchema2Manifest(ManifestInterface):
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, manifest_bytes):
|
def __init__(self, manifest_bytes):
|
||||||
|
assert isinstance(manifest_bytes, Bytes)
|
||||||
|
|
||||||
self._payload = manifest_bytes
|
self._payload = manifest_bytes
|
||||||
|
|
||||||
self._filesystem_layers = None
|
self._filesystem_layers = None
|
||||||
self._cached_built_config = None
|
self._cached_built_config = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._parsed = json.loads(ensure_utf8(self._payload))
|
self._parsed = json.loads(self._payload.as_unicode())
|
||||||
except ValueError as ve:
|
except ValueError as ve:
|
||||||
raise MalformedSchema2Manifest('malformed manifest data: %s' % ve)
|
raise MalformedSchema2Manifest('malformed manifest data: %s' % ve)
|
||||||
|
|
||||||
|
@ -166,7 +168,7 @@ class DockerSchema2Manifest(ManifestInterface):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def digest(self):
|
def digest(self):
|
||||||
return digest_tools.sha256_digest(ensure_utf8(self._payload))
|
return digest_tools.sha256_digest(self._payload.as_encoded_str())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def config(self):
|
def config(self):
|
||||||
|
@ -365,7 +367,7 @@ class DockerSchema2Manifest(ManifestInterface):
|
||||||
self.config.size)
|
self.config.size)
|
||||||
raise MalformedSchema2Manifest(msg)
|
raise MalformedSchema2Manifest(msg)
|
||||||
|
|
||||||
self._cached_built_config = DockerSchema2Config(config_bytes)
|
self._cached_built_config = DockerSchema2Config(Bytes.for_string_or_unicode(config_bytes))
|
||||||
return self._cached_built_config
|
return self._cached_built_config
|
||||||
|
|
||||||
def _generate_filesystem_layers(self):
|
def _generate_filesystem_layers(self):
|
||||||
|
@ -446,4 +448,6 @@ class DockerSchema2ManifestBuilder(object):
|
||||||
_build_layer(layer) for layer in self.filesystem_layers
|
_build_layer(layer) for layer in self.filesystem_layers
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
return DockerSchema2Manifest(json.dumps(manifest_dict, ensure_ascii=ensure_ascii, indent=3))
|
|
||||||
|
json_str = json.dumps(manifest_dict, ensure_ascii=ensure_ascii, indent=3)
|
||||||
|
return DockerSchema2Manifest(Bytes.for_string_or_unicode(json_str))
|
||||||
|
|
|
@ -2,6 +2,7 @@ import json
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from image.docker.schema2.config import MalformedSchema2Config, DockerSchema2Config
|
from image.docker.schema2.config import MalformedSchema2Config, DockerSchema2Config
|
||||||
|
from util.bytes import Bytes
|
||||||
|
|
||||||
@pytest.mark.parametrize('json_data', [
|
@pytest.mark.parametrize('json_data', [
|
||||||
'',
|
'',
|
||||||
|
@ -14,7 +15,7 @@ from image.docker.schema2.config import MalformedSchema2Config, DockerSchema2Con
|
||||||
])
|
])
|
||||||
def test_malformed_configs(json_data):
|
def test_malformed_configs(json_data):
|
||||||
with pytest.raises(MalformedSchema2Config):
|
with pytest.raises(MalformedSchema2Config):
|
||||||
DockerSchema2Config(json_data)
|
DockerSchema2Config(Bytes.for_string_or_unicode(json_data))
|
||||||
|
|
||||||
CONFIG_BYTES = json.dumps({
|
CONFIG_BYTES = json.dumps({
|
||||||
"architecture": "amd64",
|
"architecture": "amd64",
|
||||||
|
@ -106,7 +107,7 @@ CONFIG_BYTES = json.dumps({
|
||||||
})
|
})
|
||||||
|
|
||||||
def test_valid_config():
|
def test_valid_config():
|
||||||
config = DockerSchema2Config(CONFIG_BYTES)
|
config = DockerSchema2Config(Bytes.for_string_or_unicode(CONFIG_BYTES))
|
||||||
history = list(config.history)
|
history = list(config.history)
|
||||||
assert len(history) == 4
|
assert len(history) == 4
|
||||||
|
|
||||||
|
|
|
@ -6,12 +6,14 @@ import pytest
|
||||||
from image.docker.schema1 import DockerSchema1Manifest, DOCKER_SCHEMA1_CONTENT_TYPES
|
from image.docker.schema1 import DockerSchema1Manifest, DOCKER_SCHEMA1_CONTENT_TYPES
|
||||||
from image.docker.schema2.manifest import DockerSchema2Manifest
|
from image.docker.schema2.manifest import DockerSchema2Manifest
|
||||||
from image.docker.schemautil import ContentRetrieverForTesting
|
from image.docker.schemautil import ContentRetrieverForTesting
|
||||||
|
from util.bytes import Bytes
|
||||||
|
|
||||||
|
|
||||||
def _get_test_file_contents(test_name, kind):
|
def _get_test_file_contents(test_name, kind):
|
||||||
filename = '%s.%s.json' % (test_name, kind)
|
filename = '%s.%s.json' % (test_name, kind)
|
||||||
data_dir = os.path.dirname(__file__)
|
data_dir = os.path.dirname(__file__)
|
||||||
with open(os.path.join(data_dir, 'conversion_data', filename), 'r') as f:
|
with open(os.path.join(data_dir, 'conversion_data', filename), 'r') as f:
|
||||||
return f.read()
|
return Bytes.for_string_or_unicode(f.read())
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('name, config_sha', [
|
@pytest.mark.parametrize('name, config_sha', [
|
||||||
|
@ -21,7 +23,7 @@ def _get_test_file_contents(test_name, kind):
|
||||||
])
|
])
|
||||||
def test_legacy_layers(name, config_sha):
|
def test_legacy_layers(name, config_sha):
|
||||||
cr = {}
|
cr = {}
|
||||||
cr[config_sha] = _get_test_file_contents(name, 'config')
|
cr[config_sha] = _get_test_file_contents(name, 'config').as_encoded_str()
|
||||||
retriever = ContentRetrieverForTesting(cr)
|
retriever = ContentRetrieverForTesting(cr)
|
||||||
|
|
||||||
schema2 = DockerSchema2Manifest(_get_test_file_contents(name, 'schema2'))
|
schema2 = DockerSchema2Manifest(_get_test_file_contents(name, 'schema2'))
|
||||||
|
@ -47,7 +49,7 @@ def test_legacy_layers(name, config_sha):
|
||||||
])
|
])
|
||||||
def test_conversion(name, config_sha):
|
def test_conversion(name, config_sha):
|
||||||
cr = {}
|
cr = {}
|
||||||
cr[config_sha] = _get_test_file_contents(name, 'config')
|
cr[config_sha] = _get_test_file_contents(name, 'config').as_encoded_str()
|
||||||
retriever = ContentRetrieverForTesting(cr)
|
retriever = ContentRetrieverForTesting(cr)
|
||||||
|
|
||||||
schema2 = DockerSchema2Manifest(_get_test_file_contents(name, 'schema2'))
|
schema2 = DockerSchema2Manifest(_get_test_file_contents(name, 'schema2'))
|
||||||
|
@ -77,7 +79,7 @@ def test_conversion(name, config_sha):
|
||||||
])
|
])
|
||||||
def test_2to1_conversion(name, config_sha):
|
def test_2to1_conversion(name, config_sha):
|
||||||
cr = {}
|
cr = {}
|
||||||
cr[config_sha] = _get_test_file_contents(name, 'config')
|
cr[config_sha] = _get_test_file_contents(name, 'config').as_encoded_str()
|
||||||
retriever = ContentRetrieverForTesting(cr)
|
retriever = ContentRetrieverForTesting(cr)
|
||||||
|
|
||||||
schema2 = DockerSchema2Manifest(_get_test_file_contents(name, 'schema2'))
|
schema2 = DockerSchema2Manifest(_get_test_file_contents(name, 'schema2'))
|
||||||
|
|
|
@ -9,6 +9,8 @@ from image.docker.schema2.list import (MalformedSchema2ManifestList, DockerSchem
|
||||||
from image.docker.schema2.test.test_manifest import MANIFEST_BYTES as v22_bytes
|
from image.docker.schema2.test.test_manifest import MANIFEST_BYTES as v22_bytes
|
||||||
from image.docker.schemautil import ContentRetrieverForTesting
|
from image.docker.schemautil import ContentRetrieverForTesting
|
||||||
from image.docker.test.test_schema1 import MANIFEST_BYTES as v21_bytes
|
from image.docker.test.test_schema1 import MANIFEST_BYTES as v21_bytes
|
||||||
|
from util.bytes import Bytes
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('json_data', [
|
@pytest.mark.parametrize('json_data', [
|
||||||
'',
|
'',
|
||||||
|
@ -21,7 +23,7 @@ from image.docker.test.test_schema1 import MANIFEST_BYTES as v21_bytes
|
||||||
])
|
])
|
||||||
def test_malformed_manifest_lists(json_data):
|
def test_malformed_manifest_lists(json_data):
|
||||||
with pytest.raises(MalformedSchema2ManifestList):
|
with pytest.raises(MalformedSchema2ManifestList):
|
||||||
DockerSchema2ManifestList(json_data)
|
DockerSchema2ManifestList(Bytes.for_string_or_unicode(json_data))
|
||||||
|
|
||||||
|
|
||||||
MANIFESTLIST_BYTES = json.dumps({
|
MANIFESTLIST_BYTES = json.dumps({
|
||||||
|
@ -74,11 +76,11 @@ retriever = ContentRetrieverForTesting({
|
||||||
})
|
})
|
||||||
|
|
||||||
def test_valid_manifestlist():
|
def test_valid_manifestlist():
|
||||||
manifestlist = DockerSchema2ManifestList(MANIFESTLIST_BYTES)
|
manifestlist = DockerSchema2ManifestList(Bytes.for_string_or_unicode(MANIFESTLIST_BYTES))
|
||||||
assert len(manifestlist.manifests(retriever)) == 2
|
assert len(manifestlist.manifests(retriever)) == 2
|
||||||
|
|
||||||
assert manifestlist.media_type == 'application/vnd.docker.distribution.manifest.list.v2+json'
|
assert manifestlist.media_type == 'application/vnd.docker.distribution.manifest.list.v2+json'
|
||||||
assert manifestlist.bytes == MANIFESTLIST_BYTES
|
assert manifestlist.bytes.as_encoded_str() == MANIFESTLIST_BYTES
|
||||||
assert manifestlist.manifest_dict == json.loads(MANIFESTLIST_BYTES)
|
assert manifestlist.manifest_dict == json.loads(MANIFESTLIST_BYTES)
|
||||||
assert manifestlist.get_layers(retriever) is None
|
assert manifestlist.get_layers(retriever) is None
|
||||||
assert not manifestlist.blob_digests
|
assert not manifestlist.blob_digests
|
||||||
|
@ -108,18 +110,18 @@ def test_valid_manifestlist():
|
||||||
|
|
||||||
|
|
||||||
def test_get_schema1_manifest_no_matching_list():
|
def test_get_schema1_manifest_no_matching_list():
|
||||||
manifestlist = DockerSchema2ManifestList(NO_AMD_MANIFESTLIST_BYTES)
|
manifestlist = DockerSchema2ManifestList(Bytes.for_string_or_unicode(NO_AMD_MANIFESTLIST_BYTES))
|
||||||
assert len(manifestlist.manifests(retriever)) == 1
|
assert len(manifestlist.manifests(retriever)) == 1
|
||||||
|
|
||||||
assert manifestlist.media_type == 'application/vnd.docker.distribution.manifest.list.v2+json'
|
assert manifestlist.media_type == 'application/vnd.docker.distribution.manifest.list.v2+json'
|
||||||
assert manifestlist.bytes == NO_AMD_MANIFESTLIST_BYTES
|
assert manifestlist.bytes.as_encoded_str() == NO_AMD_MANIFESTLIST_BYTES
|
||||||
|
|
||||||
compatible_manifest = manifestlist.get_schema1_manifest('foo', 'bar', 'baz', retriever)
|
compatible_manifest = manifestlist.get_schema1_manifest('foo', 'bar', 'baz', retriever)
|
||||||
assert compatible_manifest is None
|
assert compatible_manifest is None
|
||||||
|
|
||||||
|
|
||||||
def test_builder():
|
def test_builder():
|
||||||
existing = DockerSchema2ManifestList(MANIFESTLIST_BYTES)
|
existing = DockerSchema2ManifestList(Bytes.for_string_or_unicode(MANIFESTLIST_BYTES))
|
||||||
builder = DockerSchema2ManifestListBuilder()
|
builder = DockerSchema2ManifestListBuilder()
|
||||||
for index, manifest in enumerate(existing.manifests(retriever)):
|
for index, manifest in enumerate(existing.manifests(retriever)):
|
||||||
builder.add_manifest(manifest.manifest_obj, "amd64", "os")
|
builder.add_manifest(manifest.manifest_obj, "amd64", "os")
|
||||||
|
|
|
@ -13,6 +13,7 @@ from image.docker.schema2.manifest import (MalformedSchema2Manifest, DockerSchem
|
||||||
from image.docker.schema2.config import DockerSchema2Config
|
from image.docker.schema2.config import DockerSchema2Config
|
||||||
from image.docker.schema2.test.test_config import CONFIG_BYTES
|
from image.docker.schema2.test.test_config import CONFIG_BYTES
|
||||||
from image.docker.schemautil import ContentRetrieverForTesting
|
from image.docker.schemautil import ContentRetrieverForTesting
|
||||||
|
from util.bytes import Bytes
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('json_data', [
|
@pytest.mark.parametrize('json_data', [
|
||||||
|
@ -26,7 +27,7 @@ from image.docker.schemautil import ContentRetrieverForTesting
|
||||||
])
|
])
|
||||||
def test_malformed_manifests(json_data):
|
def test_malformed_manifests(json_data):
|
||||||
with pytest.raises(MalformedSchema2Manifest):
|
with pytest.raises(MalformedSchema2Manifest):
|
||||||
DockerSchema2Manifest(json_data)
|
DockerSchema2Manifest(Bytes.for_string_or_unicode(json_data))
|
||||||
|
|
||||||
|
|
||||||
MANIFEST_BYTES = json.dumps({
|
MANIFEST_BYTES = json.dumps({
|
||||||
|
@ -95,7 +96,7 @@ REMOTE_MANIFEST_BYTES = json.dumps({
|
||||||
})
|
})
|
||||||
|
|
||||||
def test_valid_manifest():
|
def test_valid_manifest():
|
||||||
manifest = DockerSchema2Manifest(MANIFEST_BYTES)
|
manifest = DockerSchema2Manifest(Bytes.for_string_or_unicode(MANIFEST_BYTES))
|
||||||
assert manifest.config.size == 1885
|
assert manifest.config.size == 1885
|
||||||
assert str(manifest.config.digest) == 'sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7'
|
assert str(manifest.config.digest) == 'sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7'
|
||||||
assert manifest.media_type == "application/vnd.docker.distribution.manifest.v2+json"
|
assert manifest.media_type == "application/vnd.docker.distribution.manifest.v2+json"
|
||||||
|
@ -148,7 +149,7 @@ def test_valid_manifest():
|
||||||
|
|
||||||
|
|
||||||
def test_valid_remote_manifest():
|
def test_valid_remote_manifest():
|
||||||
manifest = DockerSchema2Manifest(REMOTE_MANIFEST_BYTES)
|
manifest = DockerSchema2Manifest(Bytes.for_string_or_unicode(REMOTE_MANIFEST_BYTES))
|
||||||
assert manifest.config.size == 1885
|
assert manifest.config.size == 1885
|
||||||
assert str(manifest.config.digest) == 'sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7'
|
assert str(manifest.config.digest) == 'sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7'
|
||||||
assert manifest.media_type == "application/vnd.docker.distribution.manifest.v2+json"
|
assert manifest.media_type == "application/vnd.docker.distribution.manifest.v2+json"
|
||||||
|
@ -209,7 +210,7 @@ def test_valid_remote_manifest():
|
||||||
|
|
||||||
|
|
||||||
def test_schema2_builder():
|
def test_schema2_builder():
|
||||||
manifest = DockerSchema2Manifest(MANIFEST_BYTES)
|
manifest = DockerSchema2Manifest(Bytes.for_string_or_unicode(MANIFEST_BYTES))
|
||||||
|
|
||||||
builder = DockerSchema2ManifestBuilder()
|
builder = DockerSchema2ManifestBuilder()
|
||||||
builder.set_config_digest(manifest.config.digest, manifest.config.size)
|
builder.set_config_digest(manifest.config.digest, manifest.config.size)
|
||||||
|
@ -232,12 +233,12 @@ def test_get_manifest_labels():
|
||||||
"history": [],
|
"history": [],
|
||||||
}, 'sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7', 1885)
|
}, 'sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7', 1885)
|
||||||
|
|
||||||
manifest = DockerSchema2Manifest(MANIFEST_BYTES)
|
manifest = DockerSchema2Manifest(Bytes.for_string_or_unicode(MANIFEST_BYTES))
|
||||||
assert manifest.get_manifest_labels(retriever) == labels
|
assert manifest.get_manifest_labels(retriever) == labels
|
||||||
|
|
||||||
|
|
||||||
def test_build_schema1():
|
def test_build_schema1():
|
||||||
manifest = DockerSchema2Manifest(MANIFEST_BYTES)
|
manifest = DockerSchema2Manifest(Bytes.for_string_or_unicode(MANIFEST_BYTES))
|
||||||
assert not manifest.has_remote_layer
|
assert not manifest.has_remote_layer
|
||||||
|
|
||||||
retriever = ContentRetrieverForTesting({
|
retriever = ContentRetrieverForTesting({
|
||||||
|
@ -277,7 +278,7 @@ def test_get_schema1_manifest():
|
||||||
],
|
],
|
||||||
}, 'sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7', 1885)
|
}, 'sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7', 1885)
|
||||||
|
|
||||||
manifest = DockerSchema2Manifest(MANIFEST_BYTES)
|
manifest = DockerSchema2Manifest(Bytes.for_string_or_unicode(MANIFEST_BYTES))
|
||||||
schema1 = manifest.get_schema1_manifest('somenamespace', 'somename', 'sometag', retriever)
|
schema1 = manifest.get_schema1_manifest('somenamespace', 'somename', 'sometag', retriever)
|
||||||
assert schema1 is not None
|
assert schema1 is not None
|
||||||
assert schema1.media_type == DOCKER_SCHEMA1_MANIFEST_CONTENT_TYPE
|
assert schema1.media_type == DOCKER_SCHEMA1_MANIFEST_CONTENT_TYPE
|
||||||
|
@ -396,7 +397,7 @@ def test_build_unencoded_unicode_manifest():
|
||||||
],
|
],
|
||||||
}, ensure_ascii=False)
|
}, ensure_ascii=False)
|
||||||
|
|
||||||
schema2_config = DockerSchema2Config(config_json)
|
schema2_config = DockerSchema2Config(Bytes.for_string_or_unicode(config_json))
|
||||||
|
|
||||||
builder = DockerSchema2ManifestBuilder()
|
builder = DockerSchema2ManifestBuilder()
|
||||||
builder.set_config(schema2_config)
|
builder.set_config(schema2_config)
|
||||||
|
@ -414,7 +415,7 @@ def test_load_unicode_manifest():
|
||||||
with open(os.path.join(test_dir, 'unicode_manifest.json'), 'r') as f:
|
with open(os.path.join(test_dir, 'unicode_manifest.json'), 'r') as f:
|
||||||
manifest_bytes = f.read()
|
manifest_bytes = f.read()
|
||||||
|
|
||||||
manifest = DockerSchema2Manifest(manifest_bytes)
|
manifest = DockerSchema2Manifest(Bytes.for_string_or_unicode(manifest_bytes))
|
||||||
assert manifest.digest == 'sha256:97556fa8c553395bd9d8e19a04acef4716ca287ffbf6bde14dd9966053912613'
|
assert manifest.digest == 'sha256:97556fa8c553395bd9d8e19a04acef4716ca287ffbf6bde14dd9966053912613'
|
||||||
|
|
||||||
layers = list(manifest.get_layers(retriever))
|
layers = list(manifest.get_layers(retriever))
|
||||||
|
|
|
@ -4,18 +4,14 @@ from image.docker.schema2 import (DOCKER_SCHEMA2_MANIFEST_CONTENT_TYPE,
|
||||||
DOCKER_SCHEMA2_MANIFESTLIST_CONTENT_TYPE)
|
DOCKER_SCHEMA2_MANIFESTLIST_CONTENT_TYPE)
|
||||||
from image.docker.schema2.manifest import DockerSchema2Manifest
|
from image.docker.schema2.manifest import DockerSchema2Manifest
|
||||||
from image.docker.schema2.list import DockerSchema2ManifestList
|
from image.docker.schema2.list import DockerSchema2ManifestList
|
||||||
|
from util.bytes import Bytes
|
||||||
|
|
||||||
|
|
||||||
def parse_manifest_from_bytes(manifest_bytes, media_type, validate=True):
|
def parse_manifest_from_bytes(manifest_bytes, media_type, validate=True):
|
||||||
""" Parses and returns a manifest from the given bytes, for the given media type.
|
""" Parses and returns a manifest from the given bytes, for the given media type.
|
||||||
Raises a ManifestException if the parse fails for some reason.
|
Raises a ManifestException if the parse fails for some reason.
|
||||||
"""
|
"""
|
||||||
# NOTE: Docker sometimes pushed manifests encoded as utf-8, so decode them
|
assert isinstance(manifest_bytes, Bytes)
|
||||||
# if we can. Otherwise, treat the string as already unicode encoded.
|
|
||||||
try:
|
|
||||||
manifest_bytes = manifest_bytes.decode('utf-8')
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
if media_type == DOCKER_SCHEMA2_MANIFEST_CONTENT_TYPE:
|
if media_type == DOCKER_SCHEMA2_MANIFEST_CONTENT_TYPE:
|
||||||
return DockerSchema2Manifest(manifest_bytes)
|
return DockerSchema2Manifest(manifest_bytes)
|
||||||
|
|
|
@ -24,14 +24,6 @@ class ContentRetrieverForTesting(ContentRetriever):
|
||||||
return ContentRetrieverForTesting(digests)
|
return ContentRetrieverForTesting(digests)
|
||||||
|
|
||||||
|
|
||||||
def ensure_utf8(unicode_or_str):
|
|
||||||
""" Ensures the given string is a utf-8 encoded str and not a unicode type. """
|
|
||||||
if isinstance(unicode_or_str, unicode):
|
|
||||||
return unicode_or_str.encode('utf-8')
|
|
||||||
|
|
||||||
return unicode_or_str
|
|
||||||
|
|
||||||
|
|
||||||
class _CustomEncoder(json.JSONEncoder):
|
class _CustomEncoder(json.JSONEncoder):
|
||||||
def encode(self, o):
|
def encode(self, o):
|
||||||
encoded = super(_CustomEncoder, self).encode(o)
|
encoded = super(_CustomEncoder, self).encode(o)
|
||||||
|
|
|
@ -8,6 +8,8 @@ import pytest
|
||||||
from app import docker_v2_signing_key
|
from app import docker_v2_signing_key
|
||||||
from image.docker.schema1 import (MalformedSchema1Manifest, DockerSchema1Manifest,
|
from image.docker.schema1 import (MalformedSchema1Manifest, DockerSchema1Manifest,
|
||||||
DockerSchema1ManifestBuilder)
|
DockerSchema1ManifestBuilder)
|
||||||
|
from util.bytes import Bytes
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('json_data', [
|
@pytest.mark.parametrize('json_data', [
|
||||||
'',
|
'',
|
||||||
|
@ -20,7 +22,7 @@ from image.docker.schema1 import (MalformedSchema1Manifest, DockerSchema1Manifes
|
||||||
])
|
])
|
||||||
def test_malformed_manifests(json_data):
|
def test_malformed_manifests(json_data):
|
||||||
with pytest.raises(MalformedSchema1Manifest):
|
with pytest.raises(MalformedSchema1Manifest):
|
||||||
DockerSchema1Manifest(json_data)
|
DockerSchema1Manifest(Bytes.for_string_or_unicode(json_data))
|
||||||
|
|
||||||
|
|
||||||
MANIFEST_BYTES = json.dumps({
|
MANIFEST_BYTES = json.dumps({
|
||||||
|
@ -64,7 +66,7 @@ MANIFEST_BYTES = json.dumps({
|
||||||
|
|
||||||
|
|
||||||
def test_valid_manifest():
|
def test_valid_manifest():
|
||||||
manifest = DockerSchema1Manifest(MANIFEST_BYTES, validate=False)
|
manifest = DockerSchema1Manifest(Bytes.for_string_or_unicode(MANIFEST_BYTES), validate=False)
|
||||||
assert len(manifest.signatures) == 1
|
assert len(manifest.signatures) == 1
|
||||||
assert manifest.namespace == ''
|
assert manifest.namespace == ''
|
||||||
assert manifest.repo_name == 'hello-world'
|
assert manifest.repo_name == 'hello-world'
|
||||||
|
@ -107,7 +109,7 @@ def test_validate_manifest():
|
||||||
with open(os.path.join(test_dir, 'validated_manifest.json'), 'r') as f:
|
with open(os.path.join(test_dir, 'validated_manifest.json'), 'r') as f:
|
||||||
manifest_bytes = f.read()
|
manifest_bytes = f.read()
|
||||||
|
|
||||||
manifest = DockerSchema1Manifest(manifest_bytes, validate=True)
|
manifest = DockerSchema1Manifest(Bytes.for_string_or_unicode(manifest_bytes), validate=True)
|
||||||
digest = manifest.digest
|
digest = manifest.digest
|
||||||
assert digest == 'sha256:b5dc4f63fdbd64f34f2314c0747ef81008f9fcddce4edfc3fd0e8ec8b358d571'
|
assert digest == 'sha256:b5dc4f63fdbd64f34f2314c0747ef81008f9fcddce4edfc3fd0e8ec8b358d571'
|
||||||
assert manifest.created_datetime
|
assert manifest.created_datetime
|
||||||
|
@ -118,7 +120,7 @@ def test_validate_manifest_with_unicode():
|
||||||
with open(os.path.join(test_dir, 'validated_manifest_with_unicode.json'), 'r') as f:
|
with open(os.path.join(test_dir, 'validated_manifest_with_unicode.json'), 'r') as f:
|
||||||
manifest_bytes = f.read()
|
manifest_bytes = f.read()
|
||||||
|
|
||||||
manifest = DockerSchema1Manifest(manifest_bytes, validate=True)
|
manifest = DockerSchema1Manifest(Bytes.for_string_or_unicode(manifest_bytes), validate=True)
|
||||||
digest = manifest.digest
|
digest = manifest.digest
|
||||||
assert digest == 'sha256:815ecf45716a96b19d54d911e6ace91f78bab26ca0dd299645d9995dacd9f1ef'
|
assert digest == 'sha256:815ecf45716a96b19d54d911e6ace91f78bab26ca0dd299645d9995dacd9f1ef'
|
||||||
assert manifest.created_datetime
|
assert manifest.created_datetime
|
||||||
|
@ -140,7 +142,7 @@ def test_validate_manifest_with_unencoded_unicode():
|
||||||
with open(os.path.join(test_dir, 'manifest_unencoded_unicode.json'), 'r') as f:
|
with open(os.path.join(test_dir, 'manifest_unencoded_unicode.json'), 'r') as f:
|
||||||
manifest_bytes = f.read()
|
manifest_bytes = f.read()
|
||||||
|
|
||||||
manifest = DockerSchema1Manifest(manifest_bytes)
|
manifest = DockerSchema1Manifest(Bytes.for_string_or_unicode(manifest_bytes))
|
||||||
digest = manifest.digest
|
digest = manifest.digest
|
||||||
assert digest == 'sha256:5d8a0f34744a39bf566ba430251adc0cc86587f86aed3ac2acfb897f349777bc'
|
assert digest == 'sha256:5d8a0f34744a39bf566ba430251adc0cc86587f86aed3ac2acfb897f349777bc'
|
||||||
assert manifest.created_datetime
|
assert manifest.created_datetime
|
||||||
|
@ -162,3 +164,17 @@ def test_build_unencoded_unicode_manifest(with_key):
|
||||||
|
|
||||||
built = builder.build(with_key, ensure_ascii=False)
|
built = builder.build(with_key, ensure_ascii=False)
|
||||||
built._validate()
|
built._validate()
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_manifest_known_issue():
|
||||||
|
test_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
with open(os.path.join(test_dir, 'validate_manifest_known_issue.json'), 'r') as f:
|
||||||
|
manifest_bytes = f.read()
|
||||||
|
|
||||||
|
manifest = DockerSchema1Manifest(Bytes.for_string_or_unicode(manifest_bytes))
|
||||||
|
digest = manifest.digest
|
||||||
|
assert digest == 'sha256:44518f5a4d1cb5b7a6347763116fb6e10f6a8563b6c40bb389a0a982f0a9f47a'
|
||||||
|
assert manifest.created_datetime
|
||||||
|
|
||||||
|
layers = list(manifest.get_layers(None))
|
||||||
|
assert layers[-1].author is None
|
||||||
|
|
|
@ -7,6 +7,7 @@ from image.docker.schema2 import DOCKER_SCHEMA2_MANIFESTLIST_CONTENT_TYPE
|
||||||
from image.docker.test.test_schema1 import MANIFEST_BYTES as SCHEMA1_BYTES
|
from image.docker.test.test_schema1 import MANIFEST_BYTES as SCHEMA1_BYTES
|
||||||
from image.docker.schema2.test.test_list import MANIFESTLIST_BYTES
|
from image.docker.schema2.test.test_list import MANIFESTLIST_BYTES
|
||||||
from image.docker.schema2.test.test_manifest import MANIFEST_BYTES as SCHEMA2_BYTES
|
from image.docker.schema2.test.test_manifest import MANIFEST_BYTES as SCHEMA2_BYTES
|
||||||
|
from util.bytes import Bytes
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('media_type, manifest_bytes', [
|
@pytest.mark.parametrize('media_type, manifest_bytes', [
|
||||||
|
@ -15,4 +16,5 @@ from image.docker.schema2.test.test_manifest import MANIFEST_BYTES as SCHEMA2_BY
|
||||||
(DOCKER_SCHEMA2_MANIFESTLIST_CONTENT_TYPE, MANIFESTLIST_BYTES),
|
(DOCKER_SCHEMA2_MANIFESTLIST_CONTENT_TYPE, MANIFESTLIST_BYTES),
|
||||||
])
|
])
|
||||||
def test_parse_manifest_from_bytes(media_type, manifest_bytes):
|
def test_parse_manifest_from_bytes(media_type, manifest_bytes):
|
||||||
assert parse_manifest_from_bytes(manifest_bytes, media_type, validate=False)
|
assert parse_manifest_from_bytes(Bytes.for_string_or_unicode(manifest_bytes), media_type,
|
||||||
|
validate=False)
|
||||||
|
|
56
image/docker/test/validate_manifest_known_issue.json
Normal file
56
image/docker/test/validate_manifest_known_issue.json
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"name": "quaymonitor/monitortest2",
|
||||||
|
"tag": "latest",
|
||||||
|
"architecture": "x86_64",
|
||||||
|
"fsLayers": [
|
||||||
|
{
|
||||||
|
"blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"blobSum": "sha256:184dc3db39b5e19dc39547f43db46ea48cd6cc779e806a3c8a5e5396acd20206"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"blobSum": "sha256:db80bcab0e8b69656505332fcdff3ef2b9f664a2029d1b2f97224cffcf689afc"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"blobSum": "sha256:184dc3db39b5e19dc39547f43db46ea48cd6cc779e806a3c8a5e5396acd20206"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"blobSum": "sha256:f0a98344d604e54694fc6118cf7a0cbd10dc7b2e9be8607ba8c5bfd7ba3c1067"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"history": [
|
||||||
|
{
|
||||||
|
"v1Compatibility": "{\"architecture\":\"x86_64\",\"config\":{\"Hostname\":\"4c9181ab6b87\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"HOME=/\",\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"],\"Cmd\":[\"sh\",\"echo\",\"\\\"2019-01-08 19:13:20 +0000\\\" \\u003e foo\"],\"Image\":\"quay.io/quay/busybox\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":{}},\"container\":\"4c9181ab6b87fe75b5c0955c6c78983dec337914b05e65fb0073cce0ad076106\",\"container_config\":{\"Hostname\":\"4c9181ab6b87\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"HOME=/\",\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"],\"Cmd\":[\"sh\",\"echo\",\"\\\"2019-01-08 19:13:20 +0000\\\" \\u003e foo\"],\"Image\":\"quay.io/quay/busybox\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":{}},\"created\":\"2019-01-08T19:13:20.674196032Z\",\"docker_version\":\"18.06.1-ce\",\"id\":\"7da7c4e4bcb121915fb33eb5c76ffef194cdcc14608010692cfce5734bd84751\",\"os\":\"linux\",\"parent\":\"ec75e623647b299585bdb0991293bd446e5545e9a4dabf9d37922d5671d9d860\",\"throwaway\":true}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"v1Compatibility": "{\"id\":\"ec75e623647b299585bdb0991293bd446e5545e9a4dabf9d37922d5671d9d860\",\"parent\":\"f32bc6daa02c76f0b1773688684bf3bee719a69db06192432e6c28a238f4cf4a\",\"created\":\"2014-02-03T15:58:08.872585903Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop) CMD [/bin/sh -c /bin/sh]\"]},\"author\":\"Jérôme Petazzoni \\u003cjerome@docker.com\\u003e\"}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"v1Compatibility": "{\"id\":\"f32bc6daa02c76f0b1773688684bf3bee719a69db06192432e6c28a238f4cf4a\",\"parent\":\"02feaf4fdc57dba2b142dae9d8dd0c90e710be710bea25ce63269e65d8f32872\",\"created\":\"2014-02-03T15:58:08.72383042Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop) ADD rootfs.tar in /\"]},\"author\":\"Jérôme Petazzoni \\u003cjerome@docker.com\\u003e\"}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"v1Compatibility": "{\"id\":\"02feaf4fdc57dba2b142dae9d8dd0c90e710be710bea25ce63269e65d8f32872\",\"parent\":\"f9a6e54178f312aa3686d7305b970e7d908d58b32e3f4554731b647e07b48fd2\",\"created\":\"2014-02-03T15:58:08.52236968Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop) MAINTAINER Jérôme Petazzoni \\u003cjerome@docker.com\\u003e\"]},\"author\":\"Jérôme Petazzoni \\u003cjerome@docker.com\\u003e\"}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"v1Compatibility": "{\"id\":\"f9a6e54178f312aa3686d7305b970e7d908d58b32e3f4554731b647e07b48fd2\",\"comment\":\"Imported from -\",\"created\":\"2013-06-13T14:03:50.821769-07:00\",\"container_config\":{\"Cmd\":[\"\"]}}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"signatures": [
|
||||||
|
{
|
||||||
|
"header": {
|
||||||
|
"jwk": {
|
||||||
|
"crv": "P-256",
|
||||||
|
"kid": "XPAM:RVQE:4LWW:ABXI:QLLK:O2LK:XJ4V:UAOJ:WM24:ZG6J:UIJ3:JAYM",
|
||||||
|
"kty": "EC",
|
||||||
|
"x": "ijnW3d93SINE1y3GjNsCMYghAb7NT21vSiYK8pWdBkM",
|
||||||
|
"y": "7t-mGjoYOhEIGVaCSEclLLkMgHz2S9WXkReZJEBx-_U"
|
||||||
|
},
|
||||||
|
"alg": "ES256"
|
||||||
|
},
|
||||||
|
"signature": "N9m-NNL8CdGwxEHHHaJDhbT5_FFKBSdyy-7lP4jnWG3AQmOWbPEXTFANTeH2CNPvAbaM9ZqQm0dQFQVnOe5GNQ",
|
||||||
|
"protected": "eyJmb3JtYXRMZW5ndGgiOjM1OTgsImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAxOS0wMS0wOFQxOToxMzoyM1oifQ"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -12,6 +12,7 @@ from image.docker.schema2.list import DOCKER_SCHEMA2_MANIFESTLIST_CONTENT_TYPE
|
||||||
from image.docker.schemas import parse_manifest_from_bytes
|
from image.docker.schemas import parse_manifest_from_bytes
|
||||||
from test.registry.protocols import (RegistryProtocol, Failures, ProtocolOptions, PushResult,
|
from test.registry.protocols import (RegistryProtocol, Failures, ProtocolOptions, PushResult,
|
||||||
PullResult)
|
PullResult)
|
||||||
|
from util.bytes import Bytes
|
||||||
|
|
||||||
|
|
||||||
@unique
|
@unique
|
||||||
|
@ -168,7 +169,8 @@ class V2Protocol(RegistryProtocol):
|
||||||
|
|
||||||
# Parse the returned manifest list and ensure it matches.
|
# Parse the returned manifest list and ensure it matches.
|
||||||
assert response.headers['Content-Type'] == DOCKER_SCHEMA2_MANIFESTLIST_CONTENT_TYPE
|
assert response.headers['Content-Type'] == DOCKER_SCHEMA2_MANIFESTLIST_CONTENT_TYPE
|
||||||
retrieved = parse_manifest_from_bytes(response.text, response.headers['Content-Type'])
|
retrieved = parse_manifest_from_bytes(Bytes.for_string_or_unicode(response.text),
|
||||||
|
response.headers['Content-Type'])
|
||||||
assert retrieved.schema_version == 2
|
assert retrieved.schema_version == 2
|
||||||
assert retrieved.is_manifest_list
|
assert retrieved.is_manifest_list
|
||||||
assert retrieved.digest == manifestlist.digest
|
assert retrieved.digest == manifestlist.digest
|
||||||
|
@ -184,7 +186,8 @@ class V2Protocol(RegistryProtocol):
|
||||||
if expected_failure is not None:
|
if expected_failure is not None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
manifest = parse_manifest_from_bytes(response.text, response.headers['Content-Type'])
|
manifest = parse_manifest_from_bytes(Bytes.for_string_or_unicode(response.text),
|
||||||
|
response.headers['Content-Type'])
|
||||||
assert not manifest.is_manifest_list
|
assert not manifest.is_manifest_list
|
||||||
assert manifest.digest == manifest_digest
|
assert manifest.digest == manifest_digest
|
||||||
|
|
||||||
|
@ -221,7 +224,7 @@ class V2Protocol(RegistryProtocol):
|
||||||
|
|
||||||
self.conduct(session, 'PUT',
|
self.conduct(session, 'PUT',
|
||||||
'/v2/%s/manifests/%s' % (self.repo_name(namespace, repo_name), manifest.digest),
|
'/v2/%s/manifests/%s' % (self.repo_name(namespace, repo_name), manifest.digest),
|
||||||
data=manifest.bytes,
|
data=manifest.bytes.as_encoded_str(),
|
||||||
expected_status=(202, expected_failure, V2ProtocolSteps.PUT_MANIFEST),
|
expected_status=(202, expected_failure, V2ProtocolSteps.PUT_MANIFEST),
|
||||||
headers=manifest_headers)
|
headers=manifest_headers)
|
||||||
|
|
||||||
|
@ -235,7 +238,7 @@ class V2Protocol(RegistryProtocol):
|
||||||
|
|
||||||
self.conduct(session, 'PUT',
|
self.conduct(session, 'PUT',
|
||||||
'/v2/%s/manifests/%s' % (self.repo_name(namespace, repo_name), tag_name),
|
'/v2/%s/manifests/%s' % (self.repo_name(namespace, repo_name), tag_name),
|
||||||
data=manifestlist.bytes,
|
data=manifestlist.bytes.as_encoded_str(),
|
||||||
expected_status=(202, expected_failure, V2ProtocolSteps.PUT_MANIFEST_LIST),
|
expected_status=(202, expected_failure, V2ProtocolSteps.PUT_MANIFEST_LIST),
|
||||||
headers=manifest_headers)
|
headers=manifest_headers)
|
||||||
|
|
||||||
|
@ -282,10 +285,10 @@ class V2Protocol(RegistryProtocol):
|
||||||
config['config'] = images[-1].config
|
config['config'] = images[-1].config
|
||||||
|
|
||||||
config_json = json.dumps(config, ensure_ascii=options.ensure_ascii)
|
config_json = json.dumps(config, ensure_ascii=options.ensure_ascii)
|
||||||
schema2_config = DockerSchema2Config(config_json)
|
schema2_config = DockerSchema2Config(Bytes.for_string_or_unicode(config_json))
|
||||||
builder.set_config(schema2_config)
|
builder.set_config(schema2_config)
|
||||||
|
|
||||||
blobs[schema2_config.digest] = schema2_config.bytes.encode('utf-8')
|
blobs[schema2_config.digest] = schema2_config.bytes.as_encoded_str()
|
||||||
return builder.build(ensure_ascii=options.ensure_ascii)
|
return builder.build(ensure_ascii=options.ensure_ascii)
|
||||||
|
|
||||||
def build_schema1(self, namespace, repo_name, tag_name, images, blobs, options):
|
def build_schema1(self, namespace, repo_name, tag_name, images, blobs, options):
|
||||||
|
@ -372,7 +375,7 @@ class V2Protocol(RegistryProtocol):
|
||||||
tag_or_digest = tag_name if not options.push_by_manifest_digest else manifest.digest
|
tag_or_digest = tag_name if not options.push_by_manifest_digest else manifest.digest
|
||||||
self.conduct(session, 'PUT',
|
self.conduct(session, 'PUT',
|
||||||
'/v2/%s/manifests/%s' % (self.repo_name(namespace, repo_name), tag_or_digest),
|
'/v2/%s/manifests/%s' % (self.repo_name(namespace, repo_name), tag_or_digest),
|
||||||
data=manifest.bytes.encode('utf-8'),
|
data=manifest.bytes.as_encoded_str(),
|
||||||
expected_status=(put_code, expected_failure, V2ProtocolSteps.PUT_MANIFEST),
|
expected_status=(put_code, expected_failure, V2ProtocolSteps.PUT_MANIFEST),
|
||||||
headers=manifest_headers)
|
headers=manifest_headers)
|
||||||
|
|
||||||
|
@ -546,7 +549,8 @@ class V2Protocol(RegistryProtocol):
|
||||||
if not self.schema2:
|
if not self.schema2:
|
||||||
assert response.headers['Content-Type'] in DOCKER_SCHEMA1_CONTENT_TYPES
|
assert response.headers['Content-Type'] in DOCKER_SCHEMA1_CONTENT_TYPES
|
||||||
|
|
||||||
manifest = parse_manifest_from_bytes(response.text, response.headers['Content-Type'])
|
manifest = parse_manifest_from_bytes(Bytes.for_string_or_unicode(response.text),
|
||||||
|
response.headers['Content-Type'])
|
||||||
manifests[tag_name] = manifest
|
manifests[tag_name] = manifest
|
||||||
|
|
||||||
if manifest.schema_version == 1:
|
if manifest.schema_version == 1:
|
||||||
|
|
|
@ -682,7 +682,7 @@ class V2RegistryPushMixin(V2RegistryMixin):
|
||||||
# a 202 response for success.
|
# a 202 response for success.
|
||||||
put_code = 400 if invalid else 202
|
put_code = 400 if invalid else 202
|
||||||
self.conduct('PUT', '/v2/%s/manifests/%s' % (repo_name, tag_name),
|
self.conduct('PUT', '/v2/%s/manifests/%s' % (repo_name, tag_name),
|
||||||
data=manifest.bytes, expected_code=put_code,
|
data=manifest.bytes.as_encoded_str(), expected_code=put_code,
|
||||||
headers={'Content-Type': 'application/json'}, auth='jwt')
|
headers={'Content-Type': 'application/json'}, auth='jwt')
|
||||||
|
|
||||||
return checksums, manifests
|
return checksums, manifests
|
||||||
|
@ -1628,7 +1628,7 @@ class V2RegistryTests(V2RegistryPullMixin, V2RegistryPushMixin, RegistryTestsMix
|
||||||
manifest = builder.build(_JWK)
|
manifest = builder.build(_JWK)
|
||||||
|
|
||||||
self.conduct('PUT', '/v2/%s/manifests/%s' % (repo_name, tag_name),
|
self.conduct('PUT', '/v2/%s/manifests/%s' % (repo_name, tag_name),
|
||||||
data=manifest.bytes, expected_code=415,
|
data=manifest.bytes.as_encoded_str(), expected_code=415,
|
||||||
headers={'Content-Type': 'application/vnd.docker.distribution.manifest.v2+json'},
|
headers={'Content-Type': 'application/vnd.docker.distribution.manifest.v2+json'},
|
||||||
auth='jwt')
|
auth='jwt')
|
||||||
|
|
||||||
|
@ -1662,7 +1662,7 @@ class V2RegistryTests(V2RegistryPullMixin, V2RegistryPushMixin, RegistryTestsMix
|
||||||
manifest = builder.build(_JWK)
|
manifest = builder.build(_JWK)
|
||||||
|
|
||||||
self.conduct('PUT', '/v2/%s/manifests/%s' % (repo_name, tag_name),
|
self.conduct('PUT', '/v2/%s/manifests/%s' % (repo_name, tag_name),
|
||||||
data=manifest.bytes, expected_code=415,
|
data=manifest.bytes.as_encoded_str(), expected_code=415,
|
||||||
headers={'Content-Type': 'application/vnd.oci.image.manifest.v1+json'},
|
headers={'Content-Type': 'application/vnd.oci.image.manifest.v1+json'},
|
||||||
auth='jwt')
|
auth='jwt')
|
||||||
|
|
||||||
|
@ -1682,7 +1682,7 @@ class V2RegistryTests(V2RegistryPullMixin, V2RegistryPushMixin, RegistryTestsMix
|
||||||
manifest = builder.build(_JWK)
|
manifest = builder.build(_JWK)
|
||||||
|
|
||||||
response = self.conduct('PUT', '/v2/%s/manifests/%s' % (repo_name, tag_name),
|
response = self.conduct('PUT', '/v2/%s/manifests/%s' % (repo_name, tag_name),
|
||||||
data=manifest.bytes, expected_code=400,
|
data=manifest.bytes.as_encoded_str(), expected_code=400,
|
||||||
headers={'Content-Type': 'application/json'}, auth='jwt')
|
headers={'Content-Type': 'application/json'}, auth='jwt')
|
||||||
self.assertEquals('MANIFEST_INVALID', response.json()['errors'][0]['code'])
|
self.assertEquals('MANIFEST_INVALID', response.json()['errors'][0]['code'])
|
||||||
|
|
||||||
|
|
32
util/bytes.py
Normal file
32
util/bytes.py
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
class Bytes(object):
|
||||||
|
""" Wrapper around strings and unicode objects to ensure we are always using
|
||||||
|
the correct encoded or decoded data.
|
||||||
|
"""
|
||||||
|
def __init__(self, data):
|
||||||
|
assert isinstance(data, str)
|
||||||
|
self._encoded_data = data
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def for_string_or_unicode(cls, input):
|
||||||
|
# If the string is a unicode string, then encode its data as UTF-8. Note that
|
||||||
|
# we don't catch any decode exceptions here, as we want those to be raised.
|
||||||
|
if isinstance(input, unicode):
|
||||||
|
return Bytes(input.encode('utf-8'))
|
||||||
|
|
||||||
|
# Next, try decoding as UTF-8. If we have a utf-8 encoded string, then we have no
|
||||||
|
# additional conversion to do.
|
||||||
|
try:
|
||||||
|
input.decode('utf-8')
|
||||||
|
return Bytes(input)
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Finally, if the data is (somehow) a unicode string inside a `str` type, then
|
||||||
|
# re-encoded the data.
|
||||||
|
return Bytes(input.encode('utf-8'))
|
||||||
|
|
||||||
|
def as_encoded_str(self):
|
||||||
|
return self._encoded_data
|
||||||
|
|
||||||
|
def as_unicode(self):
|
||||||
|
return self._encoded_data.decode('utf-8')
|
|
@ -19,6 +19,7 @@ from image.docker.schema1 import (DockerSchema1Manifest, ManifestException, Mani
|
||||||
DOCKER_SCHEMA1_SIGNED_MANIFEST_CONTENT_TYPE)
|
DOCKER_SCHEMA1_SIGNED_MANIFEST_CONTENT_TYPE)
|
||||||
|
|
||||||
from workers.worker import Worker
|
from workers.worker import Worker
|
||||||
|
from util.bytes import Bytes
|
||||||
from util.log import logfile_path
|
from util.log import logfile_path
|
||||||
from util.migrate.allocator import yield_random_entries
|
from util.migrate.allocator import yield_random_entries
|
||||||
|
|
||||||
|
@ -33,7 +34,7 @@ class BrokenManifest(ManifestInterface):
|
||||||
"""
|
"""
|
||||||
def __init__(self, digest, payload):
|
def __init__(self, digest, payload):
|
||||||
self._digest = digest
|
self._digest = digest
|
||||||
self._payload = payload
|
self._payload = Bytes.for_string_or_unicode(payload)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def digest(self):
|
def digest(self):
|
||||||
|
|
|
@ -142,7 +142,8 @@ def test_manifestbackfillworker_mislinked_manifest(clear_rows, initialized_db):
|
||||||
builder.add_layer(tag_v30.image.storage.content_checksum, '{"id": "foo"}')
|
builder.add_layer(tag_v30.image.storage.content_checksum, '{"id": "foo"}')
|
||||||
manifest = builder.build(docker_v2_signing_key)
|
manifest = builder.build(docker_v2_signing_key)
|
||||||
|
|
||||||
mislinked_manifest = TagManifest.create(json_data=manifest.bytes, digest=manifest.digest,
|
mislinked_manifest = TagManifest.create(json_data=manifest.bytes.as_encoded_str(),
|
||||||
|
digest=manifest.digest,
|
||||||
tag=tag_v50)
|
tag=tag_v50)
|
||||||
|
|
||||||
# Backfill the manifest and ensure its proper content checksum was linked.
|
# Backfill the manifest and ensure its proper content checksum was linked.
|
||||||
|
@ -176,7 +177,8 @@ def test_manifestbackfillworker_mislinked_invalid_manifest(clear_rows, initializ
|
||||||
builder.add_layer('sha256:deadbeef', '{"id": "foo"}')
|
builder.add_layer('sha256:deadbeef', '{"id": "foo"}')
|
||||||
manifest = builder.build(docker_v2_signing_key)
|
manifest = builder.build(docker_v2_signing_key)
|
||||||
|
|
||||||
broken_manifest = TagManifest.create(json_data=manifest.bytes, digest=manifest.digest,
|
broken_manifest = TagManifest.create(json_data=manifest.bytes.as_encoded_str(),
|
||||||
|
digest=manifest.digest,
|
||||||
tag=tag_v50)
|
tag=tag_v50)
|
||||||
|
|
||||||
# Backfill the manifest and ensure it is marked as broken.
|
# Backfill the manifest and ensure it is marked as broken.
|
||||||
|
@ -208,9 +210,9 @@ def test_manifestbackfillworker_repeat_digest(clear_rows, initialized_db):
|
||||||
builder.add_layer('sha256:deadbeef', '{"id": "foo"}')
|
builder.add_layer('sha256:deadbeef', '{"id": "foo"}')
|
||||||
manifest = builder.build(docker_v2_signing_key)
|
manifest = builder.build(docker_v2_signing_key)
|
||||||
|
|
||||||
manifest_1 = TagManifest.create(json_data=manifest.bytes, digest=manifest.digest,
|
manifest_1 = TagManifest.create(json_data=manifest.bytes.as_encoded_str(), digest=manifest.digest,
|
||||||
tag=tag_v30)
|
tag=tag_v30)
|
||||||
manifest_2 = TagManifest.create(json_data=manifest.bytes, digest=manifest.digest,
|
manifest_2 = TagManifest.create(json_data=manifest.bytes.as_encoded_str(), digest=manifest.digest,
|
||||||
tag=tag_v50)
|
tag=tag_v50)
|
||||||
|
|
||||||
# Backfill "both" manifests and ensure both are pointed to by a single resulting row.
|
# Backfill "both" manifests and ensure both are pointed to by a single resulting row.
|
||||||
|
|
Reference in a new issue