update interfaces to use ABC

This commit is contained in:
Jimmy Zelinskie 2016-09-23 17:50:09 -04:00
parent a1a930b833
commit 44eca10c05
14 changed files with 467 additions and 508 deletions

View file

@ -1,10 +1,12 @@
from abc import ABCMeta, abstractmethod
from collections import namedtuple
from namedlist import namedlist
from peewee import IntegrityError
from six import add_metaclass
from data import model, database
from data.model import DataModelException, TagAlreadyCreatedException
from data.model import DataModelException
from image.docker.v1 import DockerV1Metadata
_MEDIA_TYPE = "application/vnd.docker.distribution.manifest.v1+prettyjws"
@ -54,153 +56,153 @@ class Label(namedtuple('Label', ['key', 'value', 'source_type', 'media_type'])):
"""
@add_metaclass(ABCMeta)
class DockerRegistryV2DataInterface(object):
"""
Interface that represents all data store interactions required by a Docker Registry v1.
"""
@classmethod
def create_repository(cls, namespace_name, repo_name, creating_user=None):
@abstractmethod
def create_repository(self, namespace_name, repo_name, creating_user=None):
"""
Creates a new repository under the specified namespace with the given name. The user supplied is
the user creating the repository, if any.
"""
raise NotImplementedError()
pass
@classmethod
def repository_is_public(cls, namespace_name, repo_name):
@abstractmethod
def repository_is_public(self, namespace_name, repo_name):
"""
Returns true if the repository with the given name under the given namespace has public
visibility.
"""
raise NotImplementedError()
pass
@classmethod
def get_repository(cls, namespace_name, repo_name):
@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.
"""
raise NotImplementedError()
pass
@classmethod
def has_active_tag(cls, namespace_name, repo_name, tag_name):
@abstractmethod
def has_active_tag(self, namespace_name, repo_name, tag_name):
"""
Returns whether there is an active tag for the tag with the given name under the matching
repository, if any, or none if none.
"""
raise NotImplementedError()
pass
@classmethod
def get_manifest_by_tag(cls, namespace_name, repo_name, tag_name):
@abstractmethod
def get_manifest_by_tag(self, namespace_name, repo_name, tag_name):
"""
Returns the current manifest for the tag with the given name under the matching repository, if
any, or None if none.
"""
raise NotImplementedError()
pass
@classmethod
def get_manifest_by_digest(cls, namespace_name, repo_name, digest):
@abstractmethod
def get_manifest_by_digest(self, namespace_name, repo_name, digest):
"""
Returns the manifest matching the given digest under the matching repository, if any, or None if
none.
"""
raise NotImplementedError()
pass
@classmethod
def delete_manifest_by_digest(cls, namespace_name, repo_name, digest):
@abstractmethod
def delete_manifest_by_digest(self, namespace_name, repo_name, digest):
"""
Deletes the manifest with the associated digest (if any) and returns all removed tags that
pointed to that manifest. If the manifest was not found, returns an empty list.
"""
raise NotImplementedError()
pass
@classmethod
def get_docker_v1_metadata_by_tag(cls, namespace_name, repo_name, tag_name):
@abstractmethod
def get_docker_v1_metadata_by_tag(self, namespace_name, repo_name, tag_name):
"""
Returns the Docker V1 metadata associated with the tag with the given name under the matching
repository, if any. If none, returns None.
"""
raise NotImplementedError()
pass
@classmethod
def get_docker_v1_metadata_by_image_id(cls, namespace_name, repo_name, docker_image_ids):
@abstractmethod
def get_docker_v1_metadata_by_image_id(self, namespace_name, repo_name, docker_image_ids):
"""
Returns a map of Docker V1 metadata for each given image ID, matched under the repository with
the given namespace and name. Returns an empty map if the matching repository was not found.
"""
raise NotImplementedError()
pass
@classmethod
def get_parents_docker_v1_metadata(cls, namespace_name, repo_name, docker_image_id):
@abstractmethod
def get_parents_docker_v1_metadata(self, namespace_name, repo_name, docker_image_id):
"""
Returns an ordered list containing the Docker V1 metadata for each parent of the image with the
given docker ID under the matching repository. Returns an empty list if the image was not found.
"""
raise NotImplementedError()
pass
@classmethod
def create_manifest_and_update_tag(cls, namespace_name, repo_name, tag_name, manifest_digest,
@abstractmethod
def create_manifest_and_update_tag(self, namespace_name, repo_name, tag_name, manifest_digest,
manifest_bytes):
"""
Creates a new manifest with the given digest and byte data, and assigns the tag with the given
name under the matching repository to it.
"""
raise NotImplementedError()
pass
@classmethod
def synthesize_v1_image(cls, repository, storage, image_id, created, comment, command,
@abstractmethod
def synthesize_v1_image(self, repository, storage, image_id, created, comment, command,
compat_json, parent_image_id):
"""
Synthesizes a V1 image under the specified repository, pointing to the given storage and returns
the V1 metadata for the synthesized image.
"""
raise NotImplementedError()
pass
@classmethod
def save_manifest(cls, namespace_name, repo_name, tag_name, leaf_layer_docker_id, manifest_digest,
manifest_bytes):
@abstractmethod
def save_manifest(self, namespace_name, repo_name, tag_name, leaf_layer_docker_id,
manifest_digest, manifest_bytes):
"""
Saves a manifest pointing to the given leaf image, with the given manifest, under the matching
repository as a tag with the given name.
Returns a boolean whether or not the tag was newly created or not.
"""
raise NotImplementedError()
pass
@classmethod
def repository_tags(cls, namespace_name, repo_name, limit, offset):
@abstractmethod
def repository_tags(self, namespace_name, repo_name, limit, offset):
"""
Returns the active tags under the repository with the given name and namespace.
"""
raise NotImplementedError()
pass
@classmethod
def get_visible_repositories(cls, username, limit, offset):
@abstractmethod
def get_visible_repositories(self, username, limit, offset):
"""
Returns the repositories visible to the user with the given username, if any.
"""
raise NotImplementedError()
pass
@classmethod
def create_blob_upload(cls, namespace_name, repo_name, upload_uuid, location_name,
storage_metadata):
@abstractmethod
def create_blob_upload(self, namespace_name, repo_name, upload_uuid, location_name, storage_metadata):
"""
Creates a blob upload under the matching repository with the given UUID and metadata.
Returns whether the matching repository exists.
"""
raise NotImplementedError()
pass
@classmethod
def blob_upload_by_uuid(cls, namespace_name, repo_name, upload_uuid):
@abstractmethod
def blob_upload_by_uuid(self, namespace_name, repo_name, upload_uuid):
"""
Searches for a blob upload with the given UUID under the given repository and returns it or None
if none.
"""
raise NotImplementedError()
pass
@classmethod
def update_blob_upload(cls, blob_upload):
@abstractmethod
def update_blob_upload(self, blob_upload):
"""
Saves any changes to the blob upload object given to the backing data store.
Fields that can change:
@ -212,61 +214,53 @@ class DockerRegistryV2DataInterface(object):
- chunk_count
- sha_state
"""
raise NotImplementedError()
pass
@classmethod
def delete_blob_upload(cls, namespace_name, repo_name, uuid):
@abstractmethod
def delete_blob_upload(self, namespace_name, repo_name, uuid):
"""
Deletes the blob upload with the given uuid under the matching repository. If none, does
nothing.
"""
raise NotImplementedError()
pass
@classmethod
def create_blob_and_temp_tag(cls, namespace_name, repo_name, blob_digest, blob_upload,
@abstractmethod
def create_blob_and_temp_tag(self, namespace_name, repo_name, blob_digest, blob_upload,
expiration_sec):
"""
Creates a blob and links a temporary tag with the specified expiration to it under the matching
repository.
"""
raise NotImplementedError()
pass
@classmethod
def lookup_blobs_by_digest(cls, namespace_name, repo_name, digests):
"""
Returns all the blobs with matching digests found under the matching repository. If the
repository doesn't exist, returns {}.
"""
raise NotImplementedError()
@classmethod
def get_blob_by_digest(cls, namespace_name, repo_name, digest):
@abstractmethod
def get_blob_by_digest(self, namespace_name, repo_name, digest):
"""
Returns the blob with the given digest under the matching repository or None if none.
"""
raise NotImplementedError()
pass
@classmethod
def save_bittorrent_pieces(cls, blob, piece_size, piece_bytes):
@abstractmethod
def save_bittorrent_pieces(self, blob, piece_size, piece_bytes):
"""
Saves the BitTorrent piece hashes for the given blob.
"""
raise NotImplementedError()
pass
@classmethod
def create_manifest_labels(cls, namespace_name, repo_name, manifest_digest, labels):
@abstractmethod
def create_manifest_labels(self, namespace_name, repo_name, manifest_digest, labels):
"""
Creates a new labels for the provided manifest.
"""
raise NotImplementedError()
pass
@classmethod
def get_blob_path(cls, blob):
@abstractmethod
def get_blob_path(self, blob):
"""
Once everything is moved over, this could be in util.registry and not even touch the database.
"""
raise NotImplementedError()
pass
class PreOCIModel(DockerRegistryV2DataInterface):
@ -274,58 +268,40 @@ class PreOCIModel(DockerRegistryV2DataInterface):
PreOCIModel implements the data model for the v2 Docker Registry protocol using a database schema
before it was changed to support the OCI specification.
"""
@classmethod
def create_repository(cls, namespace_name, repo_name, creating_user=None):
def create_repository(self, namespace_name, repo_name, creating_user=None):
return model.repository.create_repository(namespace_name, repo_name, creating_user)
@classmethod
def repository_is_public(cls, namespace_name, repo_name):
def repository_is_public(self, namespace_name, repo_name):
return model.repository.repository_is_public(namespace_name, repo_name)
@classmethod
def _repository_for_repo(cls, repo):
""" Returns a Repository object representing the repo data model 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)
)
@classmethod
def get_repository(cls, namespace_name, repo_name):
def get_repository(self, namespace_name, repo_name):
repo = model.repository.get_repository(namespace_name, repo_name)
if repo is None:
return None
return cls._repository_for_repo(repo)
return _repository_for_repo(repo)
@classmethod
def has_active_tag(cls, namespace_name, repo_name, tag_name):
def has_active_tag(self, namespace_name, repo_name, tag_name):
try:
model.tag.get_active_tag(namespace_name, repo_name, tag_name)
return True
except database.RepositoryTag.DoesNotExist:
return False
@classmethod
def get_manifest_by_tag(cls, namespace_name, repo_name, tag_name):
def get_manifest_by_tag(self, namespace_name, repo_name, tag_name):
try:
manifest = model.tag.load_tag_manifest(namespace_name, repo_name, tag_name)
return ManifestJSON(digest=manifest.digest, json=manifest.json_data, media_type=_MEDIA_TYPE)
except model.InvalidManifestException:
return None
@classmethod
def get_manifest_by_digest(cls, namespace_name, repo_name, digest):
def get_manifest_by_digest(self, namespace_name, repo_name, digest):
try:
manifest = model.tag.load_manifest_by_digest(namespace_name, repo_name, digest)
return ManifestJSON(digest=digest, json=manifest.json_data, media_type=_MEDIA_TYPE)
except model.InvalidManifestException:
return None
@classmethod
def delete_manifest_by_digest(cls, namespace_name, repo_name, digest):
def delete_manifest_by_digest(self, namespace_name, repo_name, digest):
def _tag_view(tag):
return Tag(
name=tag.name,
@ -339,56 +315,31 @@ class PreOCIModel(DockerRegistryV2DataInterface):
tags = model.tag.delete_manifest_by_digest(namespace_name, repo_name, digest)
return [_tag_view(tag) for tag in tags]
@classmethod
def _docker_v1_metadata(cls, namespace_name, repo_name, repo_image):
"""
Returns a DockerV1Metadata object for the given 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,
content_checksum=repo_image.storage.content_checksum,
compat_json=repo_image.v1_json_metadata,
created=repo_image.created,
comment=repo_image.comment,
command=repo_image.command,
# TODO: make sure this isn't needed anywhere, as it is expensive to lookup
parent_image_id=None,
)
@classmethod
def get_docker_v1_metadata_by_tag(cls, namespace_name, repo_name, tag_name):
def get_docker_v1_metadata_by_tag(self, namespace_name, repo_name, tag_name):
try:
repo_img = model.tag.get_tag_image(namespace_name, repo_name, tag_name, include_storage=True)
return cls._docker_v1_metadata(namespace_name, repo_name, repo_img)
return _docker_v1_metadata(namespace_name, repo_name, repo_img)
except DataModelException:
return None
@classmethod
def get_docker_v1_metadata_by_image_id(cls, namespace_name, repo_name, docker_image_ids):
def get_docker_v1_metadata_by_image_id(self, namespace_name, repo_name, docker_image_ids):
repo = model.repository.get_repository(namespace_name, repo_name)
if repo is None:
return {}
images_query = model.image.lookup_repository_images(repo, docker_image_ids)
return {image.docker_image_id: cls._docker_v1_metadata(namespace_name, repo_name, image)
return {image.docker_image_id: _docker_v1_metadata(namespace_name, repo_name, image)
for image in images_query}
@classmethod
def get_parents_docker_v1_metadata(cls, namespace_name, repo_name, docker_image_id):
def get_parents_docker_v1_metadata(self, namespace_name, repo_name, docker_image_id):
repo_image = model.image.get_repo_image(namespace_name, repo_name, docker_image_id)
if repo_image is None:
return []
parents = model.image.get_parent_images(namespace_name, repo_name, repo_image)
return [cls._docker_v1_metadata(namespace_name, repo_name, image) for image in parents]
return [_docker_v1_metadata(namespace_name, repo_name, image) for image in parents]
@classmethod
def create_manifest_and_update_tag(cls, namespace_name, repo_name, tag_name, manifest_digest,
def create_manifest_and_update_tag(self, namespace_name, repo_name, tag_name, manifest_digest,
manifest_bytes):
try:
model.tag.associate_generated_tag_manifest(namespace_name, repo_name, tag_name,
@ -397,8 +348,7 @@ class PreOCIModel(DockerRegistryV2DataInterface):
# It's already there!
pass
@classmethod
def synthesize_v1_image(cls, repository, storage, image_id, created, comment, command,
def synthesize_v1_image(self, repository, storage, image_id, created, comment, command,
compat_json, parent_image_id):
repo = model.repository.get_repository(repository.namespace_name, repository.name)
if repo is None:
@ -417,18 +367,16 @@ class PreOCIModel(DockerRegistryV2DataInterface):
repo_image = model.image.synthesize_v1_image(repo, storage_obj, image_id, created, comment,
command, compat_json, parent_image)
return cls._docker_v1_metadata(repo.namespace_user.username, repo.name, repo_image)
return _docker_v1_metadata(repo.namespace_user.username, repo.name, repo_image)
@classmethod
def save_manifest(cls, namespace_name, repo_name, tag_name, leaf_layer_docker_id, manifest_digest,
manifest_bytes):
def save_manifest(self, namespace_name, repo_name, tag_name, leaf_layer_docker_id,
manifest_digest, manifest_bytes):
(_, newly_created) = model.tag.store_tag_manifest(namespace_name, repo_name, tag_name,
leaf_layer_docker_id, manifest_digest,
manifest_bytes)
return newly_created
@classmethod
def repository_tags(cls, namespace_name, repo_name, limit, offset):
def repository_tags(self, namespace_name, repo_name, limit, offset):
def _tag_view(tag):
return Tag(
name=tag.name,
@ -443,15 +391,12 @@ class PreOCIModel(DockerRegistryV2DataInterface):
tags_query = tags_query.limit(limit).offset(offset)
return [_tag_view(tag) for tag in tags_query]
@classmethod
def get_visible_repositories(cls, username, limit, offset):
def get_visible_repositories(self, username, limit, offset):
query = model.repository.get_visible_repositories(username, include_public=(username is None))
query = query.limit(limit).offset(offset)
return [cls._repository_for_repo(repo) for repo in query]
return [_repository_for_repo(repo) for repo in query]
@classmethod
def create_blob_upload(cls, namespace_name, repo_name, upload_uuid, location_name,
storage_metadata):
def create_blob_upload(self, namespace_name, repo_name, upload_uuid, location_name, storage_metadata):
try:
model.blob.initiate_upload(namespace_name, repo_name, upload_uuid, location_name,
storage_metadata)
@ -459,8 +404,7 @@ class PreOCIModel(DockerRegistryV2DataInterface):
except database.Repository.DoesNotExist:
return False
@classmethod
def blob_upload_by_uuid(cls, namespace_name, repo_name, upload_uuid):
def blob_upload_by_uuid(self, namespace_name, repo_name, upload_uuid):
try:
found = model.blob.get_blob_upload(namespace_name, repo_name, upload_uuid)
except model.InvalidBlobUpload:
@ -480,8 +424,7 @@ class PreOCIModel(DockerRegistryV2DataInterface):
storage_metadata=found.storage_metadata,
)
@classmethod
def update_blob_upload(cls, blob_upload):
def update_blob_upload(self, blob_upload):
# Lookup the blob upload object.
try:
blob_upload_record = model.blob.get_blob_upload(blob_upload.repo_namespace_name,
@ -498,21 +441,18 @@ class PreOCIModel(DockerRegistryV2DataInterface):
blob_upload_record.sha_state = blob_upload.sha_state
blob_upload_record.save()
@classmethod
def delete_blob_upload(cls, namespace_name, repo_name, uuid):
def delete_blob_upload(self, namespace_name, repo_name, uuid):
try:
found = model.blob.get_blob_upload(namespace_name, repo_name, uuid)
found.delete_instance()
except model.InvalidBlobUpload:
return
@classmethod
def create_blob_and_temp_tag(cls, namespace_name, repo_name, blob_digest, blob_upload,
def create_blob_and_temp_tag(self, namespace_name, repo_name, blob_digest, blob_upload,
expiration_sec):
location_obj = model.storage.get_image_location_for_name(blob_upload.location_name)
blob_record = model.blob.store_blob_record_and_temp_link(namespace_name, repo_name,
blob_digest,
location_obj.id,
blob_digest, location_obj.id,
blob_upload.byte_count,
expiration_sec,
blob_upload.uncompressed_byte_count)
@ -523,8 +463,7 @@ class PreOCIModel(DockerRegistryV2DataInterface):
locations=[blob_upload.location_name],
)
@classmethod
def lookup_blobs_by_digest(cls, namespace_name, repo_name, digests):
def lookup_blobs_by_digest(self, namespace_name, repo_name, digests):
def _blob_view(blob_record):
return Blob(
uuid=blob_record.uuid,
@ -539,8 +478,7 @@ class PreOCIModel(DockerRegistryV2DataInterface):
query = model.storage.lookup_repo_storages_by_content_checksum(repo, digests)
return {storage.content_checksum: _blob_view(storage) for storage in query}
@classmethod
def get_blob_by_digest(cls, namespace_name, repo_name, digest):
def get_blob_by_digest(self, namespace_name, repo_name, digest):
try:
blob_record = model.blob.get_repo_blob_by_digest(namespace_name, repo_name, digest)
return Blob(
@ -552,13 +490,11 @@ class PreOCIModel(DockerRegistryV2DataInterface):
except model.BlobDoesNotExist:
return None
@classmethod
def save_bittorrent_pieces(cls, blob, piece_size, piece_bytes):
def save_bittorrent_pieces(self, blob, piece_size, piece_bytes):
blob_record = model.storage.get_storage_by_uuid(blob.uuid)
model.storage.save_torrent_info(blob_record, piece_size, piece_bytes)
@classmethod
def create_manifest_labels(cls, namespace_name, repo_name, manifest_digest, labels):
def create_manifest_labels(self, namespace_name, repo_name, manifest_digest, labels):
if not labels:
# No point in doing anything more.
return
@ -568,7 +504,42 @@ class PreOCIModel(DockerRegistryV2DataInterface):
model.label.create_manifest_label(tag_manifest, label.key, label.value, label.source_type,
label.media_type)
@classmethod
def get_blob_path(cls, blob):
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 _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,
content_checksum=repo_image.storage.content_checksum,
compat_json=repo_image.v1_json_metadata,
created=repo_image.created,
comment=repo_image.comment,
command=repo_image.command,
# TODO: make sure this isn't needed anywhere, as it is expensive to lookup
parent_image_id=None,
)
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)
)
pre_oci_model = PreOCIModel()