import json from abc import ABCMeta, abstractmethod from collections import namedtuple from six import add_metaclass from data import model from image.docker.v1 import DockerV1Metadata 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 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_with_placements(repo_image.repository.namespace_name, repo_image.repository.name, repo_image_record) yield repo_image for parent in parents: metadata = {} try: metadata = json.loads(parent.v1_json_metadata) except ValueError: pass yield ImageWithBlob( image_id=parent.docker_image_id, blob=_blob(parent.storage), 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, ) 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): """ Returns a Blob object for the given Pre-OCI data model blob instance. """ if hasattr(blob_record, 'locations'): locations = blob_record.locations else: locations = 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), )