refactor(endpoints/api/repository*): added in pre_oci_model abstraction

this is a part of getting ready for oci stuff

[TESTING->using new PR stack]

Issue: https://coreosdev.atlassian.net/browse/QUAY-633

- [ ] It works!
- [ ] Comments provide sufficient explanations for the next contributor
- [ ] Tests cover changes and corner cases
- [ ] Follows Quay syntax patterns and format
This commit is contained in:
Charlton Austin 2017-07-21 14:04:59 -04:00
parent 94d516a2c8
commit 9e1106f164
6 changed files with 473 additions and 221 deletions

View file

@ -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']) 'service_key_modify', 'service_key_extend', 'service_key_rotate'])
def _validate_logs_arguments(start_time, end_time): def _validate_logs_arguments(start_time, end_time):
if start_time: if start_time:
try: try:

View file

@ -10,12 +10,12 @@ from datetime import timedelta, datetime
from flask import request, abort from flask import request, abort
from app import dockerfile_build_queue, tuf_metadata_api 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, from endpoints.api import (format_date, nickname, log_action, validate_json_request,
require_repo_read, require_repo_write, require_repo_admin, require_repo_read, require_repo_write, require_repo_admin,
RepositoryParamResource, resource, parse_args, ApiResource, RepositoryParamResource, resource, parse_args, ApiResource,
request_error, require_scope, path_param, page_support, request_error, require_scope, path_param, page_support,
query_param, truthy_bool, show_if) 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, from endpoints.exception import (Unauthorized, NotFound, InvalidRequest, ExceedsLicenseException,
DownstreamIssue) DownstreamIssue)
from endpoints.api.billing import lookup_allowed_private_repos, get_namespace_plan 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 auth import scopes
from util.names import REPOSITORY_NAME_REGEX from util.names import REPOSITORY_NAME_REGEX
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
REPOS_PER_PAGE = 100 REPOS_PER_PAGE = 100
MAX_DAYS_IN_3_MONTHS = 92 MAX_DAYS_IN_3_MONTHS = 92
def check_allowed_private_repos(namespace): def check_allowed_private_repos(namespace):
""" Checks to see if the given namespace has reached its private repository limit. If so, """ Checks to see if the given namespace has reached its private repository limit. If so,
raises a ExceedsLicenseException. raises a ExceedsLicenseException.
@ -106,8 +106,7 @@ class RepositoryList(ApiResource):
repository_name = req['repository'] repository_name = req['repository']
visibility = req['visibility'] visibility = req['visibility']
existing = model.repository.get_repository(namespace_name, repository_name) if model.repo_exists(namespace_name, repository_name):
if existing:
raise request_error(message='Repository already exists') raise request_error(message='Repository already exists')
visibility = req['visibility'] visibility = req['visibility']
@ -119,13 +118,11 @@ class RepositoryList(ApiResource):
raise InvalidRequest('Invalid repository name') raise InvalidRequest('Invalid repository name')
kind = req.get('repo_kind', 'image') or 'image' kind = req.get('repo_kind', 'image') or 'image'
repo = model.repository.create_repository(namespace_name, repository_name, owner, visibility, model.create_repo(namespace_name, repository_name, owner, req['description'], visibility=visibility,
repo_kind=kind) repo_kind=kind)
repo.description = req['description']
repo.save()
log_action('create_repo', namespace_name, {'repo': repository_name, log_action('create_repo', namespace_name, {'repo': repository_name,
'namespace': namespace_name}, repo=repo) 'namespace': namespace_name}, repo_name=repository_name)
return { return {
'namespace': namespace_name, 'namespace': namespace_name,
'name': repository_name, 'name': repository_name,
@ -134,7 +131,6 @@ class RepositoryList(ApiResource):
raise Unauthorized() raise Unauthorized()
@require_scope(scopes.READ_REPO) @require_scope(scopes.READ_REPO)
@nickname('listRepos') @nickname('listRepos')
@parse_args() @parse_args()
@ -160,89 +156,18 @@ class RepositoryList(ApiResource):
user = get_authenticated_user() user = get_authenticated_user()
username = user.username if user else None username = user.username if user else None
next_page_token = None last_modified = parsed_args['last_modified']
repos = None popularity = parsed_args['popularity']
repo_kind = parsed_args['repo_kind']
# Lookup the requested repositories (either starred or non-starred.) if parsed_args['starred'] and not username:
if parsed_args['starred']:
if not username:
# No repositories should be returned, as there is no user. # No repositories should be returned, as there is no user.
abort(400) abort(400)
# Return the full list of repos starred by the current user that are still visible to them. repos, next_page_token = model.get_repo_list(
def can_view_repo(repo): parsed_args['starred'], user, parsed_args['repo_kind'], parsed_args['namespace'], username,
return ReadRepositoryPermission(repo.namespace_user.username, repo.name).can() parsed_args['public'], page_token, last_modified, popularity)
unfiltered_repos = model.repository.get_user_starred_repositories(user, kind_filter=repo_kind) return {'repositories': [repo.to_dict() for repo in repos]}, next_page_token
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
@resource('/v1/repository/<apirepopath:repository>') @resource('/v1/repository/<apirepopath:repository>')
@ -273,154 +198,63 @@ class Repository(RepositoryParamResource):
def get(self, namespace, repository, parsed_args): def get(self, namespace, repository, parsed_args):
"""Fetch the specified repository.""" """Fetch the specified repository."""
logger.debug('Get repo: %s/%s' % (namespace, repository)) logger.debug('Get repo: %s/%s' % (namespace, repository))
repo = model.get_repo(namespace, repository, get_authenticated_user())
repo = model.repository.get_repository(namespace, repository)
if repo is None: if repo is None:
raise NotFound() raise NotFound()
can_write = ModifyRepositoryPermission(namespace, repository).can() repo_data = repo.to_dict()
can_admin = AdministerRepositoryPermission(namespace, repository).can() 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 parsed_args['includeStats'] and repo.repository_base_elements.kind_name != 'application':
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']:
stats = [] stats = []
found_dates = {} found_dates = {}
start_date = datetime.now() - timedelta(days=MAX_DAYS_IN_3_MONTHS) for count in repo.counts:
counts = model.log.get_repository_action_counts(repo, start_date) stats.append(count.to_dict())
for count in counts:
stats.append({
'date': count.date.isoformat(),
'count': count.count,
})
found_dates['%s/%s' % (count.date.month, count.date.day)] = True found_dates['%s/%s' % (count.date.month, count.date.day)] = True
# Fill in any missing stats with zeros. # Fill in any missing stats with zeros.
for day in range(1, MAX_DAYS_IN_3_MONTHS): for day in range(1, MAX_DAYS_IN_3_MONTHS):
day_date = datetime.now() - timedelta(days=day) day_date = datetime.now() - timedelta(days=day)
key = '%s/%s' % (day_date.month, day_date.day) key = '%s/%s' % (day_date.month, day_date.day)
if not key in found_dates: if key not in found_dates:
stats.append({ stats.append({
'date': day_date.date().isoformat(), 'date': day_date.date().isoformat(),
'count': 0, '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 repo_data['stats'] = stats
return repo_data return repo_data
@require_repo_write @require_repo_write
@nickname('updateRepo') @nickname('updateRepo')
@validate_json_request('RepoUpdate') @validate_json_request('RepoUpdate')
def put(self, namespace, repository): def put(self, namespace, repository):
""" Update the description in the specified repository. """ """ Update the description in the specified repository. """
repo = model.repository.get_repository(namespace, repository) if not model.repo_exists(namespace, repository):
if repo: raise NotFound()
values = request.get_json() values = request.get_json()
repo.description = values['description'] model.set_description(namespace, repository, values['description'])
repo.save()
log_action('set_repo_description', namespace, log_action('set_repo_description', namespace,
{'repo': repository, 'namespace': namespace, 'description': values['description']}, {'repo': repository, 'namespace': namespace, 'description': values['description']},
repo=repo) repo_name=repository)
return { return {
'success': True 'success': True
} }
raise NotFound()
@require_repo_admin @require_repo_admin
@nickname('deleteRepository') @nickname('deleteRepository')
def delete(self, namespace, repository): def delete(self, namespace, repository):
""" Delete a repository. """ """ Delete a repository. """
model.repository.purge_repository(namespace, repository) username = model.purge_repository(namespace, repository)
user = model.user.get_namespace_user(namespace)
if features.BILLING: if features.BILLING:
plan = get_namespace_plan(namespace) plan = get_namespace_plan(namespace)
check_repository_usage(user, plan) model.check_repository_usage(username, plan)
# Remove any builds from the queue. # Remove any builds from the queue.
dockerfile_build_queue.delete_namespaced_items(namespace, repository) dockerfile_build_queue.delete_namespaced_items(namespace, repository)
@ -459,17 +293,16 @@ class RepositoryVisibility(RepositoryParamResource):
@validate_json_request('ChangeVisibility') @validate_json_request('ChangeVisibility')
def post(self, namespace, repository): def post(self, namespace, repository):
""" Change the visibility of a repository. """ """ Change the visibility of a repository. """
repo = model.repository.get_repository(namespace, repository) if model.repo_exists(namespace, repository):
if repo:
values = request.get_json() values = request.get_json()
visibility = values['visibility'] visibility = values['visibility']
if visibility == 'private': if visibility == 'private':
check_allowed_private_repos(namespace) 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, log_action('change_repo_visibility', namespace,
{'repo': repository, 'namespace': namespace, 'visibility': values['visibility']}, {'repo': repository, 'namespace': namespace, 'visibility': values['visibility']},
repo=repo) repo_name=repository)
return {'success': True} return {'success': True}
@ -499,19 +332,17 @@ class RepositoryTrust(RepositoryParamResource):
@validate_json_request('ChangeRepoTrust') @validate_json_request('ChangeRepoTrust')
def post(self, namespace, repository): def post(self, namespace, repository):
""" Change the visibility of a repository. """ """ Change the visibility of a repository. """
repo = model.repository.get_repository(namespace, repository) if not model.repo_exists(namespace, repository):
if not repo:
raise NotFound() raise NotFound()
tags, _ = tuf_metadata_api.get_default_tags_with_expiration(namespace, repository) tags, _ = tuf_metadata_api.get_default_tags_with_expiration(namespace, repository)
if tags and not tuf_metadata_api.delete_metadata(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() 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, log_action('change_repo_trust', namespace,
{'repo': repository, 'namespace': namespace, 'trust_enabled': values['trust_enabled']}, {'repo': repository, 'namespace': namespace, 'trust_enabled': values['trust_enabled']},
repo=repo) repo_name=repository)
return {'success': True} return {'success': True}

View file

@ -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
"""

View file

@ -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()

View file

@ -38,8 +38,8 @@ NOT_FOUND_RESPONSE = {
]) ])
def test_post_changetrust(trust_enabled, repo_found, expected_body, expected_status, client): 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.tuf_metadata_api') as mock_tuf:
with patch('endpoints.api.repository.model') as mock_model: with patch('endpoints.api.repository_models_pre_oci.model.repository.get_repository') as mock_model:
mock_model.repository.get_repository.return_value = MagicMock() if repo_found else None mock_model.return_value = MagicMock() if repo_found else None
mock_tuf.get_default_tags_with_expiration.return_value = ['tags', 'expiration'] mock_tuf.get_default_tags_with_expiration.return_value = ['tags', 'expiration']
with client_with_identity('devtable', client) as cl: with client_with_identity('devtable', client) as cl:
params = {'repository': 'devtable/repo'} params = {'repository': 'devtable/repo'}

View file

@ -2431,12 +2431,12 @@ class TestGetRepository(ApiTestCase):
self.login(ADMIN_ACCESS_USER) self.login(ADMIN_ACCESS_USER)
# base + repo + is_starred + tags # 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, self.getJsonResponse(Repository,
params=dict(repository=ADMIN_ACCESS_USER + '/simple')) params=dict(repository=ADMIN_ACCESS_USER + '/simple'))
# base + repo + is_starred + tags # 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, json = self.getJsonResponse(Repository,
params=dict(repository=ADMIN_ACCESS_USER + '/gargantuan')) params=dict(repository=ADMIN_ACCESS_USER + '/gargantuan'))