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 class Repository(namedtuple('Repository', ['id', 'name', 'namespace_name', 'description', 'is_public'])): """ Repository represents a namespaced collection of tags. """ def _repository_for_repo(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) ) class DockerRegistryV1DataInterface(object): """ Interface that represents all data store interactions required by a Docker Registry v1. """ @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() 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) @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 @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) @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 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 @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 repo = repo_image.repository model.tag.create_temporary_hidden_tag(repo, repo_image, expiration) @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) @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 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) @classmethod def load_token(cls, token): try: model.token.load_token_data(token) return True except model.InvalidTokenException: return False @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) @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) @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)) @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]