Merge pull request #3234 from quay/joseph.schorr/QUAY-1030/interfacing-part-8
Change verbs endpoint to use the new registry data model interface
This commit is contained in:
commit
a4f1475de4
26 changed files with 876 additions and 557 deletions
|
@ -7,6 +7,23 @@ from data.database import (Repository, Namespace, ImageStorage, Image, ImageStor
|
||||||
BlobUpload, ImageStorageLocation, db_random_func)
|
BlobUpload, ImageStorageLocation, db_random_func)
|
||||||
|
|
||||||
|
|
||||||
|
def get_repository_blob_by_digest(repository, blob_digest):
|
||||||
|
""" Find the content-addressable blob linked to the specified repository.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
storage_id_query = (ImageStorage
|
||||||
|
.select(ImageStorage.id)
|
||||||
|
.join(Image)
|
||||||
|
.where(Image.repository == repository,
|
||||||
|
ImageStorage.content_checksum == blob_digest,
|
||||||
|
ImageStorage.uploading == False)
|
||||||
|
.limit(1))
|
||||||
|
|
||||||
|
return storage_model.get_storage_by_subquery(storage_id_query)
|
||||||
|
except InvalidImageException:
|
||||||
|
raise BlobDoesNotExist('Blob does not exist with digest: {0}'.format(blob_digest))
|
||||||
|
|
||||||
|
|
||||||
def get_repo_blob_by_digest(namespace, repo_name, blob_digest):
|
def get_repo_blob_by_digest(namespace, repo_name, blob_digest):
|
||||||
""" Find the content-addressable blob linked to the specified repository.
|
""" Find the content-addressable blob linked to the specified repository.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -489,44 +489,33 @@ def find_or_create_derived_storage(source_image, transformation_name, preferred_
|
||||||
new_storage = storage.create_v1_storage(preferred_location)
|
new_storage = storage.create_v1_storage(preferred_location)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
DerivedStorageForImage.create(source_image=source_image, derivative=new_storage,
|
derived = DerivedStorageForImage.create(source_image=source_image, derivative=new_storage,
|
||||||
transformation=trans, uniqueness_hash=uniqueness_hash)
|
transformation=trans, uniqueness_hash=uniqueness_hash)
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
# Storage was created while this method executed. Just return the existing.
|
# Storage was created while this method executed. Just return the existing.
|
||||||
new_storage.delete_instance(recursive=True)
|
new_storage.delete_instance(recursive=True)
|
||||||
return find_derived_storage_for_image(source_image, transformation_name, varying_metadata)
|
return find_derived_storage_for_image(source_image, transformation_name, varying_metadata)
|
||||||
|
|
||||||
return new_storage
|
return derived
|
||||||
|
|
||||||
|
|
||||||
def find_derived_storage_for_image(source_image, transformation_name, varying_metadata=None):
|
def find_derived_storage_for_image(source_image, transformation_name, varying_metadata=None):
|
||||||
uniqueness_hash = _get_uniqueness_hash(varying_metadata)
|
uniqueness_hash = _get_uniqueness_hash(varying_metadata)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
found = (ImageStorage
|
found = (DerivedStorageForImage
|
||||||
.select(ImageStorage, DerivedStorageForImage)
|
.select(ImageStorage, DerivedStorageForImage)
|
||||||
.join(DerivedStorageForImage)
|
.join(ImageStorage)
|
||||||
|
.switch(DerivedStorageForImage)
|
||||||
.join(ImageStorageTransformation)
|
.join(ImageStorageTransformation)
|
||||||
.where(DerivedStorageForImage.source_image == source_image,
|
.where(DerivedStorageForImage.source_image == source_image,
|
||||||
ImageStorageTransformation.name == transformation_name,
|
ImageStorageTransformation.name == transformation_name,
|
||||||
DerivedStorageForImage.uniqueness_hash == uniqueness_hash)
|
DerivedStorageForImage.uniqueness_hash == uniqueness_hash)
|
||||||
.get())
|
.get())
|
||||||
|
|
||||||
found.locations = {placement.location.name for placement in found.imagestorageplacement_set}
|
|
||||||
return found
|
return found
|
||||||
except ImageStorage.DoesNotExist:
|
except DerivedStorageForImage.DoesNotExist:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def delete_derived_storage_by_uuid(storage_uuid):
|
def delete_derived_storage(derived_storage):
|
||||||
try:
|
derived_storage.derivative.delete_instance(recursive=True)
|
||||||
image_storage = storage.get_storage_by_uuid(storage_uuid)
|
|
||||||
except InvalidImageException:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
DerivedStorageForImage.get(derivative=image_storage)
|
|
||||||
except DerivedStorageForImage.DoesNotExist:
|
|
||||||
return
|
|
||||||
|
|
||||||
image_storage.delete_instance(recursive=True)
|
|
||||||
|
|
|
@ -254,6 +254,7 @@ def get_storage_by_uuid(storage_uuid):
|
||||||
|
|
||||||
def get_layer_path(storage_record):
|
def get_layer_path(storage_record):
|
||||||
""" Returns the path in the storage engine to the layer data referenced by the storage row. """
|
""" Returns the path in the storage engine to the layer data referenced by the storage row. """
|
||||||
|
assert storage_record.cas_path is not None
|
||||||
return get_layer_path_for_storage(storage_record.uuid, storage_record.cas_path,
|
return get_layer_path_for_storage(storage_record.uuid, storage_record.cas_path,
|
||||||
storage_record.content_checksum)
|
storage_record.content_checksum)
|
||||||
|
|
||||||
|
@ -280,7 +281,8 @@ def lookup_repo_storages_by_content_checksum(repo, checksums):
|
||||||
query_alias = 'q{0}'.format(counter)
|
query_alias = 'q{0}'.format(counter)
|
||||||
candidate_subq = (ImageStorage
|
candidate_subq = (ImageStorage
|
||||||
.select(ImageStorage.id, ImageStorage.content_checksum,
|
.select(ImageStorage.id, ImageStorage.content_checksum,
|
||||||
ImageStorage.image_size, ImageStorage.uuid)
|
ImageStorage.image_size, ImageStorage.uuid, ImageStorage.cas_path,
|
||||||
|
ImageStorage.uncompressed_size)
|
||||||
.join(Image)
|
.join(Image)
|
||||||
.where(Image.repository == repo, ImageStorage.content_checksum == checksum)
|
.where(Image.repository == repo, ImageStorage.content_checksum == checksum)
|
||||||
.limit(1)
|
.limit(1)
|
||||||
|
@ -336,7 +338,7 @@ def get_storage_locations(uuid):
|
||||||
|
|
||||||
def save_torrent_info(storage_object, piece_length, pieces):
|
def save_torrent_info(storage_object, piece_length, pieces):
|
||||||
try:
|
try:
|
||||||
TorrentInfo.create(storage=storage_object, piece_length=piece_length, pieces=pieces)
|
return TorrentInfo.create(storage=storage_object, piece_length=piece_length, pieces=pieces)
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
# TorrentInfo already exists for this storage.
|
# TorrentInfo already exists for this storage.
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -30,6 +30,9 @@ def datatype(name, static_fields):
|
||||||
|
|
||||||
raise AttributeError('Unknown field `%s`' % name)
|
raise AttributeError('Unknown field `%s`' % name)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '<%s> #%s' % (name, self._db_id)
|
||||||
|
|
||||||
return DataType
|
return DataType
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
from collections import namedtuple
|
||||||
from enum import Enum, unique
|
from enum import Enum, unique
|
||||||
|
|
||||||
|
from cachetools import lru_cache
|
||||||
|
|
||||||
|
from data import model
|
||||||
from data.registry_model.datatype import datatype, requiresinput
|
from data.registry_model.datatype import datatype, requiresinput
|
||||||
from image.docker.schema1 import DockerSchema1Manifest
|
from image.docker.schema1 import DockerSchema1Manifest
|
||||||
|
|
||||||
|
@ -13,6 +19,35 @@ class RepositoryReference(datatype('Repository', [])):
|
||||||
|
|
||||||
return RepositoryReference(db_id=repo_obj.id)
|
return RepositoryReference(db_id=repo_obj.id)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def for_id(cls, repo_id):
|
||||||
|
return RepositoryReference(db_id=repo_id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
@lru_cache(maxsize=1)
|
||||||
|
def _repository_obj(self):
|
||||||
|
return model.repository.lookup_repository(self._db_id)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def namespace_name(self):
|
||||||
|
""" Returns the namespace name of this repository.
|
||||||
|
"""
|
||||||
|
repository = self._repository_obj
|
||||||
|
if repository is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return repository.namespace_user.username
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
""" Returns the name of this repository.
|
||||||
|
"""
|
||||||
|
repository = self._repository_obj
|
||||||
|
if repository is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return repository.name
|
||||||
|
|
||||||
|
|
||||||
class Label(datatype('Label', ['key', 'value', 'uuid', 'source_type_name', 'media_type_name'])):
|
class Label(datatype('Label', ['key', 'value', 'uuid', 'source_type_name', 'media_type_name'])):
|
||||||
""" Label represents a label on a manifest. """
|
""" Label represents a label on a manifest. """
|
||||||
|
@ -40,7 +75,15 @@ class Tag(datatype('Tag', ['name', 'reversion', 'manifest_digest', 'lifetime_sta
|
||||||
lifetime_start_ts=repository_tag.lifetime_start_ts,
|
lifetime_start_ts=repository_tag.lifetime_start_ts,
|
||||||
lifetime_end_ts=repository_tag.lifetime_end_ts,
|
lifetime_end_ts=repository_tag.lifetime_end_ts,
|
||||||
manifest_digest=manifest_digest,
|
manifest_digest=manifest_digest,
|
||||||
inputs=dict(legacy_image=legacy_image))
|
inputs=dict(legacy_image=legacy_image,
|
||||||
|
repository=RepositoryReference.for_id(repository_tag.repository_id)))
|
||||||
|
|
||||||
|
@property
|
||||||
|
@requiresinput('repository')
|
||||||
|
def repository(self, repository):
|
||||||
|
""" Returns the repository under which this tag lives.
|
||||||
|
"""
|
||||||
|
return repository
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@requiresinput('legacy_image')
|
@requiresinput('legacy_image')
|
||||||
|
@ -124,3 +167,76 @@ class SecurityScanStatus(Enum):
|
||||||
SCANNED = 'scanned'
|
SCANNED = 'scanned'
|
||||||
FAILED = 'failed'
|
FAILED = 'failed'
|
||||||
QUEUED = 'queued'
|
QUEUED = 'queued'
|
||||||
|
|
||||||
|
|
||||||
|
class ManifestLayer(namedtuple('ManifestLayer', ['layer_info', 'blob'])):
|
||||||
|
""" Represents a single layer in a manifest. The `layer_info` data will be manifest-type specific,
|
||||||
|
but will have a few expected fields (such as `digest`). The `blob` represents the associated
|
||||||
|
blob for this layer, optionally with placements.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def estimated_size(self, estimate_multiplier):
|
||||||
|
""" Returns the estimated size of this layer. If the layers' blob has an uncompressed size,
|
||||||
|
it is used. Otherwise, the compressed_size field in the layer is multiplied by the
|
||||||
|
multiplier.
|
||||||
|
"""
|
||||||
|
if self.blob.uncompressed_size:
|
||||||
|
return self.blob.uncompressed_size
|
||||||
|
|
||||||
|
return (self.layer_info.compressed_size or 0) * estimate_multiplier
|
||||||
|
|
||||||
|
|
||||||
|
class Blob(datatype('Blob', ['uuid', 'digest', 'compressed_size', 'uncompressed_size',
|
||||||
|
'uploading'])):
|
||||||
|
""" Blob represents a content-addressable piece of storage. """
|
||||||
|
@classmethod
|
||||||
|
def for_image_storage(cls, image_storage, storage_path, placements=None):
|
||||||
|
if image_storage is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return Blob(db_id=image_storage.id,
|
||||||
|
uuid=image_storage.uuid,
|
||||||
|
inputs=dict(placements=placements, storage_path=storage_path),
|
||||||
|
digest=image_storage.content_checksum,
|
||||||
|
compressed_size=image_storage.image_size,
|
||||||
|
uncompressed_size=image_storage.uncompressed_size,
|
||||||
|
uploading=image_storage.uploading)
|
||||||
|
|
||||||
|
@property
|
||||||
|
@requiresinput('storage_path')
|
||||||
|
def storage_path(self, storage_path):
|
||||||
|
""" Returns the path of this blob in storage. """
|
||||||
|
# TODO: change this to take in the storage engine?
|
||||||
|
return storage_path
|
||||||
|
|
||||||
|
@property
|
||||||
|
@requiresinput('placements')
|
||||||
|
def placements(self, placements):
|
||||||
|
""" Returns all the storage placements at which the Blob can be found. """
|
||||||
|
return placements
|
||||||
|
|
||||||
|
|
||||||
|
class DerivedImage(datatype('DerivedImage', ['verb', 'varying_metadata', 'blob'])):
|
||||||
|
""" DerivedImage represents an image derived from a manifest via some form of verb. """
|
||||||
|
@classmethod
|
||||||
|
def for_derived_storage(cls, derived, verb, varying_metadata, blob):
|
||||||
|
return DerivedImage(db_id=derived.id,
|
||||||
|
verb=verb,
|
||||||
|
varying_metadata=varying_metadata,
|
||||||
|
blob=blob)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self):
|
||||||
|
""" Returns a unique ID for this derived image. This call will consistently produce the same
|
||||||
|
unique ID across calls in the same code base.
|
||||||
|
"""
|
||||||
|
return hashlib.sha256('%s:%s' % (self.verb, self._db_id)).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
class TorrentInfo(datatype('TorrentInfo', ['pieces', 'piece_length'])):
|
||||||
|
""" TorrentInfo represents information to pull a blob via torrent. """
|
||||||
|
@classmethod
|
||||||
|
def for_torrent_info(cls, torrent_info):
|
||||||
|
return TorrentInfo(db_id=torrent_info.id,
|
||||||
|
pieces=torrent_info.pieces,
|
||||||
|
piece_length=torrent_info.piece_length)
|
||||||
|
|
|
@ -26,11 +26,12 @@ class RegistryDataInterface(object):
|
||||||
or None if none. """
|
or None if none. """
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_manifest_for_tag(self, tag):
|
def get_manifest_for_tag(self, tag, backfill_if_necessary=False):
|
||||||
""" Returns the manifest associated with the given tag. """
|
""" Returns the manifest associated with the given tag. """
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def lookup_manifest_by_digest(self, repository_ref, manifest_digest, allow_dead=False):
|
def lookup_manifest_by_digest(self, repository_ref, manifest_digest, allow_dead=False,
|
||||||
|
include_legacy_image=False):
|
||||||
""" Looks up the manifest with the given digest under the given repository and returns it
|
""" Looks up the manifest with the given digest under the given repository and returns it
|
||||||
or None if none. """
|
or None if none. """
|
||||||
|
|
||||||
|
@ -131,3 +132,73 @@ class RegistryDataInterface(object):
|
||||||
NOTE: This method will only be necessary until we've completed the backfill, at which point
|
NOTE: This method will only be necessary until we've completed the backfill, at which point
|
||||||
it should be removed.
|
it should be removed.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def is_namespace_enabled(self, namespace_name):
|
||||||
|
""" Returns whether the given namespace exists and is enabled. """
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def list_manifest_layers(self, manifest, include_placements=False):
|
||||||
|
""" Returns an *ordered list* of the layers found in the manifest, starting at the base and
|
||||||
|
working towards the leaf, including the associated Blob and its placements (if specified).
|
||||||
|
Returns None if the manifest could not be parsed and validated.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def lookup_derived_image(self, manifest, verb, varying_metadata=None, include_placements=False):
|
||||||
|
"""
|
||||||
|
Looks up the derived image for the given manifest, verb and optional varying metadata and
|
||||||
|
returns it or None if none.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def lookup_or_create_derived_image(self, manifest, verb, storage_location, varying_metadata=None,
|
||||||
|
include_placements=False):
|
||||||
|
"""
|
||||||
|
Looks up the derived image for the given maniest, verb and optional varying metadata
|
||||||
|
and returns it. If none exists, a new derived image is created.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_derived_image_signature(self, derived_image, signer_name):
|
||||||
|
"""
|
||||||
|
Returns the signature associated with the derived image and a specific signer or None if none.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def set_derived_image_signature(self, derived_image, signer_name, signature):
|
||||||
|
"""
|
||||||
|
Sets the calculated signature for the given derived image and signer to that specified.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def delete_derived_image(self, derived_image):
|
||||||
|
"""
|
||||||
|
Deletes a derived image and all of its storage.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def set_derived_image_size(self, derived_image, compressed_size):
|
||||||
|
"""
|
||||||
|
Sets the compressed size on the given derived image.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_torrent_info(self, blob):
|
||||||
|
"""
|
||||||
|
Returns the torrent information associated with the given blob or None if none.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def set_torrent_info(self, blob, piece_length, pieces):
|
||||||
|
"""
|
||||||
|
Sets the torrent infomation associated with the given blob to that specified.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_repo_blob_by_digest(self, repo_ref, blob_digest, include_placements=False):
|
||||||
|
"""
|
||||||
|
Returns the blob in the repository with the given digest, if any or None if none. Note that
|
||||||
|
there may be multiple records in the same repository for the same blob digest, so the return
|
||||||
|
value of this function may change.
|
||||||
|
"""
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
# pylint: disable=protected-access
|
# pylint: disable=protected-access
|
||||||
|
import logging
|
||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
|
@ -8,8 +9,12 @@ from data import database
|
||||||
from data import model
|
from data import model
|
||||||
from data.registry_model.interface import RegistryDataInterface
|
from data.registry_model.interface import RegistryDataInterface
|
||||||
from data.registry_model.datatypes import (Tag, RepositoryReference, Manifest, LegacyImage, Label,
|
from data.registry_model.datatypes import (Tag, RepositoryReference, Manifest, LegacyImage, Label,
|
||||||
SecurityScanStatus)
|
SecurityScanStatus, ManifestLayer, Blob, DerivedImage,
|
||||||
from image.docker.schema1 import DockerSchema1ManifestBuilder
|
TorrentInfo)
|
||||||
|
from image.docker.schema1 import DockerSchema1ManifestBuilder, ManifestException
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class PreOCIModel(RegistryDataInterface):
|
class PreOCIModel(RegistryDataInterface):
|
||||||
|
@ -40,11 +45,14 @@ class PreOCIModel(RegistryDataInterface):
|
||||||
repo = model.repository.get_repository(namespace_name, repo_name, kind_filter=kind_filter)
|
repo = model.repository.get_repository(namespace_name, repo_name, kind_filter=kind_filter)
|
||||||
return RepositoryReference.for_repo_obj(repo)
|
return RepositoryReference.for_repo_obj(repo)
|
||||||
|
|
||||||
def get_manifest_for_tag(self, tag):
|
def get_manifest_for_tag(self, tag, backfill_if_necessary=False):
|
||||||
""" Returns the manifest associated with the given tag. """
|
""" Returns the manifest associated with the given tag. """
|
||||||
try:
|
try:
|
||||||
tag_manifest = database.TagManifest.get(tag_id=tag._db_id)
|
tag_manifest = database.TagManifest.get(tag_id=tag._db_id)
|
||||||
except database.TagManifest.DoesNotExist:
|
except database.TagManifest.DoesNotExist:
|
||||||
|
if backfill_if_necessary:
|
||||||
|
return self.backfill_manifest_for_tag(tag)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
return Manifest.for_tag_manifest(tag_manifest)
|
return Manifest.for_tag_manifest(tag_manifest)
|
||||||
|
@ -173,6 +181,7 @@ class PreOCIModel(RegistryDataInterface):
|
||||||
Returns the latest, *active* tag found in the repository, with the matching name
|
Returns the latest, *active* tag found in the repository, with the matching name
|
||||||
or None if none.
|
or None if none.
|
||||||
"""
|
"""
|
||||||
|
assert isinstance(tag_name, basestring)
|
||||||
tag = model.tag.get_active_tag_for_repo(repository_ref._db_id, tag_name)
|
tag = model.tag.get_active_tag_for_repo(repository_ref._db_id, tag_name)
|
||||||
if tag is None:
|
if tag is None:
|
||||||
return None
|
return None
|
||||||
|
@ -359,5 +368,204 @@ class PreOCIModel(RegistryDataInterface):
|
||||||
|
|
||||||
return Manifest.for_tag_manifest(tag_manifest)
|
return Manifest.for_tag_manifest(tag_manifest)
|
||||||
|
|
||||||
|
def is_namespace_enabled(self, namespace_name):
|
||||||
|
""" Returns whether the given namespace exists and is enabled. """
|
||||||
|
namespace = model.user.get_namespace_user(namespace_name)
|
||||||
|
return namespace is not None and namespace.enabled
|
||||||
|
|
||||||
|
def list_manifest_layers(self, manifest, include_placements=False):
|
||||||
|
""" Returns an *ordered list* of the layers found in the manifest, starting at the base and
|
||||||
|
working towards the leaf, including the associated Blob and its placements (if specified).
|
||||||
|
Returns None if the manifest could not be parsed and validated.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
parsed = manifest.get_parsed_manifest()
|
||||||
|
except ManifestException:
|
||||||
|
logger.exception('Could not parse and validate manifest `%s`', manifest._db_id)
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
tag_manifest = database.TagManifest.get(id=manifest._db_id)
|
||||||
|
except database.TagManifest.DoesNotExist:
|
||||||
|
logger.exception('Could not find tag manifest for manifest `%s`', manifest._db_id)
|
||||||
|
return None
|
||||||
|
|
||||||
|
repo = tag_manifest.tag.repository
|
||||||
|
blob_query = model.storage.lookup_repo_storages_by_content_checksum(repo, parsed.checksums)
|
||||||
|
storage_map = {blob.content_checksum: blob for blob in blob_query}
|
||||||
|
|
||||||
|
manifest_layers = []
|
||||||
|
for layer in parsed.layers:
|
||||||
|
digest_str = str(layer.digest)
|
||||||
|
if digest_str not in storage_map:
|
||||||
|
logger.error('Missing digest `%s` for manifest `%s`', layer.digest, manifest._db_id)
|
||||||
|
return None
|
||||||
|
|
||||||
|
image_storage = storage_map[digest_str]
|
||||||
|
assert image_storage.cas_path is not None
|
||||||
|
|
||||||
|
placements = None
|
||||||
|
if include_placements:
|
||||||
|
placements = list(model.storage.get_storage_locations(image_storage.uuid))
|
||||||
|
|
||||||
|
blob = Blob.for_image_storage(image_storage,
|
||||||
|
storage_path=model.storage.get_layer_path(image_storage),
|
||||||
|
placements=placements)
|
||||||
|
manifest_layers.append(ManifestLayer(layer, blob))
|
||||||
|
|
||||||
|
return manifest_layers
|
||||||
|
|
||||||
|
def lookup_derived_image(self, manifest, verb, varying_metadata=None, include_placements=False):
|
||||||
|
"""
|
||||||
|
Looks up the derived image for the given manifest, verb and optional varying metadata and
|
||||||
|
returns it or None if none.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
tag_manifest = database.TagManifest.get(id=manifest._db_id)
|
||||||
|
except database.TagManifest.DoesNotExist:
|
||||||
|
logger.exception('Could not find tag manifest for manifest `%s`', manifest._db_id)
|
||||||
|
return None
|
||||||
|
|
||||||
|
repo_image = tag_manifest.tag.image
|
||||||
|
derived = model.image.find_derived_storage_for_image(repo_image, verb, varying_metadata)
|
||||||
|
return self._build_derived(derived, verb, varying_metadata, include_placements)
|
||||||
|
|
||||||
|
def lookup_or_create_derived_image(self, manifest, verb, storage_location, varying_metadata=None,
|
||||||
|
include_placements=False):
|
||||||
|
"""
|
||||||
|
Looks up the derived image for the given maniest, verb and optional varying metadata
|
||||||
|
and returns it. If none exists, a new derived image is created.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
tag_manifest = database.TagManifest.get(id=manifest._db_id)
|
||||||
|
except database.TagManifest.DoesNotExist:
|
||||||
|
logger.exception('Could not find tag manifest for manifest `%s`', manifest._db_id)
|
||||||
|
return None
|
||||||
|
|
||||||
|
repo_image = tag_manifest.tag.image
|
||||||
|
derived = model.image.find_or_create_derived_storage(repo_image, verb, storage_location,
|
||||||
|
varying_metadata)
|
||||||
|
return self._build_derived(derived, verb, varying_metadata, include_placements)
|
||||||
|
|
||||||
|
def _build_derived(self, derived, verb, varying_metadata, include_placements):
|
||||||
|
if derived is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
derived_storage = derived.derivative
|
||||||
|
placements = None
|
||||||
|
if include_placements:
|
||||||
|
placements = list(model.storage.get_storage_locations(derived_storage.uuid))
|
||||||
|
|
||||||
|
blob = Blob.for_image_storage(derived_storage,
|
||||||
|
storage_path=model.storage.get_layer_path(derived_storage),
|
||||||
|
placements=placements)
|
||||||
|
|
||||||
|
return DerivedImage.for_derived_storage(derived, verb, varying_metadata, blob)
|
||||||
|
|
||||||
|
def get_derived_image_signature(self, derived_image, signer_name):
|
||||||
|
"""
|
||||||
|
Returns the signature associated with the derived image and a specific signer or None if none.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
derived_storage = database.DerivedStorageForImage.get(id=derived_image._db_id)
|
||||||
|
except database.DerivedStorageForImage.DoesNotExist:
|
||||||
|
return None
|
||||||
|
|
||||||
|
storage = derived_storage.derivative
|
||||||
|
signature_entry = model.storage.lookup_storage_signature(storage, signer_name)
|
||||||
|
if signature_entry is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return signature_entry.signature
|
||||||
|
|
||||||
|
def set_derived_image_signature(self, derived_image, signer_name, signature):
|
||||||
|
"""
|
||||||
|
Sets the calculated signature for the given derived image and signer to that specified.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
derived_storage = database.DerivedStorageForImage.get(id=derived_image._db_id)
|
||||||
|
except database.DerivedStorageForImage.DoesNotExist:
|
||||||
|
return None
|
||||||
|
|
||||||
|
storage = derived_storage.derivative
|
||||||
|
signature_entry = model.storage.find_or_create_storage_signature(storage, signer_name)
|
||||||
|
signature_entry.signature = signature
|
||||||
|
signature_entry.uploading = False
|
||||||
|
signature_entry.save()
|
||||||
|
|
||||||
|
def delete_derived_image(self, derived_image):
|
||||||
|
"""
|
||||||
|
Deletes a derived image and all of its storage.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
derived_storage = database.DerivedStorageForImage.get(id=derived_image._db_id)
|
||||||
|
except database.DerivedStorageForImage.DoesNotExist:
|
||||||
|
return None
|
||||||
|
|
||||||
|
model.image.delete_derived_storage(derived_storage)
|
||||||
|
|
||||||
|
def set_derived_image_size(self, derived_image, compressed_size):
|
||||||
|
"""
|
||||||
|
Sets the compressed size on the given derived image.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
derived_storage = database.DerivedStorageForImage.get(id=derived_image._db_id)
|
||||||
|
except database.DerivedStorageForImage.DoesNotExist:
|
||||||
|
return None
|
||||||
|
|
||||||
|
storage_entry = derived_storage.derivative
|
||||||
|
storage_entry.image_size = compressed_size
|
||||||
|
storage_entry.uploading = False
|
||||||
|
storage_entry.save()
|
||||||
|
|
||||||
|
def get_torrent_info(self, blob):
|
||||||
|
"""
|
||||||
|
Returns the torrent information associated with the given blob or None if none.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
image_storage = database.ImageStorage.get(id=blob._db_id)
|
||||||
|
except database.ImageStorage.DoesNotExist:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
torrent_info = model.storage.get_torrent_info(image_storage)
|
||||||
|
except model.TorrentInfoDoesNotExist:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return TorrentInfo.for_torrent_info(torrent_info)
|
||||||
|
|
||||||
|
def set_torrent_info(self, blob, piece_length, pieces):
|
||||||
|
"""
|
||||||
|
Sets the torrent infomation associated with the given blob to that specified.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
image_storage = database.ImageStorage.get(id=blob._db_id)
|
||||||
|
except database.ImageStorage.DoesNotExist:
|
||||||
|
return None
|
||||||
|
|
||||||
|
torrent_info = model.storage.save_torrent_info(image_storage, piece_length, pieces)
|
||||||
|
return TorrentInfo.for_torrent_info(torrent_info)
|
||||||
|
|
||||||
|
def get_repo_blob_by_digest(self, repo_ref, blob_digest, include_placements=False):
|
||||||
|
"""
|
||||||
|
Returns the blob in the repository with the given digest, if any or None if none. Note that
|
||||||
|
there may be multiple records in the same repository for the same blob digest, so the return
|
||||||
|
value of this function may change.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
image_storage = model.blob.get_repository_blob_by_digest(repo_ref._db_id, blob_digest)
|
||||||
|
except model.BlobDoesNotExist:
|
||||||
|
return None
|
||||||
|
|
||||||
|
assert image_storage.cas_path is not None
|
||||||
|
|
||||||
|
placements = None
|
||||||
|
if include_placements:
|
||||||
|
placements = list(model.storage.get_storage_locations(image_storage.uuid))
|
||||||
|
|
||||||
|
return Blob.for_image_storage(image_storage,
|
||||||
|
storage_path=model.storage.get_layer_path(image_storage),
|
||||||
|
placements=placements)
|
||||||
|
|
||||||
|
|
||||||
pre_oci_model = PreOCIModel()
|
pre_oci_model = PreOCIModel()
|
||||||
|
|
|
@ -8,7 +8,8 @@ from app import docker_v2_signing_key
|
||||||
from data import model
|
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)
|
TagManifestLabel, TagManifest, TagManifestLabel, DerivedStorageForImage,
|
||||||
|
TorrentInfo)
|
||||||
from data.registry_model.registry_pre_oci_model import PreOCIModel
|
from data.registry_model.registry_pre_oci_model import PreOCIModel
|
||||||
from data.registry_model.datatypes import RepositoryReference
|
from data.registry_model.datatypes import RepositoryReference
|
||||||
|
|
||||||
|
@ -34,6 +35,8 @@ def test_find_matching_tag(names, expected, pre_oci_model):
|
||||||
assert found is None
|
assert found is None
|
||||||
else:
|
else:
|
||||||
assert found.name in expected
|
assert found.name in expected
|
||||||
|
assert found.repository.namespace_name == 'devtable'
|
||||||
|
assert found.repository.name == 'simple'
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('repo_namespace, repo_name, expected', [
|
@pytest.mark.parametrize('repo_namespace, repo_name, expected', [
|
||||||
|
@ -363,3 +366,160 @@ def test_backfill_manifest_for_tag(repo_namespace, repo_name, clear_rows, pre_oc
|
||||||
parsed_manifest = manifest.get_parsed_manifest()
|
parsed_manifest = manifest.get_parsed_manifest()
|
||||||
assert parsed_manifest.leaf_layer_v1_image_id == legacy_image.docker_image_id
|
assert parsed_manifest.leaf_layer_v1_image_id == legacy_image.docker_image_id
|
||||||
assert parsed_manifest.parent_image_ids == {p.docker_image_id for p in legacy_image.parents}
|
assert parsed_manifest.parent_image_ids == {p.docker_image_id for p in legacy_image.parents}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('repo_namespace, repo_name', [
|
||||||
|
('devtable', 'simple'),
|
||||||
|
('devtable', 'complex'),
|
||||||
|
('devtable', 'history'),
|
||||||
|
('buynlarge', 'orgrepo'),
|
||||||
|
])
|
||||||
|
def test_backfill_manifest_on_lookup(repo_namespace, repo_name, clear_rows, pre_oci_model):
|
||||||
|
repository_ref = pre_oci_model.lookup_repository(repo_namespace, repo_name)
|
||||||
|
tags = pre_oci_model.list_repository_tags(repository_ref)
|
||||||
|
assert tags
|
||||||
|
|
||||||
|
for tag in tags:
|
||||||
|
assert not tag.manifest_digest
|
||||||
|
assert not pre_oci_model.get_manifest_for_tag(tag)
|
||||||
|
|
||||||
|
manifest = pre_oci_model.get_manifest_for_tag(tag, backfill_if_necessary=True)
|
||||||
|
assert manifest
|
||||||
|
|
||||||
|
updated_tag = pre_oci_model.get_repo_tag(repository_ref, tag.name)
|
||||||
|
assert updated_tag.manifest_digest == manifest.digest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('namespace, expect_enabled', [
|
||||||
|
('devtable', True),
|
||||||
|
('buynlarge', True),
|
||||||
|
|
||||||
|
('disabled', False),
|
||||||
|
])
|
||||||
|
def test_is_namespace_enabled(namespace, expect_enabled, pre_oci_model):
|
||||||
|
assert pre_oci_model.is_namespace_enabled(namespace) == expect_enabled
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('repo_namespace, repo_name', [
|
||||||
|
('devtable', 'simple'),
|
||||||
|
('devtable', 'complex'),
|
||||||
|
('devtable', 'history'),
|
||||||
|
('buynlarge', 'orgrepo'),
|
||||||
|
])
|
||||||
|
def test_list_manifest_layers(repo_namespace, repo_name, pre_oci_model):
|
||||||
|
repository_ref = pre_oci_model.lookup_repository(repo_namespace, repo_name)
|
||||||
|
tags = pre_oci_model.list_repository_tags(repository_ref)
|
||||||
|
assert tags
|
||||||
|
|
||||||
|
for tag in tags:
|
||||||
|
manifest = pre_oci_model.get_manifest_for_tag(tag)
|
||||||
|
assert manifest
|
||||||
|
|
||||||
|
with assert_query_count(4):
|
||||||
|
layers = pre_oci_model.list_manifest_layers(manifest)
|
||||||
|
assert layers
|
||||||
|
|
||||||
|
layers = pre_oci_model.list_manifest_layers(manifest, include_placements=True)
|
||||||
|
assert layers
|
||||||
|
|
||||||
|
parsed_layers = list(manifest.get_parsed_manifest().layers)
|
||||||
|
assert len(layers) == len(parsed_layers)
|
||||||
|
|
||||||
|
for index, manifest_layer in enumerate(layers):
|
||||||
|
assert manifest_layer.layer_info == parsed_layers[index]
|
||||||
|
assert manifest_layer.blob.digest == str(parsed_layers[index].digest)
|
||||||
|
assert manifest_layer.blob.storage_path
|
||||||
|
assert manifest_layer.blob.placements
|
||||||
|
|
||||||
|
repo_blob = pre_oci_model.get_repo_blob_by_digest(repository_ref, manifest_layer.blob.digest)
|
||||||
|
assert repo_blob.digest == manifest_layer.blob.digest
|
||||||
|
|
||||||
|
assert manifest_layer.estimated_size(1) is not None
|
||||||
|
|
||||||
|
|
||||||
|
def test_derived_image(pre_oci_model):
|
||||||
|
# Clear all existing derived storage.
|
||||||
|
DerivedStorageForImage.delete().execute()
|
||||||
|
|
||||||
|
repository_ref = pre_oci_model.lookup_repository('devtable', 'simple')
|
||||||
|
tag = pre_oci_model.get_repo_tag(repository_ref, 'latest')
|
||||||
|
manifest = pre_oci_model.get_manifest_for_tag(tag)
|
||||||
|
|
||||||
|
# Ensure the squashed image doesn't exist.
|
||||||
|
assert pre_oci_model.lookup_derived_image(manifest, 'squash', {}) is None
|
||||||
|
|
||||||
|
# Create a new one.
|
||||||
|
squashed = pre_oci_model.lookup_or_create_derived_image(manifest, 'squash', 'local_us', {})
|
||||||
|
assert pre_oci_model.lookup_or_create_derived_image(manifest, 'squash', 'local_us', {}) == squashed
|
||||||
|
assert squashed.unique_id
|
||||||
|
|
||||||
|
# Check and set the size.
|
||||||
|
assert squashed.blob.compressed_size is None
|
||||||
|
pre_oci_model.set_derived_image_size(squashed, 1234)
|
||||||
|
assert pre_oci_model.lookup_derived_image(manifest, 'squash', {}).blob.compressed_size == 1234
|
||||||
|
assert pre_oci_model.lookup_derived_image(manifest, 'squash', {}).unique_id == squashed.unique_id
|
||||||
|
|
||||||
|
# Ensure its returned now.
|
||||||
|
assert pre_oci_model.lookup_derived_image(manifest, 'squash', {}) == squashed
|
||||||
|
|
||||||
|
# Ensure different metadata results in a different derived image.
|
||||||
|
assert pre_oci_model.lookup_derived_image(manifest, 'squash', {'foo': 'bar'}) is None
|
||||||
|
|
||||||
|
squashed_foo = pre_oci_model.lookup_or_create_derived_image(manifest, 'squash', 'local_us',
|
||||||
|
{'foo': 'bar'})
|
||||||
|
assert squashed_foo != squashed
|
||||||
|
assert pre_oci_model.lookup_derived_image(manifest, 'squash', {'foo': 'bar'}) == squashed_foo
|
||||||
|
|
||||||
|
assert squashed.unique_id != squashed_foo.unique_id
|
||||||
|
|
||||||
|
# Lookup with placements.
|
||||||
|
squashed = pre_oci_model.lookup_or_create_derived_image(manifest, 'squash', 'local_us', {},
|
||||||
|
include_placements=True)
|
||||||
|
assert squashed.blob.placements
|
||||||
|
|
||||||
|
# Delete the derived image.
|
||||||
|
pre_oci_model.delete_derived_image(squashed)
|
||||||
|
assert pre_oci_model.lookup_derived_image(manifest, 'squash', {}) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_derived_image_signatures(pre_oci_model):
|
||||||
|
repository_ref = pre_oci_model.lookup_repository('devtable', 'simple')
|
||||||
|
tag = pre_oci_model.get_repo_tag(repository_ref, 'latest')
|
||||||
|
manifest = pre_oci_model.get_manifest_for_tag(tag)
|
||||||
|
|
||||||
|
derived = pre_oci_model.lookup_derived_image(manifest, 'squash', {})
|
||||||
|
assert derived
|
||||||
|
|
||||||
|
signature = pre_oci_model.get_derived_image_signature(derived, 'gpg2')
|
||||||
|
assert signature is None
|
||||||
|
|
||||||
|
pre_oci_model.set_derived_image_signature(derived, 'gpg2', 'foo')
|
||||||
|
assert pre_oci_model.get_derived_image_signature(derived, 'gpg2') == 'foo'
|
||||||
|
|
||||||
|
|
||||||
|
def test_torrent_info(pre_oci_model):
|
||||||
|
# Remove all existing info.
|
||||||
|
TorrentInfo.delete().execute()
|
||||||
|
|
||||||
|
repository_ref = pre_oci_model.lookup_repository('devtable', 'simple')
|
||||||
|
tag = pre_oci_model.get_repo_tag(repository_ref, 'latest')
|
||||||
|
manifest = pre_oci_model.get_manifest_for_tag(tag)
|
||||||
|
|
||||||
|
layers = pre_oci_model.list_manifest_layers(manifest)
|
||||||
|
assert layers
|
||||||
|
|
||||||
|
assert pre_oci_model.get_torrent_info(layers[0].blob) is None
|
||||||
|
pre_oci_model.set_torrent_info(layers[0].blob, 2, 'foo')
|
||||||
|
|
||||||
|
torrent_info = pre_oci_model.get_torrent_info(layers[0].blob)
|
||||||
|
assert torrent_info is not None
|
||||||
|
assert torrent_info.piece_length == 2
|
||||||
|
assert torrent_info.pieces == 'foo'
|
||||||
|
|
||||||
|
# Try setting it again. Nothing should happen.
|
||||||
|
pre_oci_model.set_torrent_info(layers[0].blob, 3, 'bar')
|
||||||
|
|
||||||
|
torrent_info = pre_oci_model.get_torrent_info(layers[0].blob)
|
||||||
|
assert torrent_info is not None
|
||||||
|
assert torrent_info.piece_length == 2
|
||||||
|
assert torrent_info.pieces == 'foo'
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
Joseph Schorr <joseph.schorr@coreos.com> (@josephschorr)
|
|
|
@ -10,13 +10,14 @@ from auth.auth_context import get_authenticated_user
|
||||||
from auth.decorators import process_auth
|
from auth.decorators import process_auth
|
||||||
from auth.permissions import ReadRepositoryPermission
|
from auth.permissions import ReadRepositoryPermission
|
||||||
from data import database
|
from data import database
|
||||||
|
from data import model
|
||||||
|
from data.registry_model import registry_model
|
||||||
from endpoints.decorators import anon_protect, anon_allowed, route_show_if, parse_repository_name
|
from endpoints.decorators import anon_protect, anon_allowed, route_show_if, parse_repository_name
|
||||||
from endpoints.verbs.models_pre_oci import pre_oci_model as model
|
|
||||||
from endpoints.v2.blob import BLOB_DIGEST_ROUTE
|
from endpoints.v2.blob import BLOB_DIGEST_ROUTE
|
||||||
from image.appc import AppCImageFormatter
|
from image.appc import AppCImageFormatter
|
||||||
from image.docker.squashed import SquashedDockerImageFormatter
|
from image.docker.squashed import SquashedDockerImageFormatter
|
||||||
from storage import Storage
|
from storage import Storage
|
||||||
from util.audit import track_and_log
|
from util.audit import track_and_log, wrap_repository
|
||||||
from util.http import exact_abort
|
from util.http import exact_abort
|
||||||
from util.registry.filelike import wrap_with_handler
|
from util.registry.filelike import wrap_with_handler
|
||||||
from util.registry.queuefile import QueueFile
|
from util.registry.queuefile import QueueFile
|
||||||
|
@ -40,7 +41,7 @@ class VerbReporter(TarLayerFormatterReporter):
|
||||||
metric_queue.verb_action_passes.Inc(labelvalues=[self.kind, pass_count])
|
metric_queue.verb_action_passes.Inc(labelvalues=[self.kind, pass_count])
|
||||||
|
|
||||||
|
|
||||||
def _open_stream(formatter, repo_image, tag, derived_image_id, handlers, reporter):
|
def _open_stream(formatter, tag, manifest, derived_image_id, handlers, reporter):
|
||||||
"""
|
"""
|
||||||
This method generates a stream of data which will be replicated and read from the queue files.
|
This method generates a stream of data which will be replicated and read from the queue files.
|
||||||
This method runs in a separate process.
|
This method runs in a separate process.
|
||||||
|
@ -48,29 +49,25 @@ def _open_stream(formatter, repo_image, tag, derived_image_id, handlers, reporte
|
||||||
# For performance reasons, we load the full image list here, cache it, then disconnect from
|
# For performance reasons, we load the full image list here, cache it, then disconnect from
|
||||||
# the database.
|
# the database.
|
||||||
with database.UseThenDisconnect(app.config):
|
with database.UseThenDisconnect(app.config):
|
||||||
image_list = list(model.get_manifest_layers_with_blobs(repo_image))
|
layers = registry_model.list_manifest_layers(manifest, include_placements=True)
|
||||||
|
|
||||||
def get_next_image():
|
def image_stream_getter(store, blob):
|
||||||
for current_image in image_list:
|
|
||||||
yield current_image
|
|
||||||
|
|
||||||
def image_stream_getter(store, current_image):
|
|
||||||
def get_stream_for_storage():
|
def get_stream_for_storage():
|
||||||
current_image_path = model.get_blob_path(current_image.blob)
|
current_image_stream = store.stream_read_file(blob.placements, blob.storage_path)
|
||||||
current_image_stream = store.stream_read_file(current_image.blob.locations,
|
logger.debug('Returning blob %s: %s', blob.digest, blob.storage_path)
|
||||||
current_image_path)
|
|
||||||
|
|
||||||
logger.debug('Returning image layer %s: %s', current_image.image_id, current_image_path)
|
|
||||||
return current_image_stream
|
return current_image_stream
|
||||||
return get_stream_for_storage
|
return get_stream_for_storage
|
||||||
|
|
||||||
def tar_stream_getter_iterator():
|
def tar_stream_getter_iterator():
|
||||||
# Re-Initialize the storage engine because some may not respond well to forking (e.g. S3)
|
# Re-Initialize the storage engine because some may not respond well to forking (e.g. S3)
|
||||||
store = Storage(app, metric_queue, config_provider=config_provider, ip_resolver=ip_resolver)
|
store = Storage(app, metric_queue, config_provider=config_provider, ip_resolver=ip_resolver)
|
||||||
for current_image in image_list:
|
|
||||||
yield image_stream_getter(store, current_image)
|
|
||||||
|
|
||||||
stream = formatter.build_stream(repo_image, tag, derived_image_id, get_next_image,
|
# Note: We reverse because we have to start at the leaf layer and move upward,
|
||||||
|
# as per the spec for the formatters.
|
||||||
|
for layer in reversed(layers):
|
||||||
|
yield image_stream_getter(store, layer.blob)
|
||||||
|
|
||||||
|
stream = formatter.build_stream(tag, manifest, derived_image_id, layers,
|
||||||
tar_stream_getter_iterator, reporter=reporter)
|
tar_stream_getter_iterator, reporter=reporter)
|
||||||
|
|
||||||
for handler_fn in handlers:
|
for handler_fn in handlers:
|
||||||
|
@ -86,14 +83,14 @@ def _sign_derived_image(verb, derived_image, queue_file):
|
||||||
try:
|
try:
|
||||||
signature = signer.detached_sign(queue_file)
|
signature = signer.detached_sign(queue_file)
|
||||||
except:
|
except:
|
||||||
logger.exception('Exception when signing %s deriving image %s', verb, derived_image.ref)
|
logger.exception('Exception when signing %s deriving image %s', verb, derived_image)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Setup the database (since this is a new process) and then disconnect immediately
|
# Setup the database (since this is a new process) and then disconnect immediately
|
||||||
# once the operation completes.
|
# once the operation completes.
|
||||||
if not queue_file.raised_exception:
|
if not queue_file.raised_exception:
|
||||||
with database.UseThenDisconnect(app.config):
|
with database.UseThenDisconnect(app.config):
|
||||||
model.set_derived_image_signature(derived_image, signer.name, signature)
|
registry_model.set_derived_image_signature(derived_image, signer.name, signature)
|
||||||
|
|
||||||
|
|
||||||
def _write_derived_image_to_storage(verb, derived_image, queue_file):
|
def _write_derived_image_to_storage(verb, derived_image, queue_file):
|
||||||
|
@ -102,17 +99,16 @@ def _write_derived_image_to_storage(verb, derived_image, queue_file):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def handle_exception(ex):
|
def handle_exception(ex):
|
||||||
logger.debug('Exception when building %s derived image %s: %s', verb, derived_image.ref, ex)
|
logger.debug('Exception when building %s derived image %s: %s', verb, derived_image, ex)
|
||||||
|
|
||||||
with database.UseThenDisconnect(app.config):
|
with database.UseThenDisconnect(app.config):
|
||||||
model.delete_derived_image(derived_image)
|
registry_model.delete_derived_image(derived_image)
|
||||||
|
|
||||||
queue_file.add_exception_handler(handle_exception)
|
queue_file.add_exception_handler(handle_exception)
|
||||||
|
|
||||||
# Re-Initialize the storage engine because some may not respond well to forking (e.g. S3)
|
# Re-Initialize the storage engine because some may not respond well to forking (e.g. S3)
|
||||||
store = Storage(app, metric_queue, config_provider=config_provider, ip_resolver=ip_resolver)
|
store = Storage(app, metric_queue, config_provider=config_provider, ip_resolver=ip_resolver)
|
||||||
image_path = model.get_blob_path(derived_image.blob)
|
store.stream_write(derived_image.blob.placements, derived_image.blob.storage_path, queue_file)
|
||||||
store.stream_write(derived_image.blob.locations, image_path, queue_file)
|
|
||||||
queue_file.close()
|
queue_file.close()
|
||||||
|
|
||||||
|
|
||||||
|
@ -121,17 +117,16 @@ def _torrent_for_blob(blob, is_public):
|
||||||
with an error if the state is not valid (e.g. non-public, non-user request).
|
with an error if the state is not valid (e.g. non-public, non-user request).
|
||||||
"""
|
"""
|
||||||
# Make sure the storage has a size.
|
# Make sure the storage has a size.
|
||||||
if not blob.size:
|
if not blob.compressed_size:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
# Lookup the torrent information for the storage.
|
# Lookup the torrent information for the storage.
|
||||||
torrent_info = model.get_torrent_info(blob)
|
torrent_info = registry_model.get_torrent_info(blob)
|
||||||
if torrent_info is None:
|
if torrent_info is None:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
# Lookup the webseed path for the storage.
|
# Lookup the webseed path for the storage.
|
||||||
path = model.get_blob_path(blob)
|
webseed = storage.get_direct_download_url(blob.placements, blob.storage_path,
|
||||||
webseed = storage.get_direct_download_url(blob.locations, path,
|
|
||||||
expires_in=app.config['BITTORRENT_WEBSEED_LIFETIME'])
|
expires_in=app.config['BITTORRENT_WEBSEED_LIFETIME'])
|
||||||
if webseed is None:
|
if webseed is None:
|
||||||
# We cannot support webseeds for storages that cannot provide direct downloads.
|
# We cannot support webseeds for storages that cannot provide direct downloads.
|
||||||
|
@ -151,8 +146,8 @@ def _torrent_for_blob(blob, is_public):
|
||||||
name = per_user_torrent_filename(torrent_config, user.uuid, blob.uuid)
|
name = per_user_torrent_filename(torrent_config, user.uuid, blob.uuid)
|
||||||
|
|
||||||
# Return the torrent file.
|
# Return the torrent file.
|
||||||
torrent_file = make_torrent(torrent_config, name, webseed, blob.size, torrent_info.piece_length,
|
torrent_file = make_torrent(torrent_config, name, webseed, blob.compressed_size,
|
||||||
torrent_info.pieces)
|
torrent_info.piece_length, torrent_info.pieces)
|
||||||
|
|
||||||
headers = {
|
headers = {
|
||||||
'Content-Type': 'application/x-bittorrent',
|
'Content-Type': 'application/x-bittorrent',
|
||||||
|
@ -161,7 +156,7 @@ def _torrent_for_blob(blob, is_public):
|
||||||
return make_response(torrent_file, 200, headers)
|
return make_response(torrent_file, 200, headers)
|
||||||
|
|
||||||
|
|
||||||
def _torrent_repo_verb(repo_image, tag, verb, **kwargs):
|
def _torrent_repo_verb(repository, tag, manifest, verb, **kwargs):
|
||||||
""" Handles returning a torrent for the given verb on the given image and tag. """
|
""" Handles returning a torrent for the given verb on the given image and tag. """
|
||||||
if not features.BITTORRENT:
|
if not features.BITTORRENT:
|
||||||
# Torrent feature is not enabled.
|
# Torrent feature is not enabled.
|
||||||
|
@ -169,60 +164,73 @@ def _torrent_repo_verb(repo_image, tag, verb, **kwargs):
|
||||||
|
|
||||||
# Lookup an *existing* derived storage for the verb. If the verb's image storage doesn't exist,
|
# Lookup an *existing* derived storage for the verb. If the verb's image storage doesn't exist,
|
||||||
# we cannot create it here, so we 406.
|
# we cannot create it here, so we 406.
|
||||||
derived_image = model.lookup_derived_image(repo_image, verb, varying_metadata={'tag': tag})
|
derived_image = registry_model.lookup_derived_image(manifest, verb,
|
||||||
|
varying_metadata={'tag': tag.name},
|
||||||
|
include_placements=True)
|
||||||
if derived_image is None:
|
if derived_image is None:
|
||||||
abort(406)
|
abort(406)
|
||||||
|
|
||||||
# Return the torrent.
|
# Return the torrent.
|
||||||
repo = model.get_repository(repo_image.repository.namespace_name, repo_image.repository.name)
|
torrent = _torrent_for_blob(derived_image.blob, model.repository.is_repository_public(repository))
|
||||||
repo_is_public = repo is not None and repo.is_public
|
|
||||||
torrent = _torrent_for_blob(derived_image.blob, repo_is_public)
|
|
||||||
|
|
||||||
# Log the action.
|
# Log the action.
|
||||||
track_and_log('repo_verb', repo_image.repository, tag=tag, verb=verb, torrent=True, **kwargs)
|
track_and_log('repo_verb', wrap_repository(repository), tag=tag.name, verb=verb, torrent=True,
|
||||||
|
**kwargs)
|
||||||
return torrent
|
return torrent
|
||||||
|
|
||||||
|
|
||||||
def _verify_repo_verb(_, namespace, repo_name, tag, verb, checker=None):
|
def _verify_repo_verb(_, namespace, repo_name, tag_name, verb, checker=None):
|
||||||
permission = ReadRepositoryPermission(namespace, repo_name)
|
permission = ReadRepositoryPermission(namespace, repo_name)
|
||||||
repo = model.get_repository(namespace, repo_name)
|
repo = model.repository.get_repository(namespace, repo_name)
|
||||||
repo_is_public = repo is not None and repo.is_public
|
repo_is_public = repo is not None and model.repository.is_repository_public(repo)
|
||||||
if not permission.can() and not repo_is_public:
|
if not permission.can() and not repo_is_public:
|
||||||
logger.debug('No permission to read repository %s/%s for user %s with verb %s', namespace,
|
logger.debug('No permission to read repository %s/%s for user %s with verb %s', namespace,
|
||||||
repo_name, get_authenticated_user(), verb)
|
repo_name, get_authenticated_user(), verb)
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
# Lookup the requested tag.
|
if repo is not None and repo.kind.name != 'image':
|
||||||
tag_image = model.get_tag_image(namespace, repo_name, tag)
|
|
||||||
if tag_image is None:
|
|
||||||
logger.debug('Tag %s does not exist in repository %s/%s for user %s', tag, namespace, repo_name,
|
|
||||||
get_authenticated_user())
|
|
||||||
abort(404)
|
|
||||||
|
|
||||||
if repo is not None and repo.kind != 'image':
|
|
||||||
logger.debug('Repository %s/%s for user %s is not an image repo', namespace, repo_name,
|
logger.debug('Repository %s/%s for user %s is not an image repo', namespace, repo_name,
|
||||||
get_authenticated_user())
|
get_authenticated_user())
|
||||||
abort(405)
|
abort(405)
|
||||||
|
|
||||||
# Make sure the repo's namespace isn't disabled.
|
# Make sure the repo's namespace isn't disabled.
|
||||||
if not model.is_namespace_enabled(namespace):
|
if not registry_model.is_namespace_enabled(namespace):
|
||||||
abort(400)
|
abort(400)
|
||||||
|
|
||||||
|
# Lookup the requested tag.
|
||||||
|
repo_ref = registry_model.lookup_repository(namespace, repo_name)
|
||||||
|
tag = registry_model.get_repo_tag(repo_ref, tag_name)
|
||||||
|
if tag is None:
|
||||||
|
logger.debug('Tag %s does not exist in repository %s/%s for user %s', tag, namespace, repo_name,
|
||||||
|
get_authenticated_user())
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
# Get its associated manifest.
|
||||||
|
manifest = registry_model.get_manifest_for_tag(tag, backfill_if_necessary=True)
|
||||||
|
if manifest is None:
|
||||||
|
logger.debug('Could not get manifest on %s/%s:%s::%s', namespace, repo_name, tag.name, verb)
|
||||||
|
abort(404)
|
||||||
|
|
||||||
# If there is a data checker, call it first.
|
# If there is a data checker, call it first.
|
||||||
if checker is not None:
|
if checker is not None:
|
||||||
if not checker(tag_image):
|
if not checker(tag, manifest):
|
||||||
logger.debug('Check mismatch on %s/%s:%s, verb %s', namespace, repo_name, tag, verb)
|
logger.debug('Check mismatch on %s/%s:%s, verb %s', namespace, repo_name, tag.name, verb)
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
return tag_image
|
# Preload the tag's repository information, so it gets cached.
|
||||||
|
assert tag.repository.namespace_name
|
||||||
|
assert tag.repository.name
|
||||||
|
|
||||||
|
return tag, manifest
|
||||||
|
|
||||||
|
|
||||||
def _repo_verb_signature(namespace, repository, tag, verb, checker=None, **kwargs):
|
def _repo_verb_signature(namespace, repository, tag_name, verb, checker=None, **kwargs):
|
||||||
# Verify that the image exists and that we have access to it.
|
# Verify that the tag exists and that we have access to it.
|
||||||
repo_image = _verify_repo_verb(storage, namespace, repository, tag, verb, checker)
|
tag, manifest = _verify_repo_verb(storage, namespace, repository, tag_name, verb, checker)
|
||||||
|
|
||||||
# derived_image the derived image storage for the verb.
|
# Find the derived image storage for the verb.
|
||||||
derived_image = model.lookup_derived_image(repo_image, verb, varying_metadata={'tag': tag})
|
derived_image = registry_model.lookup_derived_image(manifest, verb,
|
||||||
|
varying_metadata={'tag': tag.name})
|
||||||
if derived_image is None or derived_image.blob.uploading:
|
if derived_image is None or derived_image.blob.uploading:
|
||||||
return make_response('', 202)
|
return make_response('', 202)
|
||||||
|
|
||||||
|
@ -231,7 +239,7 @@ def _repo_verb_signature(namespace, repository, tag, verb, checker=None, **kwarg
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
# Lookup the signature for the verb.
|
# Lookup the signature for the verb.
|
||||||
signature_value = model.get_derived_image_signature(derived_image, signer.name)
|
signature_value = registry_model.get_derived_image_signature(derived_image, signer.name)
|
||||||
if signature_value is None:
|
if signature_value is None:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
|
@ -239,53 +247,57 @@ def _repo_verb_signature(namespace, repository, tag, verb, checker=None, **kwarg
|
||||||
return make_response(signature_value)
|
return make_response(signature_value)
|
||||||
|
|
||||||
|
|
||||||
def _repo_verb(namespace, repository, tag, verb, formatter, sign=False, checker=None, **kwargs):
|
def _repo_verb(namespace, repository, tag_name, verb, formatter, sign=False, checker=None,
|
||||||
|
**kwargs):
|
||||||
# Verify that the image exists and that we have access to it.
|
# Verify that the image exists and that we have access to it.
|
||||||
logger.debug('Verifying repo verb %s for repository %s/%s with user %s with mimetype %s',
|
logger.debug('Verifying repo verb %s for repository %s/%s with user %s with mimetype %s',
|
||||||
verb, namespace, repository, get_authenticated_user(), request.accept_mimetypes.best)
|
verb, namespace, repository, get_authenticated_user(), request.accept_mimetypes.best)
|
||||||
repo_image = _verify_repo_verb(storage, namespace, repository, tag, verb, checker)
|
tag, manifest = _verify_repo_verb(storage, namespace, repository, tag_name, verb, checker)
|
||||||
|
|
||||||
|
# Load the repository for later.
|
||||||
|
repo = model.repository.get_repository(namespace, repository)
|
||||||
|
if repo is None:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
# Check for torrent. If found, we return a torrent for the repo verb image (if the derived
|
# Check for torrent. If found, we return a torrent for the repo verb image (if the derived
|
||||||
# image already exists).
|
# image already exists).
|
||||||
if request.accept_mimetypes.best == 'application/x-bittorrent':
|
if request.accept_mimetypes.best == 'application/x-bittorrent':
|
||||||
metric_queue.repository_pull.Inc(labelvalues=[namespace, repository, verb + '+torrent', True])
|
metric_queue.repository_pull.Inc(labelvalues=[namespace, repository, verb + '+torrent', True])
|
||||||
return _torrent_repo_verb(repo_image, tag, verb, **kwargs)
|
return _torrent_repo_verb(repo, tag, manifest, verb, **kwargs)
|
||||||
|
|
||||||
# Log the action.
|
# Log the action.
|
||||||
track_and_log('repo_verb', repo_image.repository, tag=tag, verb=verb, **kwargs)
|
track_and_log('repo_verb', wrap_repository(repo), tag=tag.name, verb=verb, **kwargs)
|
||||||
metric_queue.repository_pull.Inc(labelvalues=[namespace, repository, verb, True])
|
metric_queue.repository_pull.Inc(labelvalues=[namespace, repository, verb, True])
|
||||||
|
|
||||||
# Lookup/create the derived image for the verb and repo image.
|
# Lookup/create the derived image for the verb and repo image.
|
||||||
derived_image = model.lookup_or_create_derived_image(
|
derived_image = registry_model.lookup_or_create_derived_image(
|
||||||
repo_image, verb, storage.preferred_locations[0], varying_metadata={'tag': tag})
|
manifest, verb, storage.preferred_locations[0], varying_metadata={'tag': tag.name},
|
||||||
|
include_placements=True)
|
||||||
if not derived_image.blob.uploading:
|
if not derived_image.blob.uploading:
|
||||||
logger.debug('Derived %s image %s exists in storage', verb, derived_image.ref)
|
logger.debug('Derived %s image %s exists in storage', verb, derived_image)
|
||||||
derived_layer_path = model.get_blob_path(derived_image.blob)
|
|
||||||
is_head_request = request.method == 'HEAD'
|
is_head_request = request.method == 'HEAD'
|
||||||
download_url = storage.get_direct_download_url(derived_image.blob.locations,
|
download_url = storage.get_direct_download_url(derived_image.blob.placements,
|
||||||
derived_layer_path, head=is_head_request)
|
derived_image.blob.storage_path,
|
||||||
|
head=is_head_request)
|
||||||
if download_url:
|
if download_url:
|
||||||
logger.debug('Redirecting to download URL for derived %s image %s', verb, derived_image.ref)
|
logger.debug('Redirecting to download URL for derived %s image %s', verb, derived_image)
|
||||||
return redirect(download_url)
|
return redirect(download_url)
|
||||||
|
|
||||||
# Close the database handle here for this process before we send the long download.
|
# Close the database handle here for this process before we send the long download.
|
||||||
database.close_db_filter(None)
|
database.close_db_filter(None)
|
||||||
|
|
||||||
logger.debug('Sending cached derived %s image %s', verb, derived_image.ref)
|
logger.debug('Sending cached derived %s image %s', verb, derived_image)
|
||||||
return send_file(
|
return send_file(
|
||||||
storage.stream_read_file(derived_image.blob.locations, derived_layer_path),
|
storage.stream_read_file(derived_image.blob.placements, derived_image.blob.storage_path),
|
||||||
mimetype=LAYER_MIMETYPE)
|
mimetype=LAYER_MIMETYPE)
|
||||||
|
|
||||||
logger.debug('Building and returning derived %s image %s', verb, derived_image.ref)
|
logger.debug('Building and returning derived %s image %s', verb, derived_image)
|
||||||
|
|
||||||
# Close the database connection before any process forking occurs. This is important because
|
# Close the database connection before any process forking occurs. This is important because
|
||||||
# the Postgres driver does not react kindly to forking, so we need to make sure it is closed
|
# the Postgres driver does not react kindly to forking, so we need to make sure it is closed
|
||||||
# so that each process will get its own unique connection.
|
# so that each process will get its own unique connection.
|
||||||
database.close_db_filter(None)
|
database.close_db_filter(None)
|
||||||
|
|
||||||
# Calculate a derived image ID.
|
|
||||||
derived_image_id = hashlib.sha256(repo_image.image_id + ':' + verb).hexdigest()
|
|
||||||
|
|
||||||
def _cleanup():
|
def _cleanup():
|
||||||
# Close any existing DB connection once the process has exited.
|
# Close any existing DB connection once the process has exited.
|
||||||
database.close_db_filter(None)
|
database.close_db_filter(None)
|
||||||
|
@ -294,15 +306,15 @@ def _repo_verb(namespace, repository, tag, verb, formatter, sign=False, checker=
|
||||||
|
|
||||||
def _store_metadata_and_cleanup():
|
def _store_metadata_and_cleanup():
|
||||||
with database.UseThenDisconnect(app.config):
|
with database.UseThenDisconnect(app.config):
|
||||||
model.set_torrent_info(derived_image.blob, app.config['BITTORRENT_PIECE_SIZE'],
|
registry_model.set_torrent_info(derived_image.blob, app.config['BITTORRENT_PIECE_SIZE'],
|
||||||
hasher.final_piece_hashes())
|
hasher.final_piece_hashes())
|
||||||
model.set_blob_size(derived_image.blob, hasher.hashed_bytes)
|
registry_model.set_derived_image_size(derived_image, hasher.hashed_bytes)
|
||||||
|
|
||||||
# Create a queue process to generate the data. The queue files will read from the process
|
# Create a queue process to generate the data. The queue files will read from the process
|
||||||
# and send the results to the client and storage.
|
# and send the results to the client and storage.
|
||||||
handlers = [hasher.update]
|
handlers = [hasher.update]
|
||||||
reporter = VerbReporter(verb)
|
reporter = VerbReporter(verb)
|
||||||
args = (formatter, repo_image, tag, derived_image_id, handlers, reporter)
|
args = (formatter, tag, manifest, derived_image.unique_id, handlers, reporter)
|
||||||
queue_process = QueueProcess(
|
queue_process = QueueProcess(
|
||||||
_open_stream,
|
_open_stream,
|
||||||
8 * 1024,
|
8 * 1024,
|
||||||
|
@ -337,8 +349,8 @@ def _repo_verb(namespace, repository, tag, verb, formatter, sign=False, checker=
|
||||||
|
|
||||||
|
|
||||||
def os_arch_checker(os, arch):
|
def os_arch_checker(os, arch):
|
||||||
def checker(repo_image):
|
def checker(tag, manifest):
|
||||||
image_json = repo_image.compat_metadata
|
image_json = manifest.leaf_layer.v1_metadata
|
||||||
|
|
||||||
# Verify the architecture and os.
|
# Verify the architecture and os.
|
||||||
operating_system = image_json.get('os', 'linux')
|
operating_system = image_json.get('os', 'linux')
|
||||||
|
@ -394,8 +406,8 @@ def get_squashed_tag(namespace, repository, tag):
|
||||||
@process_auth
|
@process_auth
|
||||||
@parse_repository_name()
|
@parse_repository_name()
|
||||||
def get_tag_torrent(namespace_name, repo_name, digest):
|
def get_tag_torrent(namespace_name, repo_name, digest):
|
||||||
repo = model.get_repository(namespace_name, repo_name)
|
repo = model.repository.get_repository(namespace_name, repo_name)
|
||||||
repo_is_public = repo is not None and repo.is_public
|
repo_is_public = repo is not None and model.repository.is_repository_public(repo)
|
||||||
|
|
||||||
permission = ReadRepositoryPermission(namespace_name, repo_name)
|
permission = ReadRepositoryPermission(namespace_name, repo_name)
|
||||||
if not permission.can() and not repo_is_public:
|
if not permission.can() and not repo_is_public:
|
||||||
|
@ -406,10 +418,14 @@ def get_tag_torrent(namespace_name, repo_name, digest):
|
||||||
# We can not generate a private torrent cluster without a user uuid (e.g. token auth)
|
# We can not generate a private torrent cluster without a user uuid (e.g. token auth)
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
if repo is not None and repo.kind != 'image':
|
if repo is not None and repo.kind.name != 'image':
|
||||||
abort(405)
|
abort(405)
|
||||||
|
|
||||||
blob = model.get_repo_blob_by_digest(namespace_name, repo_name, digest)
|
repo_ref = registry_model.lookup_repository(namespace_name, repo_name)
|
||||||
|
if repo_ref is None:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
blob = registry_model.get_repo_blob_by_digest(repo_ref, digest, include_placements=True)
|
||||||
if blob is None:
|
if blob is None:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
|
|
|
@ -1,159 +0,0 @@
|
||||||
from abc import ABCMeta, abstractmethod
|
|
||||||
from collections import namedtuple
|
|
||||||
|
|
||||||
from six import add_metaclass
|
|
||||||
|
|
||||||
|
|
||||||
class Repository(
|
|
||||||
namedtuple('Repository', ['id', 'name', 'namespace_name', 'description', 'is_public',
|
|
||||||
'kind'])):
|
|
||||||
"""
|
|
||||||
Repository represents a namespaced collection of tags.
|
|
||||||
:type id: int
|
|
||||||
:type name: string
|
|
||||||
:type namespace_name: string
|
|
||||||
:type description: string
|
|
||||||
:type is_public: bool
|
|
||||||
:type kind: string
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class DerivedImage(namedtuple('DerivedImage', ['ref', 'blob', 'internal_source_image_db_id'])):
|
|
||||||
"""
|
|
||||||
DerivedImage represents a user-facing alias for an image which was derived from another image.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class RepositoryReference(namedtuple('RepositoryReference', ['id', 'name', 'namespace_name'])):
|
|
||||||
"""
|
|
||||||
RepositoryReference represents a reference to a Repository, without its full metadata.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class ImageWithBlob(
|
|
||||||
namedtuple('Image', [
|
|
||||||
'image_id', 'blob', 'compat_metadata', 'repository', 'internal_db_id', 'v1_metadata'])):
|
|
||||||
"""
|
|
||||||
ImageWithBlob represents a user-facing alias for referencing an image, along with its blob.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class Blob(namedtuple('Blob', ['uuid', 'size', 'uncompressed_size', 'uploading', 'locations'])):
|
|
||||||
"""
|
|
||||||
Blob represents an opaque binary blob saved to the storage system.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class TorrentInfo(namedtuple('TorrentInfo', ['piece_length', 'pieces'])):
|
|
||||||
"""
|
|
||||||
TorrentInfo represents the torrent piece information associated with a blob.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
@add_metaclass(ABCMeta)
|
|
||||||
class VerbsDataInterface(object):
|
|
||||||
"""
|
|
||||||
Interface that represents all data store interactions required by the registry's custom HTTP
|
|
||||||
verbs.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_repository(self, namespace_name, repo_name):
|
|
||||||
"""
|
|
||||||
Returns a repository tuple for the repository with the given name under the given namespace.
|
|
||||||
Returns None if no such repository was found.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_manifest_layers_with_blobs(self, repo_image):
|
|
||||||
"""
|
|
||||||
Returns the full set of manifest layers and their associated blobs starting at the given
|
|
||||||
repository image and working upwards to the root image.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_blob_path(self, blob):
|
|
||||||
"""
|
|
||||||
Returns the storage path for the given blob.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_derived_image_signature(self, derived_image, signer_name):
|
|
||||||
"""
|
|
||||||
Returns the signature associated with the derived image and a specific signer or None if none.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def set_derived_image_signature(self, derived_image, signer_name, signature):
|
|
||||||
"""
|
|
||||||
Sets the calculated signature for the given derived image and signer to that specified.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def delete_derived_image(self, derived_image):
|
|
||||||
"""
|
|
||||||
Deletes a derived image and all of its storage.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def set_blob_size(self, blob, size):
|
|
||||||
"""
|
|
||||||
Sets the size field on a blob to the value specified.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_repo_blob_by_digest(self, namespace_name, repo_name, digest):
|
|
||||||
"""
|
|
||||||
Returns the blob with the given digest under the matching repository or None if none.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_torrent_info(self, blob):
|
|
||||||
"""
|
|
||||||
Returns the torrent information associated with the given blob or None if none.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def set_torrent_info(self, blob, piece_length, pieces):
|
|
||||||
"""
|
|
||||||
Sets the torrent infomation associated with the given blob to that specified.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def lookup_derived_image(self, repo_image, verb, varying_metadata=None):
|
|
||||||
"""
|
|
||||||
Looks up the derived image for the given repository image, verb and optional varying metadata
|
|
||||||
and returns it or None if none.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def lookup_or_create_derived_image(self, repo_image, verb, location, varying_metadata=None):
|
|
||||||
"""
|
|
||||||
Looks up the derived image for the given repository image, verb and optional varying metadata
|
|
||||||
and returns it. If none exists, a new derived image is created.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_tag_image(self, namespace_name, repo_name, tag_name):
|
|
||||||
"""
|
|
||||||
Returns the image associated with the live tag with the given name under the matching repository
|
|
||||||
or None if none.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def is_namespace_enabled(self, namespace_name):
|
|
||||||
""" Returns whether the given namespace exists and is enabled. """
|
|
||||||
pass
|
|
|
@ -1,205 +0,0 @@
|
||||||
import json
|
|
||||||
|
|
||||||
from data import model
|
|
||||||
from image.docker.v1 import DockerV1Metadata
|
|
||||||
|
|
||||||
from endpoints.verbs.models_interface import (
|
|
||||||
Blob,
|
|
||||||
DerivedImage,
|
|
||||||
ImageWithBlob,
|
|
||||||
Repository,
|
|
||||||
RepositoryReference,
|
|
||||||
TorrentInfo,
|
|
||||||
VerbsDataInterface,)
|
|
||||||
|
|
||||||
|
|
||||||
class PreOCIModel(VerbsDataInterface):
|
|
||||||
"""
|
|
||||||
PreOCIModel implements the data model for the registry's custom HTTP verbs using a database schema
|
|
||||||
before it was changed to support the OCI specification.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get_repository(self, namespace_name, repo_name):
|
|
||||||
repo = model.repository.get_repository(namespace_name, repo_name)
|
|
||||||
if repo is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return _repository_for_repo(repo)
|
|
||||||
|
|
||||||
def get_manifest_layers_with_blobs(self, repo_image):
|
|
||||||
repo_image_record = model.image.get_image_by_id(
|
|
||||||
repo_image.repository.namespace_name, repo_image.repository.name, repo_image.image_id)
|
|
||||||
|
|
||||||
parents = model.image.get_parent_images(
|
|
||||||
repo_image.repository.namespace_name, repo_image.repository.name, repo_image_record)
|
|
||||||
placements_map = model.image.get_placements_for_images(parents)
|
|
||||||
|
|
||||||
yield repo_image
|
|
||||||
|
|
||||||
for parent in parents:
|
|
||||||
metadata = {}
|
|
||||||
try:
|
|
||||||
metadata = json.loads(parent.v1_json_metadata)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
locations = [placement.location.name for placement in placements_map[parent.storage.id]]
|
|
||||||
yield ImageWithBlob(
|
|
||||||
image_id=parent.docker_image_id,
|
|
||||||
blob=_blob(parent.storage, locations=locations),
|
|
||||||
repository=repo_image.repository,
|
|
||||||
compat_metadata=metadata,
|
|
||||||
v1_metadata=_docker_v1_metadata(repo_image.repository.namespace_name,
|
|
||||||
repo_image.repository.name, parent),
|
|
||||||
internal_db_id=parent.id,)
|
|
||||||
|
|
||||||
def get_derived_image_signature(self, derived_image, signer_name):
|
|
||||||
storage = model.storage.get_storage_by_uuid(derived_image.blob.uuid)
|
|
||||||
signature_entry = model.storage.lookup_storage_signature(storage, signer_name)
|
|
||||||
if signature_entry is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return signature_entry.signature
|
|
||||||
|
|
||||||
def set_derived_image_signature(self, derived_image, signer_name, signature):
|
|
||||||
storage = model.storage.get_storage_by_uuid(derived_image.blob.uuid)
|
|
||||||
signature_entry = model.storage.find_or_create_storage_signature(storage, signer_name)
|
|
||||||
signature_entry.signature = signature
|
|
||||||
signature_entry.uploading = False
|
|
||||||
signature_entry.save()
|
|
||||||
|
|
||||||
def delete_derived_image(self, derived_image):
|
|
||||||
model.image.delete_derived_storage_by_uuid(derived_image.blob.uuid)
|
|
||||||
|
|
||||||
def set_blob_size(self, blob, size):
|
|
||||||
storage_entry = model.storage.get_storage_by_uuid(blob.uuid)
|
|
||||||
storage_entry.image_size = size
|
|
||||||
storage_entry.uploading = False
|
|
||||||
storage_entry.save()
|
|
||||||
|
|
||||||
def get_blob_path(self, blob):
|
|
||||||
blob_record = model.storage.get_storage_by_uuid(blob.uuid)
|
|
||||||
return model.storage.get_layer_path(blob_record)
|
|
||||||
|
|
||||||
def get_repo_blob_by_digest(self, namespace_name, repo_name, digest):
|
|
||||||
try:
|
|
||||||
blob_record = model.blob.get_repo_blob_by_digest(namespace_name, repo_name, digest)
|
|
||||||
except model.BlobDoesNotExist:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return _blob(blob_record)
|
|
||||||
|
|
||||||
def get_torrent_info(self, blob):
|
|
||||||
blob_record = model.storage.get_storage_by_uuid(blob.uuid)
|
|
||||||
|
|
||||||
try:
|
|
||||||
torrent_info = model.storage.get_torrent_info(blob_record)
|
|
||||||
except model.TorrentInfoDoesNotExist:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return TorrentInfo(
|
|
||||||
pieces=torrent_info.pieces,
|
|
||||||
piece_length=torrent_info.piece_length,)
|
|
||||||
|
|
||||||
def set_torrent_info(self, blob, piece_length, pieces):
|
|
||||||
blob_record = model.storage.get_storage_by_uuid(blob.uuid)
|
|
||||||
model.storage.save_torrent_info(blob_record, piece_length, pieces)
|
|
||||||
|
|
||||||
def lookup_derived_image(self, repo_image, verb, varying_metadata=None):
|
|
||||||
blob_record = model.image.find_derived_storage_for_image(repo_image.internal_db_id, verb,
|
|
||||||
varying_metadata)
|
|
||||||
if blob_record is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return _derived_image(blob_record, repo_image)
|
|
||||||
|
|
||||||
def lookup_or_create_derived_image(self, repo_image, verb, location, varying_metadata=None):
|
|
||||||
blob_record = model.image.find_or_create_derived_storage(repo_image.internal_db_id, verb,
|
|
||||||
location, varying_metadata)
|
|
||||||
return _derived_image(blob_record, repo_image)
|
|
||||||
|
|
||||||
def get_tag_image(self, namespace_name, repo_name, tag_name):
|
|
||||||
try:
|
|
||||||
found = model.tag.get_tag_image(namespace_name, repo_name, tag_name, include_storage=True)
|
|
||||||
except model.DataModelException:
|
|
||||||
return None
|
|
||||||
|
|
||||||
metadata = {}
|
|
||||||
try:
|
|
||||||
metadata = json.loads(found.v1_json_metadata)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return ImageWithBlob(
|
|
||||||
image_id=found.docker_image_id,
|
|
||||||
blob=_blob(found.storage),
|
|
||||||
repository=RepositoryReference(
|
|
||||||
namespace_name=namespace_name,
|
|
||||||
name=repo_name,
|
|
||||||
id=found.repository_id,),
|
|
||||||
compat_metadata=metadata,
|
|
||||||
v1_metadata=_docker_v1_metadata(namespace_name, repo_name, found),
|
|
||||||
internal_db_id=found.id,)
|
|
||||||
|
|
||||||
def is_namespace_enabled(self, namespace_name):
|
|
||||||
namespace = model.user.get_namespace_user(namespace_name)
|
|
||||||
return namespace is not None and namespace.enabled
|
|
||||||
|
|
||||||
|
|
||||||
pre_oci_model = PreOCIModel()
|
|
||||||
|
|
||||||
|
|
||||||
def _docker_v1_metadata(namespace_name, repo_name, repo_image):
|
|
||||||
"""
|
|
||||||
Returns a DockerV1Metadata object for the given Pre-OCI repo_image under the
|
|
||||||
repository with the given namespace and name. Note that the namespace and
|
|
||||||
name are passed here as an optimization, and are *not checked* against the
|
|
||||||
image.
|
|
||||||
"""
|
|
||||||
return DockerV1Metadata(
|
|
||||||
namespace_name=namespace_name,
|
|
||||||
repo_name=repo_name,
|
|
||||||
image_id=repo_image.docker_image_id,
|
|
||||||
checksum=repo_image.v1_checksum,
|
|
||||||
compat_json=repo_image.v1_json_metadata,
|
|
||||||
created=repo_image.created,
|
|
||||||
comment=repo_image.comment,
|
|
||||||
command=repo_image.command,
|
|
||||||
|
|
||||||
# Note: These are not needed in verbs and are expensive to load, so we just skip them.
|
|
||||||
content_checksum=None,
|
|
||||||
parent_image_id=None,)
|
|
||||||
|
|
||||||
|
|
||||||
def _derived_image(blob_record, repo_image):
|
|
||||||
"""
|
|
||||||
Returns a DerivedImage object for the given Pre-OCI data model blob and repo_image instance.
|
|
||||||
"""
|
|
||||||
return DerivedImage(
|
|
||||||
ref=repo_image.internal_db_id,
|
|
||||||
blob=_blob(blob_record),
|
|
||||||
internal_source_image_db_id=repo_image.internal_db_id,)
|
|
||||||
|
|
||||||
|
|
||||||
def _blob(blob_record, locations=None):
|
|
||||||
"""
|
|
||||||
Returns a Blob object for the given Pre-OCI data model blob instance.
|
|
||||||
"""
|
|
||||||
locations = locations or model.storage.get_storage_locations(blob_record.uuid)
|
|
||||||
return Blob(
|
|
||||||
uuid=blob_record.uuid,
|
|
||||||
size=blob_record.image_size,
|
|
||||||
uncompressed_size=blob_record.uncompressed_size,
|
|
||||||
uploading=blob_record.uploading,
|
|
||||||
locations=locations,)
|
|
||||||
|
|
||||||
|
|
||||||
def _repository_for_repo(repo):
|
|
||||||
""" Returns a Repository object representing the Pre-OCI data model repo instance given. """
|
|
||||||
return Repository(
|
|
||||||
id=repo.id,
|
|
||||||
name=repo.name,
|
|
||||||
namespace_name=repo.namespace_user.username,
|
|
||||||
description=repo.description,
|
|
||||||
is_public=model.repository.is_repository_public(repo),
|
|
||||||
kind=model.repository.get_repo_kind_name(repo),)
|
|
|
@ -18,10 +18,10 @@ class AppCImageFormatter(TarImageFormatter):
|
||||||
Image formatter which produces an tarball according to the AppC specification.
|
Image formatter which produces an tarball according to the AppC specification.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def stream_generator(self, repo_image, tag, synthetic_image_id, get_image_iterator,
|
def stream_generator(self, tag, manifest, synthetic_image_id, layer_iterator,
|
||||||
tar_stream_getter_iterator, reporter=None):
|
tar_stream_getter_iterator, reporter=None):
|
||||||
image_mtime = 0
|
image_mtime = 0
|
||||||
created = next(get_image_iterator()).v1_metadata.created
|
created = manifest.get_parsed_manifest().created_datetime
|
||||||
if created is not None:
|
if created is not None:
|
||||||
image_mtime = calendar.timegm(created.utctimetuple())
|
image_mtime = calendar.timegm(created.utctimetuple())
|
||||||
|
|
||||||
|
@ -30,12 +30,12 @@ class AppCImageFormatter(TarImageFormatter):
|
||||||
# rootfs - The root file system
|
# rootfs - The root file system
|
||||||
|
|
||||||
# Yield the manifest.
|
# Yield the manifest.
|
||||||
manifest = json.dumps(DockerV1ToACIManifestTranslator.build_manifest(
|
aci_manifest = json.dumps(DockerV1ToACIManifestTranslator.build_manifest(
|
||||||
repo_image,
|
|
||||||
tag,
|
tag,
|
||||||
|
manifest,
|
||||||
synthetic_image_id
|
synthetic_image_id
|
||||||
))
|
))
|
||||||
yield self.tar_file('manifest', manifest, mtime=image_mtime)
|
yield self.tar_file('manifest', aci_manifest, mtime=image_mtime)
|
||||||
|
|
||||||
# Yield the merged layer dtaa.
|
# Yield the merged layer dtaa.
|
||||||
yield self.tar_folder('rootfs', mtime=image_mtime)
|
yield self.tar_folder('rootfs', mtime=image_mtime)
|
||||||
|
@ -170,16 +170,16 @@ class DockerV1ToACIManifestTranslator(object):
|
||||||
return volumes
|
return volumes
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def build_manifest(repo_image, tag, synthetic_image_id):
|
def build_manifest(tag, manifest, synthetic_image_id):
|
||||||
""" Builds an ACI manifest of an existing repository image. """
|
""" Builds an ACI manifest of an existing repository image. """
|
||||||
docker_layer_data = JSONPathDict(repo_image.compat_metadata)
|
docker_layer_data = JSONPathDict(manifest.leaf_layer.v1_metadata)
|
||||||
config = docker_layer_data['config'] or JSONPathDict({})
|
config = docker_layer_data['config'] or JSONPathDict({})
|
||||||
|
|
||||||
namespace = repo_image.repository.namespace_name
|
namespace = tag.repository.namespace_name
|
||||||
repo_name = repo_image.repository.name
|
repo_name = tag.repository.name
|
||||||
source_url = "%s://%s/%s/%s:%s" % (app.config['PREFERRED_URL_SCHEME'],
|
source_url = "%s://%s/%s/%s:%s" % (app.config['PREFERRED_URL_SCHEME'],
|
||||||
app.config['SERVER_HOSTNAME'],
|
app.config['SERVER_HOSTNAME'],
|
||||||
namespace, repo_name, tag)
|
namespace, repo_name, tag.name)
|
||||||
|
|
||||||
# ACI requires that the execution command be absolutely referenced. Therefore, if we find
|
# ACI requires that the execution command be absolutely referenced. Therefore, if we find
|
||||||
# a relative command, we give it as an argument to /bin/sh to resolve and execute for us.
|
# a relative command, we give it as an argument to /bin/sh to resolve and execute for us.
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from image.appc import DockerV1ToACIManifestTranslator
|
from image.appc import DockerV1ToACIManifestTranslator
|
||||||
from endpoints.verbs.models_interface import RepositoryReference, ImageWithBlob
|
|
||||||
from util.dict_wrappers import JSONPathDict
|
from util.dict_wrappers import JSONPathDict
|
||||||
|
|
||||||
|
|
||||||
|
@ -74,31 +73,6 @@ EXAMPLE_MANIFEST_OBJ = {
|
||||||
"throwaway": True
|
"throwaway": True
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def repo_image():
|
|
||||||
repo_ref = RepositoryReference(1, 'simple', 'devtable')
|
|
||||||
return ImageWithBlob(1, None, EXAMPLE_MANIFEST_OBJ, repo_ref, 1, None)
|
|
||||||
|
|
||||||
|
|
||||||
def test_port_conversion(repo_image):
|
|
||||||
output = DockerV1ToACIManifestTranslator.build_manifest(repo_image, 'v3.0.15', 'abcdef')
|
|
||||||
ports = output['app']['ports']
|
|
||||||
ports.sort()
|
|
||||||
assert {'name':'port-2379', 'port':2379, 'protocol':'tcp'} == ports[0]
|
|
||||||
assert {'name':'port-2380', 'port':2380, 'protocol':'tcp'} == ports[1]
|
|
||||||
|
|
||||||
|
|
||||||
def test_legacy_port_conversion(repo_image):
|
|
||||||
del repo_image.compat_metadata['config']['ExposedPorts']
|
|
||||||
repo_image.compat_metadata['config']['ports'] = ['8080', '8081']
|
|
||||||
output = DockerV1ToACIManifestTranslator.build_manifest(repo_image, 'v3.0.15', 'abcdef')
|
|
||||||
ports = output['app']['ports']
|
|
||||||
ports.sort()
|
|
||||||
assert {'name':'port-8080', 'port':8080, 'protocol':'tcp'} == ports[0]
|
|
||||||
assert {'name':'port-8081', 'port':8081, 'protocol':'tcp'} == ports[1]
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("vcfg,expected", [
|
@pytest.mark.parametrize("vcfg,expected", [
|
||||||
({'Volumes': None}, []),
|
({'Volumes': None}, []),
|
||||||
({'Volumes': {}}, []),
|
({'Volumes': {}}, []),
|
||||||
|
|
|
@ -7,17 +7,17 @@ class TarImageFormatter(object):
|
||||||
Base class for classes which produce a tar containing image and layer data.
|
Base class for classes which produce a tar containing image and layer data.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def build_stream(self, repo_image, tag, synthetic_image_id, get_image_iterator,
|
def build_stream(self, tag, manifest, synthetic_image_id, layer_iterator,
|
||||||
tar_stream_getter_iterator, reporter=None):
|
tar_stream_getter_iterator, reporter=None):
|
||||||
"""
|
"""
|
||||||
Builds and streams a synthetic .tar.gz that represents the formatted tar created by this class's
|
Builds and streams a synthetic .tar.gz that represents the formatted tar created by this class's
|
||||||
implementation.
|
implementation.
|
||||||
"""
|
"""
|
||||||
return GzipWrap(self.stream_generator(repo_image, tag, synthetic_image_id, get_image_iterator,
|
return GzipWrap(self.stream_generator(tag, manifest, synthetic_image_id, layer_iterator,
|
||||||
tar_stream_getter_iterator, reporter=reporter))
|
tar_stream_getter_iterator, reporter=reporter))
|
||||||
|
|
||||||
def stream_generator(self, repo_image, tag, synthetic_image_id, get_image_iterator,
|
def stream_generator(self, tag, manifest, synthetic_image_id, layer_iterator,
|
||||||
tar_stream_getter_iterator):
|
tar_stream_getter_iterator, reporter=None):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def tar_file(self, name, contents, mtime=None):
|
def tar_file(self, name, contents, mtime=None):
|
||||||
|
|
|
@ -11,6 +11,8 @@ import logging
|
||||||
from collections import namedtuple, OrderedDict
|
from collections import namedtuple, OrderedDict
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
import dateutil.parser
|
||||||
|
|
||||||
from jsonschema import validate as validate_schema, ValidationError
|
from jsonschema import validate as validate_schema, ValidationError
|
||||||
|
|
||||||
from jwkest.jws import SIGNER_ALGS, keyrep
|
from jwkest.jws import SIGNER_ALGS, keyrep
|
||||||
|
@ -70,7 +72,8 @@ class InvalidSchema1Signature(ManifestException):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Schema1Layer(namedtuple('Schema1Layer', ['digest', 'v1_metadata', 'raw_v1_metadata'])):
|
class Schema1Layer(namedtuple('Schema1Layer', ['digest', 'v1_metadata', 'raw_v1_metadata',
|
||||||
|
'compressed_size'])):
|
||||||
"""
|
"""
|
||||||
Represents all of the data about an individual layer in a given Manifest.
|
Represents all of the data about an individual layer in a given Manifest.
|
||||||
This is the union of the fsLayers (digest) and the history entries (v1_compatibility).
|
This is the union of the fsLayers (digest) and the history entries (v1_compatibility).
|
||||||
|
@ -268,6 +271,18 @@ class DockerSchema1Manifest(ManifestInterface):
|
||||||
def leaf_layer(self):
|
def leaf_layer(self):
|
||||||
return self.layers[-1]
|
return self.layers[-1]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def created_datetime(self):
|
||||||
|
created_datetime_str = self.leaf_layer.v1_metadata.created
|
||||||
|
if created_datetime_str is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
return dateutil.parser.parse(created_datetime_str).replace(tzinfo=None)
|
||||||
|
except:
|
||||||
|
# parse raises different exceptions, so we cannot use a specific kind of handler here.
|
||||||
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def layers(self):
|
def layers(self):
|
||||||
if self._layers is None:
|
if self._layers is None:
|
||||||
|
@ -305,7 +320,9 @@ class DockerSchema1Manifest(ManifestInterface):
|
||||||
extracted = Schema1V1Metadata(v1_metadata['id'], v1_metadata.get('parent'),
|
extracted = Schema1V1Metadata(v1_metadata['id'], v1_metadata.get('parent'),
|
||||||
v1_metadata.get('created'), v1_metadata.get('comment'),
|
v1_metadata.get('created'), v1_metadata.get('comment'),
|
||||||
command, labels)
|
command, labels)
|
||||||
yield Schema1Layer(image_digest, extracted, metadata_string)
|
|
||||||
|
compressed_size = v1_metadata.get('Size')
|
||||||
|
yield Schema1Layer(image_digest, extracted, metadata_string, compressed_size)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _payload(self):
|
def _payload(self):
|
||||||
|
|
|
@ -25,8 +25,9 @@ DOCKER_SCHEMA2_MANIFEST_URLS_KEY = 'urls'
|
||||||
|
|
||||||
# Named tuples.
|
# Named tuples.
|
||||||
DockerV2ManifestConfig = namedtuple('DockerV2ManifestConfig', ['size', 'digest'])
|
DockerV2ManifestConfig = namedtuple('DockerV2ManifestConfig', ['size', 'digest'])
|
||||||
DockerV2ManifestLayer = namedtuple('DockerV2ManifestLayer', ['index', 'size', 'digest',
|
DockerV2ManifestLayer = namedtuple('DockerV2ManifestLayer', ['index', 'digest',
|
||||||
'is_remote', 'urls'])
|
'is_remote', 'urls',
|
||||||
|
'compressed_size'])
|
||||||
|
|
||||||
LayerWithV1ID = namedtuple('LayerWithV1ID', ['layer', 'v1_id', 'v1_parent_id'])
|
LayerWithV1ID = namedtuple('LayerWithV1ID', ['layer', 'v1_id', 'v1_parent_id'])
|
||||||
|
|
||||||
|
@ -191,7 +192,7 @@ class DockerSchema2Manifest(ManifestInterface):
|
||||||
layer[DOCKER_SCHEMA2_MANIFEST_DIGEST_KEY])
|
layer[DOCKER_SCHEMA2_MANIFEST_DIGEST_KEY])
|
||||||
|
|
||||||
yield DockerV2ManifestLayer(index=index,
|
yield DockerV2ManifestLayer(index=index,
|
||||||
size=layer[DOCKER_SCHEMA2_MANIFEST_SIZE_KEY],
|
compressed_size=layer[DOCKER_SCHEMA2_MANIFEST_SIZE_KEY],
|
||||||
digest=digest,
|
digest=digest,
|
||||||
is_remote=is_remote,
|
is_remote=is_remote,
|
||||||
urls=layer.get(DOCKER_SCHEMA2_MANIFEST_URLS_KEY))
|
urls=layer.get(DOCKER_SCHEMA2_MANIFEST_URLS_KEY))
|
||||||
|
|
|
@ -62,13 +62,13 @@ def test_valid_manifest():
|
||||||
|
|
||||||
assert len(manifest.layers) == 4
|
assert len(manifest.layers) == 4
|
||||||
assert manifest.layers[0].is_remote
|
assert manifest.layers[0].is_remote
|
||||||
assert manifest.layers[0].size == 1234
|
assert manifest.layers[0].compressed_size == 1234
|
||||||
assert str(manifest.layers[0].digest) == 'sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736'
|
assert str(manifest.layers[0].digest) == 'sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736'
|
||||||
assert manifest.layers[0].urls
|
assert manifest.layers[0].urls
|
||||||
|
|
||||||
assert manifest.leaf_layer == manifest.layers[3]
|
assert manifest.leaf_layer == manifest.layers[3]
|
||||||
assert not manifest.leaf_layer.is_remote
|
assert not manifest.leaf_layer.is_remote
|
||||||
assert manifest.leaf_layer.size == 73109
|
assert manifest.leaf_layer.compressed_size == 73109
|
||||||
|
|
||||||
blob_digests = list(manifest.blob_digests)
|
blob_digests = list(manifest.blob_digests)
|
||||||
assert len(blob_digests) == len(manifest.layers)
|
assert len(blob_digests) == len(manifest.layers)
|
||||||
|
|
|
@ -28,10 +28,11 @@ class SquashedDockerImageFormatter(TarImageFormatter):
|
||||||
# daemon dies when trying to load the entire tar into memory.
|
# daemon dies when trying to load the entire tar into memory.
|
||||||
SIZE_MULTIPLIER = 1.2
|
SIZE_MULTIPLIER = 1.2
|
||||||
|
|
||||||
def stream_generator(self, repo_image, tag, synthetic_image_id, get_image_iterator,
|
def stream_generator(self, tag, manifest, synthetic_image_id, layer_iterator,
|
||||||
tar_stream_getter_iterator, reporter=None):
|
tar_stream_getter_iterator, reporter=None):
|
||||||
image_mtime = 0
|
image_mtime = 0
|
||||||
created = next(get_image_iterator()).v1_metadata.created
|
parsed_manifest = manifest.get_parsed_manifest()
|
||||||
|
created = parsed_manifest.created_datetime
|
||||||
if created is not None:
|
if created is not None:
|
||||||
image_mtime = calendar.timegm(created.utctimetuple())
|
image_mtime = calendar.timegm(created.utctimetuple())
|
||||||
|
|
||||||
|
@ -46,12 +47,12 @@ class SquashedDockerImageFormatter(TarImageFormatter):
|
||||||
|
|
||||||
# Yield the repositories file:
|
# Yield the repositories file:
|
||||||
synthetic_layer_info = {}
|
synthetic_layer_info = {}
|
||||||
synthetic_layer_info[tag + '.squash'] = synthetic_image_id
|
synthetic_layer_info[tag.name + '.squash'] = synthetic_image_id
|
||||||
|
|
||||||
hostname = app.config['SERVER_HOSTNAME']
|
hostname = app.config['SERVER_HOSTNAME']
|
||||||
repositories = {}
|
repositories = {}
|
||||||
namespace = repo_image.repository.namespace_name
|
namespace = tag.repository.namespace_name
|
||||||
repository = repo_image.repository.name
|
repository = tag.repository.name
|
||||||
repositories[hostname + '/' + namespace + '/' + repository] = synthetic_layer_info
|
repositories[hostname + '/' + namespace + '/' + repository] = synthetic_layer_info
|
||||||
|
|
||||||
yield self.tar_file('repositories', json.dumps(repositories), mtime=image_mtime)
|
yield self.tar_file('repositories', json.dumps(repositories), mtime=image_mtime)
|
||||||
|
@ -60,7 +61,7 @@ class SquashedDockerImageFormatter(TarImageFormatter):
|
||||||
yield self.tar_folder(synthetic_image_id, mtime=image_mtime)
|
yield self.tar_folder(synthetic_image_id, mtime=image_mtime)
|
||||||
|
|
||||||
# Yield the JSON layer data.
|
# Yield the JSON layer data.
|
||||||
layer_json = SquashedDockerImageFormatter._build_layer_json(repo_image, synthetic_image_id)
|
layer_json = SquashedDockerImageFormatter._build_layer_json(parsed_manifest, synthetic_image_id)
|
||||||
yield self.tar_file(synthetic_image_id + '/json', json.dumps(layer_json), mtime=image_mtime)
|
yield self.tar_file(synthetic_image_id + '/json', json.dumps(layer_json), mtime=image_mtime)
|
||||||
|
|
||||||
# Yield the VERSION file.
|
# Yield the VERSION file.
|
||||||
|
@ -68,16 +69,8 @@ class SquashedDockerImageFormatter(TarImageFormatter):
|
||||||
|
|
||||||
# Yield the merged layer data's header.
|
# Yield the merged layer data's header.
|
||||||
estimated_file_size = 0
|
estimated_file_size = 0
|
||||||
for image in get_image_iterator():
|
for layer in layer_iterator:
|
||||||
# In V1 we have the actual uncompressed size, which is needed for back compat with
|
estimated_file_size += layer.estimated_size(SquashedDockerImageFormatter.SIZE_MULTIPLIER)
|
||||||
# older versions of Docker.
|
|
||||||
# In V2, we use the size given in the image JSON.
|
|
||||||
if image.blob.uncompressed_size:
|
|
||||||
estimated_file_size += image.blob.uncompressed_size
|
|
||||||
else:
|
|
||||||
image_json = image.compat_metadata
|
|
||||||
estimated_file_size += (image_json.get('Size', 0) *
|
|
||||||
SquashedDockerImageFormatter.SIZE_MULTIPLIER)
|
|
||||||
|
|
||||||
# Make sure the estimated file size is an integer number of bytes.
|
# Make sure the estimated file size is an integer number of bytes.
|
||||||
estimated_file_size = int(math.ceil(estimated_file_size))
|
estimated_file_size = int(math.ceil(estimated_file_size))
|
||||||
|
@ -115,9 +108,8 @@ class SquashedDockerImageFormatter(TarImageFormatter):
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _build_layer_json(repo_image, synthetic_image_id):
|
def _build_layer_json(manifest, synthetic_image_id):
|
||||||
layer_json = repo_image.compat_metadata
|
updated_json = json.loads(manifest.leaf_layer.raw_v1_metadata)
|
||||||
updated_json = copy.deepcopy(layer_json)
|
|
||||||
updated_json['id'] = synthetic_image_id
|
updated_json['id'] = synthetic_image_id
|
||||||
|
|
||||||
if 'parent' in updated_json:
|
if 'parent' in updated_json:
|
||||||
|
|
|
@ -77,7 +77,11 @@ def test_valid_manifest():
|
||||||
assert manifest.layers[1].v1_metadata.image_id == 'someid'
|
assert manifest.layers[1].v1_metadata.image_id == 'someid'
|
||||||
assert manifest.layers[1].v1_metadata.parent_image_id == 'anotherid'
|
assert manifest.layers[1].v1_metadata.parent_image_id == 'anotherid'
|
||||||
|
|
||||||
|
assert manifest.layers[0].compressed_size is None
|
||||||
|
assert manifest.layers[1].compressed_size is None
|
||||||
|
|
||||||
assert manifest.leaf_layer == manifest.layers[1]
|
assert manifest.leaf_layer == manifest.layers[1]
|
||||||
|
assert manifest.created_datetime is None
|
||||||
|
|
||||||
|
|
||||||
def test_validate_manifest():
|
def test_validate_manifest():
|
||||||
|
@ -88,6 +92,7 @@ def test_validate_manifest():
|
||||||
manifest = DockerSchema1Manifest(manifest_bytes, validate=True)
|
manifest = DockerSchema1Manifest(manifest_bytes, validate=True)
|
||||||
digest = manifest.digest
|
digest = manifest.digest
|
||||||
assert digest == 'sha256:b5dc4f63fdbd64f34f2314c0747ef81008f9fcddce4edfc3fd0e8ec8b358d571'
|
assert digest == 'sha256:b5dc4f63fdbd64f34f2314c0747ef81008f9fcddce4edfc3fd0e8ec8b358d571'
|
||||||
|
assert manifest.created_datetime
|
||||||
|
|
||||||
|
|
||||||
def test_validate_manifest_with_unicode():
|
def test_validate_manifest_with_unicode():
|
||||||
|
@ -98,6 +103,7 @@ def test_validate_manifest_with_unicode():
|
||||||
manifest = DockerSchema1Manifest(manifest_bytes, validate=True)
|
manifest = DockerSchema1Manifest(manifest_bytes, validate=True)
|
||||||
digest = manifest.digest
|
digest = manifest.digest
|
||||||
assert digest == 'sha256:815ecf45716a96b19d54d911e6ace91f78bab26ca0dd299645d9995dacd9f1ef'
|
assert digest == 'sha256:815ecf45716a96b19d54d911e6ace91f78bab26ca0dd299645d9995dacd9f1ef'
|
||||||
|
assert manifest.created_datetime
|
||||||
|
|
||||||
|
|
||||||
def test_validate_manifest_with_unicode_encoded():
|
def test_validate_manifest_with_unicode_encoded():
|
||||||
|
@ -108,3 +114,4 @@ def test_validate_manifest_with_unicode_encoded():
|
||||||
manifest = DockerSchema1Manifest.for_latin1_bytes(manifest_bytes, validate=True)
|
manifest = DockerSchema1Manifest.for_latin1_bytes(manifest_bytes, validate=True)
|
||||||
digest = manifest.digest
|
digest = manifest.digest
|
||||||
assert digest == 'sha256:dde3714ce7e23edc6413aa85c0b42792e4f2f79e9ea36afc154d63ff3d04e86c'
|
assert digest == 'sha256:dde3714ce7e23edc6413aa85c0b42792e4f2f79e9ea36afc154d63ff3d04e86c'
|
||||||
|
assert manifest.created_datetime
|
||||||
|
|
|
@ -26,7 +26,7 @@ class FakeStorage(BaseStorageV2):
|
||||||
|
|
||||||
def get_content(self, path):
|
def get_content(self, path):
|
||||||
if not path in _FAKE_STORAGE_MAP:
|
if not path in _FAKE_STORAGE_MAP:
|
||||||
raise IOError('Fake file %s not found' % path)
|
raise IOError('Fake file %s not found. Exist: %s' % (path, _FAKE_STORAGE_MAP.keys()))
|
||||||
|
|
||||||
_FAKE_STORAGE_MAP.get(path).seek(0)
|
_FAKE_STORAGE_MAP.get(path).seek(0)
|
||||||
return _FAKE_STORAGE_MAP.get(path).read()
|
return _FAKE_STORAGE_MAP.get(path).read()
|
||||||
|
|
|
@ -30,8 +30,49 @@ def sized_images():
|
||||||
parent_bytes = layer_bytes_for_contents('parent contents', mode='')
|
parent_bytes = layer_bytes_for_contents('parent contents', mode='')
|
||||||
image_bytes = layer_bytes_for_contents('some contents', mode='')
|
image_bytes = layer_bytes_for_contents('some contents', mode='')
|
||||||
return [
|
return [
|
||||||
Image(id='parentid', bytes=parent_bytes, parent_id=None, size=len(parent_bytes)),
|
Image(id='parentid', bytes=parent_bytes, parent_id=None, size=len(parent_bytes),
|
||||||
Image(id='someid', bytes=image_bytes, parent_id='parentid', size=len(image_bytes)),
|
config={'foo': 'bar'}),
|
||||||
|
Image(id='someid', bytes=image_bytes, parent_id='parentid', size=len(image_bytes),
|
||||||
|
config={'foo': 'childbar'}),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def multi_layer_images():
|
||||||
|
""" Returns complex images (with sizes) for push and pull testing. """
|
||||||
|
# Note: order is from base layer down to leaf.
|
||||||
|
layer1_bytes = layer_bytes_for_contents('layer 1 contents', mode='', other_files={
|
||||||
|
'file1': 'from-layer-1',
|
||||||
|
})
|
||||||
|
|
||||||
|
layer2_bytes = layer_bytes_for_contents('layer 2 contents', mode='', other_files={
|
||||||
|
'file2': 'from-layer-2',
|
||||||
|
})
|
||||||
|
|
||||||
|
layer3_bytes = layer_bytes_for_contents('layer 3 contents', mode='', other_files={
|
||||||
|
'file1': 'from-layer-3',
|
||||||
|
'file3': 'from-layer-3',
|
||||||
|
})
|
||||||
|
|
||||||
|
layer4_bytes = layer_bytes_for_contents('layer 4 contents', mode='', other_files={
|
||||||
|
'file3': 'from-layer-4',
|
||||||
|
})
|
||||||
|
|
||||||
|
layer5_bytes = layer_bytes_for_contents('layer 5 contents', mode='', other_files={
|
||||||
|
'file4': 'from-layer-5',
|
||||||
|
})
|
||||||
|
|
||||||
|
return [
|
||||||
|
Image(id='layer1', bytes=layer1_bytes, parent_id=None, size=len(layer1_bytes),
|
||||||
|
config={'internal_id': 'layer1'}),
|
||||||
|
Image(id='layer2', bytes=layer2_bytes, parent_id='layer1', size=len(layer2_bytes),
|
||||||
|
config={'internal_id': 'layer2'}),
|
||||||
|
Image(id='layer3', bytes=layer3_bytes, parent_id='layer2', size=len(layer3_bytes),
|
||||||
|
config={'internal_id': 'layer3'}),
|
||||||
|
Image(id='layer4', bytes=layer4_bytes, parent_id='layer3', size=len(layer4_bytes),
|
||||||
|
config={'internal_id': 'layer4'}),
|
||||||
|
Image(id='someid', bytes=layer5_bytes, parent_id='layer4', size=len(layer5_bytes),
|
||||||
|
config={'internal_id': 'layer5'}),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -14,8 +14,9 @@ PushResult = namedtuple('PushResult', ['checksums', 'manifests', 'headers'])
|
||||||
PullResult = namedtuple('PullResult', ['manifests', 'image_ids'])
|
PullResult = namedtuple('PullResult', ['manifests', 'image_ids'])
|
||||||
|
|
||||||
|
|
||||||
def layer_bytes_for_contents(contents, mode='|gz'):
|
def layer_bytes_for_contents(contents, mode='|gz', other_files=None):
|
||||||
layer_data = StringIO()
|
layer_data = StringIO()
|
||||||
|
tar_file = tarfile.open(fileobj=layer_data, mode='w' + mode)
|
||||||
|
|
||||||
def add_file(name, contents):
|
def add_file(name, contents):
|
||||||
tar_file_info = tarfile.TarInfo(name=name)
|
tar_file_info = tarfile.TarInfo(name=name)
|
||||||
|
@ -23,12 +24,16 @@ def layer_bytes_for_contents(contents, mode='|gz'):
|
||||||
tar_file_info.size = len(contents)
|
tar_file_info.size = len(contents)
|
||||||
tar_file_info.mtime = 1
|
tar_file_info.mtime = 1
|
||||||
|
|
||||||
tar_file = tarfile.open(fileobj=layer_data, mode='w' + mode)
|
|
||||||
tar_file.addfile(tar_file_info, StringIO(contents))
|
tar_file.addfile(tar_file_info, StringIO(contents))
|
||||||
tar_file.close()
|
|
||||||
|
|
||||||
add_file('contents', contents)
|
add_file('contents', contents)
|
||||||
|
|
||||||
|
if other_files is not None:
|
||||||
|
for file_name, file_contents in other_files.iteritems():
|
||||||
|
add_file(file_name, file_contents)
|
||||||
|
|
||||||
|
tar_file.close()
|
||||||
|
|
||||||
layer_bytes = layer_data.getvalue()
|
layer_bytes = layer_data.getvalue()
|
||||||
layer_data.close()
|
layer_data.close()
|
||||||
return layer_bytes
|
return layer_bytes
|
||||||
|
|
|
@ -930,6 +930,58 @@ def test_squashed_image_disabled_user(pusher, sized_images, liveserver_session,
|
||||||
assert response.status_code == 403
|
assert response.status_code == 403
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('use_estimates', [
|
||||||
|
False,
|
||||||
|
True,
|
||||||
|
])
|
||||||
|
def test_multilayer_squashed_images(use_estimates, pusher, multi_layer_images, liveserver_session,
|
||||||
|
liveserver, registry_server_executor, app_reloader):
|
||||||
|
""" Test: Pulling of multilayer, complex squashed images. """
|
||||||
|
credentials = ('devtable', 'password')
|
||||||
|
|
||||||
|
# Push an image to download.
|
||||||
|
pusher.push(liveserver_session, 'devtable', 'newrepo', 'latest', multi_layer_images,
|
||||||
|
credentials=credentials)
|
||||||
|
|
||||||
|
if use_estimates:
|
||||||
|
# Clear the uncompressed size stored for the images, to ensure that we estimate instead.
|
||||||
|
for image in multi_layer_images:
|
||||||
|
registry_server_executor.on(liveserver).clear_uncompressed_size(image.id)
|
||||||
|
|
||||||
|
# Pull the squashed version.
|
||||||
|
response = liveserver_session.get('/c1/squash/devtable/newrepo/latest', auth=credentials)
|
||||||
|
tar = tarfile.open(fileobj=StringIO(response.content))
|
||||||
|
|
||||||
|
# Verify the squashed image.
|
||||||
|
expected_image_id = 'cdc6d6c0d07d2cbacfc579e49ce0c256c5084b9b2b16c1b1b0c45f26a12a4ba5'
|
||||||
|
expected_names = ['repositories',
|
||||||
|
expected_image_id,
|
||||||
|
'%s/json' % expected_image_id,
|
||||||
|
'%s/VERSION' % expected_image_id,
|
||||||
|
'%s/layer.tar' % expected_image_id]
|
||||||
|
|
||||||
|
assert tar.getnames() == expected_names
|
||||||
|
|
||||||
|
# Verify the JSON image data.
|
||||||
|
json_data = (tar.extractfile(tar.getmember('%s/json' % expected_image_id)).read())
|
||||||
|
|
||||||
|
# Ensure the JSON loads and parses.
|
||||||
|
result = json.loads(json_data)
|
||||||
|
assert result['id'] == expected_image_id
|
||||||
|
assert result['config']['internal_id'] == 'layer5'
|
||||||
|
|
||||||
|
# Ensure that squashed layer tar can be opened.
|
||||||
|
tar = tarfile.open(fileobj=tar.extractfile(tar.getmember('%s/layer.tar' % expected_image_id)))
|
||||||
|
assert set(tar.getnames()) == {'contents', 'file1', 'file2', 'file3', 'file4'}
|
||||||
|
|
||||||
|
# Check the contents of various files.
|
||||||
|
assert tar.extractfile('contents').read() == 'layer 5 contents'
|
||||||
|
assert tar.extractfile('file1').read() == 'from-layer-3'
|
||||||
|
assert tar.extractfile('file2').read() == 'from-layer-2'
|
||||||
|
assert tar.extractfile('file3').read() == 'from-layer-4'
|
||||||
|
assert tar.extractfile('file4').read() == 'from-layer-5'
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('use_estimates', [
|
@pytest.mark.parametrize('use_estimates', [
|
||||||
False,
|
False,
|
||||||
True,
|
True,
|
||||||
|
@ -953,7 +1005,7 @@ def test_squashed_images(use_estimates, pusher, sized_images, liveserver_session
|
||||||
tar = tarfile.open(fileobj=StringIO(response.content))
|
tar = tarfile.open(fileobj=StringIO(response.content))
|
||||||
|
|
||||||
# Verify the squashed image.
|
# Verify the squashed image.
|
||||||
expected_image_id = '13a2d9711a3e242fcd50a7627c02d86901ac801b78dcea3147e8ff640078de52'
|
expected_image_id = 'cdc6d6c0d07d2cbacfc579e49ce0c256c5084b9b2b16c1b1b0c45f26a12a4ba5'
|
||||||
expected_names = ['repositories',
|
expected_names = ['repositories',
|
||||||
expected_image_id,
|
expected_image_id,
|
||||||
'%s/json' % expected_image_id,
|
'%s/json' % expected_image_id,
|
||||||
|
@ -968,9 +1020,14 @@ def test_squashed_images(use_estimates, pusher, sized_images, liveserver_session
|
||||||
# Ensure the JSON loads and parses.
|
# Ensure the JSON loads and parses.
|
||||||
result = json.loads(json_data)
|
result = json.loads(json_data)
|
||||||
assert result['id'] == expected_image_id
|
assert result['id'] == expected_image_id
|
||||||
|
assert result['config']['foo'] == 'childbar'
|
||||||
|
|
||||||
# Ensure that squashed layer tar can be opened.
|
# Ensure that squashed layer tar can be opened.
|
||||||
tarfile.open(fileobj=tar.extractfile(tar.getmember('%s/layer.tar' % expected_image_id)))
|
tar = tarfile.open(fileobj=tar.extractfile(tar.getmember('%s/layer.tar' % expected_image_id)))
|
||||||
|
assert tar.getnames() == ['contents']
|
||||||
|
|
||||||
|
# Check the contents.
|
||||||
|
assert tar.extractfile('contents').read() == 'some contents'
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('push_user, push_namespace, push_repo, mount_repo_name, expected_failure', [
|
@pytest.mark.parametrize('push_user, push_namespace, push_repo, mount_repo_name, expected_failure', [
|
||||||
|
|
|
@ -2103,7 +2103,7 @@ class SquashingTests(RegistryTestCaseMixin, V1RegistryPushMixin, LiveServerTestC
|
||||||
|
|
||||||
# Create the repo.
|
# Create the repo.
|
||||||
self.do_push('devtable', 'newrepo', 'devtable', 'password', images=initial_images)
|
self.do_push('devtable', 'newrepo', 'devtable', 'password', images=initial_images)
|
||||||
initial_image_id = '91081df45b58dc62dd207441785eef2b895f0383fbe601c99a3cf643c79957dc'
|
initial_image_id = 'cdc6d6c0d07d2cbacfc579e49ce0c256c5084b9b2b16c1b1b0c45f26a12a4ba5'
|
||||||
|
|
||||||
# Pull the squashed version of the tag.
|
# Pull the squashed version of the tag.
|
||||||
tar, _ = self.get_squashed_image(auth=('devtable', 'password'))
|
tar, _ = self.get_squashed_image(auth=('devtable', 'password'))
|
||||||
|
@ -2119,7 +2119,7 @@ class SquashingTests(RegistryTestCaseMixin, V1RegistryPushMixin, LiveServerTestC
|
||||||
|
|
||||||
# Create the repo.
|
# Create the repo.
|
||||||
self.do_push('devtable', 'newrepo', 'devtable', 'password', images=initial_images)
|
self.do_push('devtable', 'newrepo', 'devtable', 'password', images=initial_images)
|
||||||
initial_image_id = '91081df45b58dc62dd207441785eef2b895f0383fbe601c99a3cf643c79957dc'
|
initial_image_id = 'cdc6d6c0d07d2cbacfc579e49ce0c256c5084b9b2b16c1b1b0c45f26a12a4ba5'
|
||||||
|
|
||||||
# Pull the squashed version of the tag.
|
# Pull the squashed version of the tag.
|
||||||
tar, _ = self.get_squashed_image()
|
tar, _ = self.get_squashed_image()
|
||||||
|
@ -2134,7 +2134,7 @@ class SquashingTests(RegistryTestCaseMixin, V1RegistryPushMixin, LiveServerTestC
|
||||||
]
|
]
|
||||||
|
|
||||||
self.do_push('devtable', 'newrepo', 'devtable', 'password', images=updated_images)
|
self.do_push('devtable', 'newrepo', 'devtable', 'password', images=updated_images)
|
||||||
updated_image_id = '38df4bd4cdffc6b7d656dbd2813c73e864f2d362ad887c999ac315224ad281ac'
|
updated_image_id = '57e8d9226ca95ed4d9b303a4104cb6475605e00f6ce536fe6ed54a5d1f559882'
|
||||||
|
|
||||||
# Pull the squashed version of the tag and ensure it has changed.
|
# Pull the squashed version of the tag and ensure it has changed.
|
||||||
tar, _ = self.get_squashed_image()
|
tar, _ = self.get_squashed_image()
|
||||||
|
@ -2157,7 +2157,7 @@ class SquashingTests(RegistryTestCaseMixin, V1RegistryPushMixin, LiveServerTestC
|
||||||
self.conduct('POST', '/__test/removeuncompressed/initialid')
|
self.conduct('POST', '/__test/removeuncompressed/initialid')
|
||||||
|
|
||||||
# Pull the squashed version of the tag.
|
# Pull the squashed version of the tag.
|
||||||
initial_image_id = '91081df45b58dc62dd207441785eef2b895f0383fbe601c99a3cf643c79957dc'
|
initial_image_id = 'cdc6d6c0d07d2cbacfc579e49ce0c256c5084b9b2b16c1b1b0c45f26a12a4ba5'
|
||||||
tar, _ = self.get_squashed_image()
|
tar, _ = self.get_squashed_image()
|
||||||
self.assertTrue(initial_image_id in tar.getnames())
|
self.assertTrue(initial_image_id in tar.getnames())
|
||||||
|
|
||||||
|
@ -2179,7 +2179,7 @@ class SquashingTests(RegistryTestCaseMixin, V1RegistryPushMixin, LiveServerTestC
|
||||||
self.do_push('devtable', 'newrepo', 'devtable', 'password', images=images)
|
self.do_push('devtable', 'newrepo', 'devtable', 'password', images=images)
|
||||||
|
|
||||||
# Pull the squashed version of the tag.
|
# Pull the squashed version of the tag.
|
||||||
expected_image_id = 'bd590ae79fba5ebc6550aaf016c0bd0f49b1d78178e0f83e0ca1c56c2bb7e7bf'
|
expected_image_id = 'cdc6d6c0d07d2cbacfc579e49ce0c256c5084b9b2b16c1b1b0c45f26a12a4ba5'
|
||||||
|
|
||||||
expected_names = ['repositories',
|
expected_names = ['repositories',
|
||||||
expected_image_id,
|
expected_image_id,
|
||||||
|
@ -2212,7 +2212,7 @@ class SquashingTests(RegistryTestCaseMixin, V1RegistryPushMixin, LiveServerTestC
|
||||||
|
|
||||||
# Create the repo.
|
# Create the repo.
|
||||||
self.do_push('devtable', 'newrepo', 'devtable', 'password', images=initial_images)
|
self.do_push('devtable', 'newrepo', 'devtable', 'password', images=initial_images)
|
||||||
initial_image_id = '91081df45b58dc62dd207441785eef2b895f0383fbe601c99a3cf643c79957dc'
|
initial_image_id = 'cdc6d6c0d07d2cbacfc579e49ce0c256c5084b9b2b16c1b1b0c45f26a12a4ba5'
|
||||||
|
|
||||||
# Try to pull the torrent of the squashed image. This should fail with a 406 since the
|
# Try to pull the torrent of the squashed image. This should fail with a 406 since the
|
||||||
# squashed image doesn't yet exist.
|
# squashed image doesn't yet exist.
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
|
|
||||||
|
from collections import namedtuple
|
||||||
from urlparse import urlparse
|
from urlparse import urlparse
|
||||||
|
|
||||||
from flask import request
|
from flask import request
|
||||||
|
@ -11,6 +12,13 @@ from auth.auth_context import get_authenticated_context, get_authenticated_user
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
Repository = namedtuple('Repository', ['namespace_name', 'name', 'id'])
|
||||||
|
|
||||||
|
def wrap_repository(repo_obj):
|
||||||
|
return Repository(namespace_name=repo_obj.namespace_user.username, name=repo_obj.name,
|
||||||
|
id=repo_obj.id)
|
||||||
|
|
||||||
|
|
||||||
def track_and_log(event_name, repo_obj, analytics_name=None, analytics_sample=1, **kwargs):
|
def track_and_log(event_name, repo_obj, analytics_name=None, analytics_sample=1, **kwargs):
|
||||||
repo_name = repo_obj.name
|
repo_name = repo_obj.name
|
||||||
namespace_name = repo_obj.namespace_name
|
namespace_name = repo_obj.namespace_name
|
||||||
|
|
Reference in a new issue