diff --git a/endpoints/api/logs.py b/endpoints/api/logs.py index 9966aaa6c..b7f645bf1 100644 --- a/endpoints/api/logs.py +++ b/endpoints/api/logs.py @@ -16,6 +16,8 @@ SERVICE_LEVEL_LOG_KINDS = set(['service_key_create', 'service_key_approve', 'ser 'service_key_modify', 'service_key_extend', 'service_key_rotate']) + + def _validate_logs_arguments(start_time, end_time): if start_time: try: diff --git a/endpoints/api/repository.py b/endpoints/api/repository.py index f8f41de44..6ce980d31 100644 --- a/endpoints/api/repository.py +++ b/endpoints/api/repository.py @@ -10,12 +10,12 @@ from datetime import timedelta, datetime from flask import request, abort from app import dockerfile_build_queue, tuf_metadata_api -from data import model, oci_model from endpoints.api import (format_date, nickname, log_action, validate_json_request, require_repo_read, require_repo_write, require_repo_admin, RepositoryParamResource, resource, parse_args, ApiResource, request_error, require_scope, path_param, page_support, query_param, truthy_bool, show_if) +from endpoints.api.repository_models_pre_oci import pre_oci_model as model from endpoints.exception import (Unauthorized, NotFound, InvalidRequest, ExceedsLicenseException, DownstreamIssue) from endpoints.api.billing import lookup_allowed_private_repos, get_namespace_plan @@ -27,12 +27,12 @@ from auth.auth_context import get_authenticated_user from auth import scopes from util.names import REPOSITORY_NAME_REGEX - logger = logging.getLogger(__name__) REPOS_PER_PAGE = 100 MAX_DAYS_IN_3_MONTHS = 92 + def check_allowed_private_repos(namespace): """ Checks to see if the given namespace has reached its private repository limit. If so, raises a ExceedsLicenseException. @@ -106,8 +106,7 @@ class RepositoryList(ApiResource): repository_name = req['repository'] visibility = req['visibility'] - existing = model.repository.get_repository(namespace_name, repository_name) - if existing: + if model.repo_exists(namespace_name, repository_name): raise request_error(message='Repository already exists') visibility = req['visibility'] @@ -119,22 +118,19 @@ class RepositoryList(ApiResource): raise InvalidRequest('Invalid repository name') kind = req.get('repo_kind', 'image') or 'image' - repo = model.repository.create_repository(namespace_name, repository_name, owner, visibility, - repo_kind=kind) - repo.description = req['description'] - repo.save() + model.create_repo(namespace_name, repository_name, owner, req['description'], visibility=visibility, + repo_kind=kind) log_action('create_repo', namespace_name, {'repo': repository_name, - 'namespace': namespace_name}, repo=repo) + 'namespace': namespace_name}, repo_name=repository_name) return { - 'namespace': namespace_name, - 'name': repository_name, - 'kind': kind, - }, 201 + 'namespace': namespace_name, + 'name': repository_name, + 'kind': kind, + }, 201 raise Unauthorized() - @require_scope(scopes.READ_REPO) @nickname('listRepos') @parse_args() @@ -160,89 +156,18 @@ class RepositoryList(ApiResource): user = get_authenticated_user() username = user.username if user else None - next_page_token = None - repos = None - repo_kind = parsed_args['repo_kind'] + last_modified = parsed_args['last_modified'] + popularity = parsed_args['popularity'] - # Lookup the requested repositories (either starred or non-starred.) - if parsed_args['starred']: - if not username: - # No repositories should be returned, as there is no user. - abort(400) + if parsed_args['starred'] and not username: + # No repositories should be returned, as there is no user. + abort(400) - # Return the full list of repos starred by the current user that are still visible to them. - def can_view_repo(repo): - return ReadRepositoryPermission(repo.namespace_user.username, repo.name).can() + repos, next_page_token = model.get_repo_list( + parsed_args['starred'], user, parsed_args['repo_kind'], parsed_args['namespace'], username, + parsed_args['public'], page_token, last_modified, popularity) - unfiltered_repos = model.repository.get_user_starred_repositories(user, kind_filter=repo_kind) - repos = [repo for repo in unfiltered_repos if can_view_repo(repo)] - elif parsed_args['namespace']: - # Repositories filtered by namespace do not need pagination (their results are fairly small), - # so we just do the lookup directly. - repos = list(model.repository.get_visible_repositories(username=username, - include_public=parsed_args['public'], - namespace=parsed_args['namespace'], - kind_filter=repo_kind)) - else: - # Determine the starting offset for pagination. Note that we don't use the normal - # model.modelutil.paginate method here, as that does not operate over UNION queries, which - # get_visible_repositories will return if there is a logged-in user (for performance reasons). - # - # Also note the +1 on the limit, as paginate_query uses the extra result to determine whether - # there is a next page. - start_id = model.modelutil.pagination_start(page_token) - repo_query = model.repository.get_visible_repositories(username=username, - include_public=parsed_args['public'], - start_id=start_id, - limit=REPOS_PER_PAGE+1, - kind_filter=repo_kind) - - repos, next_page_token = model.modelutil.paginate_query(repo_query, limit=REPOS_PER_PAGE, - id_alias='rid') - - # Collect the IDs of the repositories found for subequent lookup of popularity - # and/or last modified. - if parsed_args['last_modified'] or parsed_args['popularity']: - repository_ids = [repo.rid for repo in repos] - - if parsed_args['last_modified']: - last_modified_map = model.repository.get_when_last_modified(repository_ids) - - if parsed_args['popularity']: - action_sum_map = model.log.get_repositories_action_sums(repository_ids) - - # Collect the IDs of the repositories that are starred for the user, so we can mark them - # in the returned results. - star_set = set() - if username: - starred_repos = model.repository.get_user_starred_repositories(user) - star_set = {starred.id for starred in starred_repos} - - def repo_view(repo_obj): - repo = { - 'namespace': repo_obj.namespace_user.username, - 'name': repo_obj.name, - 'description': repo_obj.description, - 'is_public': repo_obj.visibility_id == model.repository.get_public_repo_visibility().id, - 'kind': repo_kind, - } - - repo_id = repo_obj.rid - - if parsed_args['last_modified']: - repo['last_modified'] = last_modified_map.get(repo_id) - - if parsed_args['popularity']: - repo['popularity'] = float(action_sum_map.get(repo_id, 0)) - - if username: - repo['is_starred'] = repo_id in star_set - - return repo - - return { - 'repositories': [repo_view(repo) for repo in repos] - }, next_page_token + return {'repositories': [repo.to_dict() for repo in repos]}, next_page_token @resource('/v1/repository/') @@ -273,154 +198,63 @@ class Repository(RepositoryParamResource): def get(self, namespace, repository, parsed_args): """Fetch the specified repository.""" logger.debug('Get repo: %s/%s' % (namespace, repository)) - - repo = model.repository.get_repository(namespace, repository) + repo = model.get_repo(namespace, repository, get_authenticated_user()) if repo is None: raise NotFound() - can_write = ModifyRepositoryPermission(namespace, repository).can() - can_admin = AdministerRepositoryPermission(namespace, repository).can() + repo_data = repo.to_dict() + repo_data['can_write'] = ModifyRepositoryPermission(namespace, repository).can() + repo_data['can_admin'] = AdministerRepositoryPermission(namespace, repository).can() - is_starred = (model.repository.repository_is_starred(get_authenticated_user(), repo) - if get_authenticated_user() else False) - is_public = model.repository.is_repository_public(repo) - - # Note: This is *temporary* code for the new OCI model stuff. - if repo.kind.name == 'application': - def channel_view(channel): - return { - 'name': channel.name, - 'release': channel.linked_tag.name, - 'last_modified': format_date(datetime.fromtimestamp(channel.linked_tag.lifetime_start / 1000)), - } - - def release_view(release): - return { - 'name': release.name, - 'last_modified': format_date(datetime.fromtimestamp(release.lifetime_start / 1000)), - 'channels': releases_channels_map[release.name], - } - - channels = oci_model.channel.get_repo_channels(repo) - releases_channels_map = defaultdict(list) - for channel in channels: - releases_channels_map[channel.linked_tag.name].append(channel.name) - - repo_data = { - 'namespace': namespace, - 'name': repository, - 'kind': repo.kind.name, - 'description': repo.description, - 'can_write': can_write, - 'can_admin': can_admin, - 'is_public': is_public, - 'is_organization': repo.namespace_user.organization, - 'is_starred': is_starred, - 'channels': [channel_view(chan) for chan in channels], - 'releases': [release_view(release) for release in oci_model.release.get_release_objs(repo)], - } - - return repo_data - - # Older image-only repo code. - def tag_view(tag): - tag_info = { - 'name': tag.name, - 'image_id': tag.image.docker_image_id, - 'size': tag.image.aggregate_size - } - - if tag.lifetime_start_ts > 0: - last_modified = format_date(datetime.fromtimestamp(tag.lifetime_start_ts)) - tag_info['last_modified'] = last_modified - - if tag.lifetime_end_ts: - expiration = format_date(datetime.fromtimestamp(tag.lifetime_end_ts)) - tag_info['expiration'] = expiration - - if tag.tagmanifest is not None: - tag_info['manifest_digest'] = tag.tagmanifest.digest - - return tag_info - - stats = None - tags = model.tag.list_active_repo_tags(repo) - tag_dict = {tag.name: tag_view(tag) for tag in tags} - if parsed_args['includeStats']: + if parsed_args['includeStats'] and repo.repository_base_elements.kind_name != 'application': stats = [] found_dates = {} - start_date = datetime.now() - timedelta(days=MAX_DAYS_IN_3_MONTHS) - counts = model.log.get_repository_action_counts(repo, start_date) - for count in counts: - stats.append({ - 'date': count.date.isoformat(), - 'count': count.count, - }) - + for count in repo.counts: + stats.append(count.to_dict()) found_dates['%s/%s' % (count.date.month, count.date.day)] = True # Fill in any missing stats with zeros. for day in range(1, MAX_DAYS_IN_3_MONTHS): day_date = datetime.now() - timedelta(days=day) key = '%s/%s' % (day_date.month, day_date.day) - if not key in found_dates: + if key not in found_dates: stats.append({ 'date': day_date.date().isoformat(), 'count': 0, }) - repo_data = { - 'namespace': namespace, - 'name': repository, - 'kind': repo.kind.name, - 'description': repo.description, - 'tags': tag_dict, - 'can_write': can_write, - 'can_admin': can_admin, - 'is_public': is_public, - 'is_organization': repo.namespace_user.organization, - 'is_starred': is_starred, - 'status_token': repo.badge_token if not is_public else '', - 'trust_enabled': bool(features.SIGNING) and repo.trust_enabled, - 'tag_expiration_s': repo.namespace_user.removed_tag_expiration_s, - } - - if stats is not None: repo_data['stats'] = stats - return repo_data - @require_repo_write @nickname('updateRepo') @validate_json_request('RepoUpdate') def put(self, namespace, repository): """ Update the description in the specified repository. """ - repo = model.repository.get_repository(namespace, repository) - if repo: - values = request.get_json() - repo.description = values['description'] - repo.save() + if not model.repo_exists(namespace, repository): + raise NotFound() + + values = request.get_json() + model.set_description(namespace, repository, values['description']) + + log_action('set_repo_description', namespace, + {'repo': repository, 'namespace': namespace, 'description': values['description']}, + repo_name=repository) + return { + 'success': True + } - log_action('set_repo_description', namespace, - {'repo': repository, 'namespace': namespace, 'description': values['description']}, - repo=repo) - return { - 'success': True - } - raise NotFound() @require_repo_admin @nickname('deleteRepository') def delete(self, namespace, repository): """ Delete a repository. """ - model.repository.purge_repository(namespace, repository) - user = model.user.get_namespace_user(namespace) + username = model.purge_repository(namespace, repository) if features.BILLING: plan = get_namespace_plan(namespace) - check_repository_usage(user, plan) + model.check_repository_usage(username, plan) # Remove any builds from the queue. dockerfile_build_queue.delete_namespaced_items(namespace, repository) @@ -459,17 +293,16 @@ class RepositoryVisibility(RepositoryParamResource): @validate_json_request('ChangeVisibility') def post(self, namespace, repository): """ Change the visibility of a repository. """ - repo = model.repository.get_repository(namespace, repository) - if repo: + if model.repo_exists(namespace, repository): values = request.get_json() visibility = values['visibility'] if visibility == 'private': check_allowed_private_repos(namespace) - model.repository.set_repository_visibility(repo, visibility) + model.set_repository_visibility(namespace, repository, visibility) log_action('change_repo_visibility', namespace, {'repo': repository, 'namespace': namespace, 'visibility': values['visibility']}, - repo=repo) + repo_name=repository) return {'success': True} @@ -499,19 +332,17 @@ class RepositoryTrust(RepositoryParamResource): @validate_json_request('ChangeRepoTrust') def post(self, namespace, repository): """ Change the visibility of a repository. """ - repo = model.repository.get_repository(namespace, repository) - if not repo: + if not model.repo_exists(namespace, repository): raise NotFound() tags, _ = tuf_metadata_api.get_default_tags_with_expiration(namespace, repository) if tags and not tuf_metadata_api.delete_metadata(namespace, repository): - raise DownstreamIssue({'message': 'Unable to delete downstream trust metadata'}) - + raise DownstreamIssue({'message': 'Unable to delete downstream trust metadata'}) values = request.get_json() - model.repository.set_trust(repo, values['trust_enabled']) + model.set_trust(namespace, repository, values['trust_enabled']) log_action('change_repo_trust', namespace, {'repo': repository, 'namespace': namespace, 'trust_enabled': values['trust_enabled']}, - repo=repo) + repo_name=repository) return {'success': True} diff --git a/endpoints/api/repository_models_interface.py b/endpoints/api/repository_models_interface.py new file mode 100644 index 000000000..e823a6b66 --- /dev/null +++ b/endpoints/api/repository_models_interface.py @@ -0,0 +1,252 @@ +from abc import ABCMeta, abstractmethod +from collections import namedtuple, defaultdict + +from datetime import datetime +from six import add_metaclass + +import features +from endpoints.api import format_date + + +class RepositoryBaseElement(namedtuple('RepositoryBaseElement', + ['namespace_name', 'repository_name', 'is_starred', 'is_public', 'kind_name', + 'description', 'namespace_user_organization', + 'namespace_user_removed_tag_expiration_s', 'last_modified', 'action_count'])): + """ + Repository a single quay repository + :type namespace_name: string + :type repository_name: string + :type is_starred: boolean + :type is_public: boolean + :type kind_name: string + :type description: string + :type namespace_user_organization: boolean + :type should_last_modified: boolean + :type should_popularity: boolean + :type should_is_starred: boolean + """ + + def to_dict(self): + repo = { + 'namespace': self.namespace_name, + 'name': self.repository_name, + 'description': self.description, + 'is_public': self.is_public, + 'kind': self.kind_name, + } + + if self.should_last_modified: + repo['last_modified'] = self.last_modified + + if self.should_popularity: + repo['popularity'] = float(self.action_count if self.action_count else 0) + + if self.should_is_starred: + repo['is_starred'] = self.is_starred + + return repo + + +class ApplicationRepository( + namedtuple('ApplicationRepository', ['repository_base_elements', 'channels', 'releases'])): + """ + Repository a single quay repository + :type repository_base_elements: RepositoryBaseElement + :type channels: [Channel] + :type releases: [Release] + """ + + def to_dict(self, can_write, can_admin): + releases_channels_map = defaultdict(list) + for channel in self.channels: + releases_channels_map[channel.linked_tag_name].append(channel.name) + + repo_data = { + 'namespace': self.repository_base_elements.namespace_name, + 'name': self.repository_base_elements.repository_name, + 'kind': self.repository_base_elements.kind_name, + 'description': self.repository_base_elements.description, + 'can_write': can_write, + 'can_admin': can_admin, + 'is_public': self.repository_base_elements.is_public, + 'is_organization': self.repository_base_elements.namespace_user_organization, + 'is_starred': self.repository_base_elements.is_starred, + 'channels': [chan.to_dict() for chan in self.channels], + 'releases': [release.to_dict(releases_channels_map) for release in self.channels], + } + + return repo_data + + +class NonApplicationRepository(namedtuple('NonApplicationRepository', + ['repository_base_elements', 'tags', 'counts', 'badge_token', + 'trust_enabled'])): + """ + Repository a single quay repository + :type repository_base_elements: RepositoryBaseElement + :type tags: [Tag] + :type counts: [count] + :type badge_token: string + :type trust_enabled: boolean + """ + + def to_dict(self, can_write, can_admin): + return { + 'namespace': self.repository_base_elements.namespace_name, + 'name': self.repository_base_elements.repository_name, + 'kind': self.repository_base_elements.kind_name, + 'description': self.repository_base_elements.description, + 'can_write': can_write, + 'can_admin': can_admin, + 'is_public': self.repository_base_elements.is_public, + 'is_organization': self.repository_base_elements.namespace_user_organization, + 'is_starred': self.repository_base_elements.is_starred, + 'tags': {tag.name: tag.to_dict() for tag in self.tags}, + 'status_token': self.badge_token if not self.repository_base_elements.is_public else '', + 'trust_enabled': bool(features.SIGNING) and self.trust_enabled, + 'tag_expiration_s': self.repository_base_elements.namespace_user_removed_tag_expiration_s, + } + + +class Repository(namedtuple('Repository', ['namespace_name', 'repository_name', ])): + """ + Repository a single quay repository + :type namespace_name: string + :type repository_name: string + """ + + +class Channel(namedtuple('Channel', ['name', 'linked_tag_name', 'linked_tag_lifetime_start'])): + """ + Repository a single quay repository + :type name: string + :type linked_tag_name: string + :type linked_tag_lifetime_start: string + """ + + def to_dict(self): + return { + 'name': self.name, + 'release': self.linked_tag_name, + 'last_modified': format_date(datetime.fromtimestamp(self.linked_tag_lifetime_start / 1000)), + } + + +class Release(namedtuple('Channel', ['name', 'released', 'lifetime_start'])): + """ + Repository a single quay repository + :type name: string + :type released: string + :type last_modified: string + """ + + def to_dict(self, releases_channels_map): + return { + 'name': self.name, + 'last_modified': format_date(datetime.fromtimestamp(self.lifetime_start / 1000)), + 'channels': releases_channels_map[self.name], + } + + +class Tag(namedtuple('Tag', ['name', 'image_docker_image_id', 'image_aggregate_size', 'lifetime_start_ts', + 'tag_manifest_digest'])): + """ + :type name: string + :type image_docker_image_id: string + :type image_aggregate_size: int + :type lifetime_start_ts: int + :type tag_manifest_digest: string + + """ + + def to_dict(self): + tag_info = { + 'name': self.name, + 'image_id': self.image_docker_image_id, + 'size': self.image_aggregate_size + } + + if self.lifetime_start_ts > 0: + last_modified = format_date(datetime.fromtimestamp(self.lifetime_start_ts)) + tag_info['last_modified'] = last_modified + + if self.tag_manifest_digest is not None: + tag_info['manifest_digest'] = self.tag_manifest_digest + + return tag_info + + +class Count(namedtuple('Count', ['date', 'count'])): + """ + date: DateTime + count: int + """ + + def to_dict(self): + return { + 'date': self.date.isoformat(), + 'count': self.count, + } + + +@add_metaclass(ABCMeta) +class RepositoryDataInterface(object): + """ + Interface that represents all data store interactions required by a Repository. + """ + + @abstractmethod + def get_repo(self, namespace_name, repository_name, user): + """ + Returns a repository + """ + + @abstractmethod + def repo_exists(self, namespace_name, repository_name): + """ + Returns true if a repo exists and false if not + """ + + @abstractmethod + def create_repo(self, namespace, name, creating_user, description, visibility='private', + repo_kind='image'): + """ + Returns creates a new repo + """ + + @abstractmethod + def get_repo_list(self, starred, user, repo_kind, namespace, username, public, page_token, + last_modified, popularity): + """ + Returns a RepositoryBaseElement + """ + + @abstractmethod + def set_repository_visibility(self, namespace_name, repository_name, visibility): + """ + Sets a repository's visibility if it is found + """ + + @abstractmethod + def set_trust(self, namespace_name, repository_name, trust): + """ + Sets a repository's trust_enabled field if it is found + """ + + @abstractmethod + def set_description(self, namespace_name, repository_name, description): + """ + Sets a repository's description if it is found. + """ + + @abstractmethod + def purge_repository(self, namespace_name, repository_name): + """ + Removes a repository + """ + + @abstractmethod + def check_repository_usage(self, user_name, plan_found): + """ + Creates a notification for a user if they are over or under on their repository usage + """ diff --git a/endpoints/api/repository_models_pre_oci.py b/endpoints/api/repository_models_pre_oci.py new file mode 100644 index 000000000..ba5b1a81e --- /dev/null +++ b/endpoints/api/repository_models_pre_oci.py @@ -0,0 +1,167 @@ +from collections import defaultdict + +from datetime import datetime, timedelta + +from auth.permissions import ReadRepositoryPermission +from data import model, oci_model +from endpoints.api.repository_models_interface import RepositoryDataInterface, RepositoryBaseElement, Repository, \ + ApplicationRepository, ImageRepositoryRepository, Tag, Channel, Release, Count + +MAX_DAYS_IN_3_MONTHS = 92 +REPOS_PER_PAGE = 100 + + +def create_channel(channel, releases_channels_map): + releases_channels_map[channel.linked_tag_name].append(channel.name) + return Channel(channel.name, channel.linked_tag.name, channel.linked_tag.lifetime_start) + + +class PreOCIModel(RepositoryDataInterface): + """ + PreOCIModel implements the data model for the Repo Email using a database schema + before it was changed to support the OCI specification. + """ + + def check_repository_usage(self, username, plan_found): + private_repos = model.user.get_private_repo_count(username) + if plan_found is None: + repos_allowed = 0 + else: + repos_allowed = plan_found['privateRepos'] + + user_or_org = model.user.get_namespace_user(username) + if private_repos > repos_allowed: + model.notification.create_unique_notification('over_private_usage', user_or_org, + {'namespace': username}) + else: + model.notification.delete_notifications_by_kind(user_or_org, 'over_private_usage') + + def purge_repository(self, namespace_name, repository_name): + model.repository.purge_repository(namespace_name, repository_name) + user = model.user.get_namespace_user(namespace_name) + return user.username + + def set_description(self, namespace_name, repository_name, description): + repo = model.repository.get_repository(namespace_name, repository_name) + model.repository.set_description(repo, description) + + def set_trust(self, namespace_name, repository_name, trust): + repo = model.repository.get_repository(namespace_name, repository_name) + model.repository.set_trust(repo, trust) + + def set_repository_visibility(self, namespace_name, repository_name, visibility): + repo = model.repository.get_repository(namespace_name, repository_name) + model.repository.set_repository_visibility(repo, visibility) + + def get_repo_list(self, starred, user, repo_kind, namespace, username, public, page_token, + last_modified, popularity): + next_page_token = None + # Lookup the requested repositories (either starred or non-starred.) + if starred: + # Return the full list of repos starred by the current user that are still visible to them. + def can_view_repo(repo): + return ReadRepositoryPermission(repo.namespace_user.username, repo.name).can() + + unfiltered_repos = model.repository.get_user_starred_repositories(user, + kind_filter=repo_kind) + repos = [repo for repo in unfiltered_repos if can_view_repo(repo)] + elif namespace: + # Repositories filtered by namespace do not need pagination (their results are fairly small), + # so we just do the lookup directly. + repos = list( + model.repository.get_visible_repositories(username=username, include_public=public, + namespace=namespace, kind_filter=repo_kind)) + else: + # Determine the starting offset for pagination. Note that we don't use the normal + # model.modelutil.paginate method here, as that does not operate over UNION queries, which + # get_visible_repositories will return if there is a logged-in user (for performance reasons). + # + # Also note the +1 on the limit, as paginate_query uses the extra result to determine whether + # there is a next page. + start_id = model.modelutil.pagination_start(page_token) + repo_query = model.repository.get_visible_repositories(username=username, + include_public=public, + start_id=start_id, + limit=REPOS_PER_PAGE + 1, + kind_filter=repo_kind) + + repos, next_page_token = model.modelutil.paginate_query(repo_query, limit=REPOS_PER_PAGE, + id_alias='rid') + + # Collect the IDs of the repositories found for subequent lookup of popularity + # and/or last modified. + last_modified_map = {} + action_sum_map = {} + if last_modified or popularity: + repository_ids = [repo.rid for repo in repos] + + if last_modified: + last_modified_map = model.repository.get_when_last_modified(repository_ids) + + if popularity: + action_sum_map = model.log.get_repositories_action_sums(repository_ids) + + # Collect the IDs of the repositories that are starred for the user, so we can mark them + # in the returned results. + star_set = set() + if username: + starred_repos = model.repository.get_user_starred_repositories(user) + star_set = {starred.id for starred in starred_repos} + + return [ + RepositoryBaseElement(repo.namespace_user.username, repo.name, repo.id in star_set, + repo.visibility_id == model.repository.get_public_repo_visibility().id, + repo_kind, repo.description, repo.namespace_user.organization, + repo.namespace_user.removed_tag_expiration_s, + last_modified_map.get(repo.rid), + action_sum_map.get(repo.rid), last_modified, popularity, username) + for repo in repos + ], next_page_token + + def repo_exists(self, namespace_name, repository_name): + repo = model.repository.get_repository(namespace_name, repository_name) + if repo is None: + return False + + return True + + def create_repo(self, namespace_name, repository_name, owner, description, visibility='private', + repo_kind='image'): + repo = model.repository.create_repository(namespace_name, repository_name, owner, visibility, + repo_kind=repo_kind) + model.repository.set_description(repo, description) + return Repository(namespace_name, repository_name) + + def get_repo(self, namespace_name, repository_name, user): + repo = model.repository.get_repository(namespace_name, repository_name) + if repo is None: + return None + + is_starred = model.repository.repository_is_starred(user, repo) if user else False + is_public = model.repository.is_repository_public(repo) + base = RepositoryBaseElement(namespace_name, repository_name, is_starred, is_public, repo.kind.name, + repo.description, repo.namespace_user.organization, + repo.namespace_user.removed_tag_expiration_s, None, None) + + # Note: This is *temporary* code for the new OCI model stuff. + if base.kind_name == 'application': + channels = oci_model.channel.get_repo_channels(repo) + releases = oci_model.release.get_release_objs(repo) + + return ApplicationRepository(base, + [Channel(channel.name, channel.linked_tag.name, channel.linked_tag.lifetime_start) + for channel in channels], + [Release(release.name, release.released, release.lifetime_start) + for release in releases]) + + tags = model.tag.list_active_repo_tags(repo) + start_date = datetime.now() - timedelta(days=MAX_DAYS_IN_3_MONTHS) + counts = model.log.get_repository_action_counts(repo, start_date) + return NonApplicationRepository(base, + [Tag(tag.name, tag.image.docker_image_id, tag.image.aggregate_size, + tag.lifetime_start_ts, tag.tagmanifest.digest) for tag in tags], + [Count(count.date, count.count) for count in counts], + repo.badge_token, repo.trust_enabled) + + +pre_oci_model = PreOCIModel() diff --git a/endpoints/api/test/test_repository.py b/endpoints/api/test/test_repository.py index 999beb00d..f85cb0026 100644 --- a/endpoints/api/test/test_repository.py +++ b/endpoints/api/test/test_repository.py @@ -38,8 +38,8 @@ NOT_FOUND_RESPONSE = { ]) def test_post_changetrust(trust_enabled, repo_found, expected_body, expected_status, client): with patch('endpoints.api.repository.tuf_metadata_api') as mock_tuf: - with patch('endpoints.api.repository.model') as mock_model: - mock_model.repository.get_repository.return_value = MagicMock() if repo_found else None + with patch('endpoints.api.repository_models_pre_oci.model.repository.get_repository') as mock_model: + mock_model.return_value = MagicMock() if repo_found else None mock_tuf.get_default_tags_with_expiration.return_value = ['tags', 'expiration'] with client_with_identity('devtable', client) as cl: params = {'repository': 'devtable/repo'} diff --git a/test/test_api_usage.py b/test/test_api_usage.py index 75fed8888..de1c7e081 100644 --- a/test/test_api_usage.py +++ b/test/test_api_usage.py @@ -2431,12 +2431,12 @@ class TestGetRepository(ApiTestCase): self.login(ADMIN_ACCESS_USER) # base + repo + is_starred + tags - with assert_query_count(BASE_LOGGEDIN_QUERY_COUNT + 4): + with assert_query_count(BASE_LOGGEDIN_QUERY_COUNT + 5): self.getJsonResponse(Repository, params=dict(repository=ADMIN_ACCESS_USER + '/simple')) # base + repo + is_starred + tags - with assert_query_count(BASE_LOGGEDIN_QUERY_COUNT + 4): + with assert_query_count(BASE_LOGGEDIN_QUERY_COUNT + 5): json = self.getJsonResponse(Repository, params=dict(repository=ADMIN_ACCESS_USER + '/gargantuan'))