create interfaces for v1 and v2 data model

This commit is contained in:
Jimmy Zelinskie 2016-08-30 15:05:15 -04:00
parent b775458d4b
commit c06d395f96
14 changed files with 1048 additions and 732 deletions

View file

@ -1,263 +1,435 @@
from collections import namedtuple
from app import app, storage as store
from data import model
from data.model import db_transaction
from util.morecollections import AttrDict
from data.interfaces.common import repository_for_repo
def placement_locations_docker_v1(namespace_name, repo_name, image_id):
""" Returns all the placements for the image with the given V1 Docker ID, found under the
given repository or None if no image was found.
class Repository(namedtuple('Repository', ['id', 'name', 'namespace_name', 'description',
'is_public'])):
"""
repo_image = model.image.get_repo_image_and_storage(namespace_name, repo_name, image_id)
if repo_image is None or repo_image.storage is None:
return None
return repo_image.storage.locations
def placement_locations_and_path_docker_v1(namespace_name, repo_name, image_id):
""" Returns a tuple of the placements and storage path location for the image with the
given V1 Docker ID, found under the given repository or None if no image was found.
Repository represents a namespaced collection of tags.
"""
repo_image = model.image.get_repo_image_extended(namespace_name, repo_name, image_id)
if not repo_image or repo_image.storage is None:
return None, None
return repo_image.storage.locations, model.storage.get_layer_path(repo_image.storage)
def docker_v1_metadata(namespace_name, repo_name, image_id):
""" Returns various pieces of metadata associated with an image with the given V1 Docker ID,
including the checksum and its V1 JSON metadata.
def _repository_for_repo(repo):
"""
repo_image = model.image.get_repo_image(namespace_name, repo_name, image_id)
if repo_image is None:
return None
return AttrDict({
'namespace_name': namespace_name,
'repo_name': repo_name,
'image_id': image_id,
'checksum': repo_image.v1_checksum,
'compat_json': repo_image.v1_json_metadata,
})
def update_docker_v1_metadata(namespace_name, repo_name, image_id, created_date_str, comment,
command, compat_json, parent_image_id=None):
""" Updates various pieces of V1 metadata associated with a particular image. """
parent_image = None
if parent_image_id is not None:
parent_image = model.image.get_repo_image(namespace_name, repo_name, parent_image_id)
model.image.set_image_metadata(image_id, namespace_name, repo_name, created_date_str, comment,
command, compat_json, parent=parent_image)
def storage_exists(namespace_name, repo_name, image_id):
""" Returns whether storage already exists for the image with the V1 Docker ID under the
given repository.
Returns a Repository object representing the repo data model instance given.
"""
repo_image = model.image.get_repo_image_and_storage(namespace_name, repo_name, image_id)
if repo_image is None or repo_image.storage is None:
return False
if repo_image.storage.uploading:
return False
layer_path = model.storage.get_layer_path(repo_image.storage)
return store.exists(repo_image.storage.locations, layer_path)
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)
)
def store_docker_v1_checksums(namespace_name, repo_name, image_id, checksum, content_checksum):
""" Stores the various V1 checksums for the image with the V1 Docker ID. """
repo_image = model.image.get_repo_image_and_storage(namespace_name, repo_name, image_id)
if repo_image is None or repo_image.storage is None:
return
class DockerRegistryV1DataInterface(object):
"""
Interface that represents all data store interactions required by a Docker Registry v1.
"""
with db_transaction():
repo_image.storage.content_checksum = content_checksum
repo_image.v1_checksum = checksum
@classmethod
def placement_locations_docker_v1(cls, namespace_name, repo_name, image_id):
"""
Returns all the placements for the image with the given V1 Docker ID, found under the given
repository or None if no image was found.
"""
raise NotImplementedError()
@classmethod
def placement_locations_and_path_docker_v1(cls, namespace_name, repo_name, image_id):
"""
Returns all the placements for the image with the given V1 Docker ID, found under the given
repository or None if no image was found.
"""
raise NotImplementedError()
@classmethod
def docker_v1_metadata(cls, namespace_name, repo_name, image_id):
"""
Returns various pieces of metadata associated with an image with the given V1 Docker ID,
including the checksum and its V1 JSON metadata.
"""
raise NotImplementedError()
@classmethod
def update_docker_v1_metadata(cls, namespace_name, repo_name, image_id, created_date_str, comment,
command, compat_json, parent_image_id=None):
"""
Updates various pieces of V1 metadata associated with a particular image.
"""
raise NotImplementedError()
@classmethod
def storage_exists(cls, namespace_name, repo_name, image_id):
"""
Returns whether storage already exists for the image with the V1 Docker ID under the given
repository.
"""
raise NotImplementedError()
@classmethod
def store_docker_v1_checksums(cls, namespace_name, repo_name, image_id, checksum, content_checksum):
"""
Stores the various V1 checksums for the image with the V1 Docker ID.
"""
raise NotImplementedError()
@classmethod
def is_image_uploading(cls, namespace_name, repo_name, image_id):
"""
Returns whether the image with the V1 Docker ID is currently marked as uploading.
"""
raise NotImplementedError()
@classmethod
def update_image_uploading(cls, namespace_name, repo_name, image_id, is_uploading):
""" Marks the image with the V1 Docker ID with the given uploading status. """
raise NotImplementedError()
@classmethod
def update_image_sizes(cls, namespace_name, repo_name, image_id, size, uncompressed_size):
"""
Updates the sizing information for the image with the given V1 Docker ID.
"""
raise NotImplementedError()
@classmethod
def get_image_size(cls, namespace_name, repo_name, image_id):
"""
Returns the wire size of the image with the given Docker V1 ID.
"""
raise NotImplementedError()
@classmethod
def create_bittorrent_pieces(cls, namespace_name, repo_name, image_id, pieces_bytes):
"""
Saves the BitTorrent piece hashes for the image with the given Docker V1 ID.
"""
raise NotImplementedError()
@classmethod
def image_ancestry(cls, namespace_name, repo_name, image_id):
"""
Returns a list containing the full ancestry of Docker V1 IDs, in order, for the image with the
given Docker V1 ID.
"""
raise NotImplementedError()
@classmethod
def repository_exists(cls, namespace_name, repo_name):
"""
Returns whether the repository with the given name and namespace exists.
"""
raise NotImplementedError()
@classmethod
def create_or_link_image(cls, username, namespace_name, repo_name, image_id, storage_location):
"""
Adds the given image to the given repository, by either linking to an existing image visible to
the user with the given username, or creating a new one if no existing image matches.
"""
raise NotImplementedError()
@classmethod
def create_temp_hidden_tag(cls, namespace_name, repo_name, image_id, expiration):
"""
Creates a hidden tag under the matching namespace pointing to the image with the given V1 Docker
ID.
"""
raise NotImplementedError()
@classmethod
def list_tags(cls, namespace_name, repo_name):
"""
Returns all the tags defined in the repository with the given namespace and name.
"""
raise NotImplementedError()
@classmethod
def create_or_update_tag(cls, namespace_name, repo_name, image_id, tag_name):
"""
Creates or updates a tag under the matching repository to point to the image with the given
Docker V1 ID.
"""
raise NotImplementedError()
@classmethod
def find_image_id_by_tag(cls, namespace_name, repo_name, tag_name):
"""
Returns the Docker V1 image ID for the HEAD image for the tag with the given name under the
matching repository, or None if none.
"""
raise NotImplementedError()
@classmethod
def delete_tag(cls, namespace_name, repo_name, tag_name):
""" Deletes the given tag from the given repository. """
raise NotImplementedError()
@classmethod
def load_token(cls, token):
"""
Loads the data associated with the given (deprecated) access token, and, if
found returns True.
"""
raise NotImplementedError()
@classmethod
def verify_robot(cls, username, token):
"""
Returns True if the given robot username and token match an existing robot
account.
"""
raise NotImplementedError()
@classmethod
def change_user_password(cls, user, new_password):
"""
Changes the password associated with the given user.
"""
raise NotImplementedError()
@classmethod
def get_repository(cls, namespace_name, repo_name):
"""
Returns the repository with the given name under the given namespace or None
if none.
"""
raise NotImplementedError()
@classmethod
def create_repository(cls, namespace_name, repo_name, user=None):
"""
Creates a new repository under the given namespace with the given name, for
the given user.
"""
raise NotImplementedError()
@classmethod
def repository_is_public(cls, namespace_name, repo_name):
"""
Returns whether the repository with the given name under the given namespace
is public. If no matching repository was found, returns False.
"""
raise NotImplementedError()
@classmethod
def validate_oauth_token(cls, token):
""" Returns whether the given OAuth token validates. """
raise NotImplementedError()
@classmethod
def get_sorted_matching_repositories(cls, search_term, only_public, can_read, limit):
"""
Returns a sorted list of repositories matching the given search term.
can_read is a callback that will be invoked for each repository found, to
filter results to only those visible to the current user (if any).
"""
raise NotImplementedError()
class PreOCIModel(DockerRegistryV1DataInterface):
"""
PreOCIModel implements the data model for the v1 Docker Registry protocol using a database schema
before it was changed to support the OCI specification.
"""
@classmethod
def placement_locations_docker_v1(cls, namespace_name, repo_name, image_id):
repo_image = model.image.get_repo_image_and_storage(namespace_name, repo_name, image_id)
if repo_image is None or repo_image.storage is None:
return None
return repo_image.storage.locations
@classmethod
def placement_locations_and_path_docker_v1(cls, namespace_name, repo_name, image_id):
repo_image = model.image.get_repo_image_extended(namespace_name, repo_name, image_id)
if not repo_image or repo_image.storage is None:
return None, None
return repo_image.storage.locations, model.storage.get_layer_path(repo_image.storage)
@classmethod
def docker_v1_metadata(cls, namespace_name, repo_name, image_id):
repo_image = model.image.get_repo_image(namespace_name, repo_name, image_id)
if repo_image is None:
return None
return AttrDict({
'namespace_name': namespace_name,
'repo_name': repo_name,
'image_id': image_id,
'checksum': repo_image.v1_checksum,
'compat_json': repo_image.v1_json_metadata,
})
@classmethod
def update_docker_v1_metadata(cls, namespace_name, repo_name, image_id, created_date_str, comment,
command, compat_json, parent_image_id=None):
parent_image = None
if parent_image_id is not None:
parent_image = model.image.get_repo_image(namespace_name, repo_name, parent_image_id)
model.image.set_image_metadata(image_id, namespace_name, repo_name, created_date_str, comment,
command, compat_json, parent=parent_image)
@classmethod
def storage_exists(cls, namespace_name, repo_name, image_id):
repo_image = model.image.get_repo_image_and_storage(namespace_name, repo_name, image_id)
if repo_image is None or repo_image.storage is None:
return False
if repo_image.storage.uploading:
return False
layer_path = model.storage.get_layer_path(repo_image.storage)
return store.exists(repo_image.storage.locations, layer_path)
@classmethod
def store_docker_v1_checksums(cls, namespace_name, repo_name, image_id, checksum, content_checksum):
repo_image = model.image.get_repo_image_and_storage(namespace_name, repo_name, image_id)
if repo_image is None or repo_image.storage is None:
return
with db_transaction():
repo_image.storage.content_checksum = content_checksum
repo_image.v1_checksum = checksum
repo_image.storage.save()
repo_image.save()
@classmethod
def is_image_uploading(cls, namespace_name, repo_name, image_id):
repo_image = model.image.get_repo_image_and_storage(namespace_name, repo_name, image_id)
if repo_image is None or repo_image.storage is None:
return False
return repo_image.storage.uploading
@classmethod
def update_image_uploading(cls, namespace_name, repo_name, image_id, is_uploading):
repo_image = model.image.get_repo_image_and_storage(namespace_name, repo_name, image_id)
if repo_image is None or repo_image.storage is None:
return
repo_image.storage.uploading = is_uploading
repo_image.storage.save()
repo_image.save()
return repo_image.storage
@classmethod
def update_image_sizes(cls, namespace_name, repo_name, image_id, size, uncompressed_size):
model.storage.set_image_storage_metadata(image_id, namespace_name, repo_name, size,
uncompressed_size)
def is_image_uploading(namespace_name, repo_name, image_id):
""" Returns whether the image with the V1 Docker ID is currently marked as uploading. """
repo_image = model.image.get_repo_image_and_storage(namespace_name, repo_name, image_id)
if repo_image is None or repo_image.storage is None:
return False
@classmethod
def get_image_size(cls, namespace_name, repo_name, image_id):
repo_image = model.image.get_repo_image_and_storage(namespace_name, repo_name, image_id)
if repo_image is None or repo_image.storage is None:
return None
return repo_image.storage.image_size
return repo_image.storage.uploading
@classmethod
def create_bittorrent_pieces(cls, namespace_name, repo_name, image_id, pieces_bytes):
repo_image = model.image.get_repo_image_and_storage(namespace_name, repo_name, image_id)
if repo_image is None or repo_image.storage is None:
return
model.storage.save_torrent_info(repo_image.storage, app.config['BITTORRENT_PIECE_SIZE'],
pieces_bytes)
def update_image_uploading(namespace_name, repo_name, image_id, is_uploading):
""" Marks the image with the V1 Docker ID with the given uploading status. """
repo_image = model.image.get_repo_image_and_storage(namespace_name, repo_name, image_id)
if repo_image is None or repo_image.storage is None:
return
@classmethod
def image_ancestry(cls, namespace_name, repo_name, image_id):
try:
image = model.image.get_image_by_id(namespace_name, repo_name, image_id)
except model.InvalidImageException:
return None
repo_image.storage.uploading = is_uploading
repo_image.storage.save()
return repo_image.storage
parents = model.image.get_parent_images(namespace_name, repo_name, image)
ancestry_docker_ids = [image.docker_image_id]
ancestry_docker_ids.extend([parent.docker_image_id for parent in parents])
return ancestry_docker_ids
@classmethod
def repository_exists(cls, namespace_name, repo_name):
repo = model.repository.get_repository(namespace_name, repo_name)
return repo is not None
def update_image_sizes(namespace_name, repo_name, image_id, size, uncompressed_size):
""" Updates the sizing information for the image with the given V1 Docker ID. """
model.storage.set_image_storage_metadata(image_id, namespace_name, repo_name, size,
uncompressed_size)
@classmethod
def create_or_link_image(cls, username, namespace_name, repo_name, image_id, storage_location):
repo = model.repository.get_repository(namespace_name, repo_name)
model.image.find_create_or_link_image(image_id, repo, username, {}, storage_location)
@classmethod
def create_temp_hidden_tag(cls, namespace_name, repo_name, image_id, expiration):
repo_image = model.image.get_repo_image(namespace_name, repo_name, image_id)
if repo_image is None:
return
def get_image_size(namespace_name, repo_name, image_id):
""" Returns the wire size of the image with the given Docker V1 ID. """
repo_image = model.image.get_repo_image_and_storage(namespace_name, repo_name, image_id)
if repo_image is None or repo_image.storage is None:
return None
repo = repo_image.repository
model.tag.create_temporary_hidden_tag(repo, repo_image, expiration)
return repo_image.storage.image_size
@classmethod
def list_tags(cls, namespace_name, repo_name):
return model.tag.list_repository_tags(namespace_name, repo_name)
@classmethod
def create_or_update_tag(cls, namespace_name, repo_name, image_id, tag_name):
model.tag.create_or_update_tag(namespace_name, repo_name, tag_name, image_id)
def create_bittorrent_pieces(namespace_name, repo_name, image_id, pieces_bytes):
""" Saves the bittorrent piece hashes for the image with the given Docker V1 ID. """
repo_image = model.image.get_repo_image_and_storage(namespace_name, repo_name, image_id)
if repo_image is None or repo_image.storage is None:
return
@classmethod
def find_image_id_by_tag(cls, namespace_name, repo_name, tag_name):
try:
tag_image = model.tag.get_tag_image(namespace_name, repo_name, tag_name)
except model.DataModelException:
return None
model.storage.save_torrent_info(repo_image.storage, app.config['BITTORRENT_PIECE_SIZE'],
pieces_bytes)
return tag_image.docker_image_id
@classmethod
def delete_tag(cls, namespace_name, repo_name, tag_name):
model.tag.delete_tag(namespace_name, repo_name, tag_name)
def image_ancestry(namespace_name, repo_name, image_id):
""" Returns a list containing the full ancestry of Docker V1 IDs, in order, for the image with
the givne Docker V1 ID.
"""
try:
image = model.image.get_image_by_id(namespace_name, repo_name, image_id)
except model.InvalidImageException:
return None
@classmethod
def load_token(cls, token):
try:
model.token.load_token_data(token)
return True
except model.InvalidTokenException:
return False
parents = model.image.get_parent_images(namespace_name, repo_name, image)
ancestry_docker_ids = [image.docker_image_id]
ancestry_docker_ids.extend([parent.docker_image_id for parent in parents])
return ancestry_docker_ids
@classmethod
def verify_robot(cls, username, token):
try:
return bool(model.user.verify_robot(username, token))
except model.InvalidRobotException:
return False
@classmethod
def change_user_password(cls, user, new_password):
model.user.change_password(user, new_password)
def repository_exists(namespace_name, repo_name):
""" Returns whether the repository with the given name and namespace exists. """
repo = model.repository.get_repository(namespace_name, repo_name)
return repo is not None
@classmethod
def get_repository(cls, namespace_name, repo_name):
repo = model.repository.get_repository(namespace_name, repo_name)
if repo is None:
return None
return _repository_for_repo(repo)
@classmethod
def create_repository(cls, namespace_name, repo_name, user=None):
model.repository.create_repository(namespace_name, repo_name, user)
def create_or_link_image(username, namespace_name, repo_name, image_id, storage_location):
""" Adds the given image to the given repository, by either linking to an existing image
visible to the user with the given username, or creating a new one if no existing image
matches.
"""
repo = model.repository.get_repository(namespace_name, repo_name)
model.image.find_create_or_link_image(image_id, repo, username, {}, storage_location)
@classmethod
def repository_is_public(cls, namespace_name, repo_name):
return model.repository.repository_is_public(namespace_name, repo_name)
@classmethod
def validate_oauth_token(cls, token):
return bool(model.oauth.validate_access_token(token))
def create_temp_hidden_tag(namespace_name, repo_name, image_id, expiration):
""" Creates a hidden tag under the matching namespace pointing to the image with the given V1
Docker ID.
"""
repo_image = model.image.get_repo_image(namespace_name, repo_name, image_id)
if repo_image is None:
return
repo = repo_image.repository
model.tag.create_temporary_hidden_tag(repo, repo_image, expiration)
def list_tags(namespace_name, repo_name):
""" Returns all the tags defined in the repository with the given namespace and name. """
return model.tag.list_repository_tags(namespace_name, repo_name)
def create_or_update_tag(namespace_name, repo_name, image_id, tag_name):
""" Creates or updates a tag under the matching repository to point to the image with the given
Docker V1 ID.
"""
model.tag.create_or_update_tag(namespace_name, repo_name, tag_name, image_id)
def find_image_id_by_tag(namespace_name, repo_name, tag_name):
""" Returns the Docker V1 image ID for the HEAD image for the tag with the given name under
the matching repository, or None if none.
"""
try:
tag_image = model.tag.get_tag_image(namespace_name, repo_name, tag_name)
except model.DataModelException:
return None
return tag_image.docker_image_id
def delete_tag(namespace_name, repo_name, tag_name):
""" Deletes the given tag from the given repository. """
model.tag.delete_tag(namespace_name, repo_name, tag_name)
def load_token(token):
""" Loads the data associated with the given (deprecated) access token, and, if found
returns True.
"""
try:
model.token.load_token_data(token)
return True
except model.InvalidTokenException:
return False
def verify_robot(username, token):
""" Returns True if the given robot username and token match an existing robot
account.
"""
try:
return bool(model.user.verify_robot(username, token))
except model.InvalidRobotException:
return False
def change_user_password(user, new_password):
""" Changes the password associated with the given user. """
model.user.change_password(user, new_password)
def get_repository(namespace_name, repo_name):
""" Returns the repository with the given name under the given namespace or None if none. """
repo = model.repository.get_repository(namespace_name, repo_name)
if repo is None:
return None
return repository_for_repo(repo)
def create_repository(namespace_name, repo_name, user=None):
""" Creates a new repository under the given namespace with the given name, for the given user.
"""
model.repository.create_repository(namespace_name, repo_name, user)
def repository_is_public(namespace_name, repo_name):
""" Returns whether the repository with the given name under the given namespace is public.
If no matching repository was found, returns False.
"""
return model.repository.repository_is_public(namespace_name, repo_name)
def validate_oauth_token(token):
""" Returns whether the given OAuth token validates. """
return bool(model.oauth.validate_access_token(token))
def get_sorted_matching_repositories(search_term, only_public, can_read, limit):
""" Returns a sorted list of repositories matching the given search term. can_read is a callback
that will be invoked for each repository found, to filter results to only those visible to
the current user (if any).
"""
repos = model.repository.get_sorted_matching_repositories(search_term, only_public, can_read,
limit=limit)
return [repository_for_repo(repo) for repo in repos]
@classmethod
def get_sorted_matching_repositories(cls, search_term, only_public, can_read, limit):
repos = model.repository.get_sorted_matching_repositories(search_term, only_public, can_read,
limit=limit)
return [_repository_for_repo(repo) for repo in repos]