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:
parent
94d516a2c8
commit
9e1106f164
6 changed files with 473 additions and 221 deletions
|
@ -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:
|
||||
|
|
|
@ -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,13 +118,11 @@ 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,
|
||||
model.create_repo(namespace_name, repository_name, owner, req['description'], visibility=visibility,
|
||||
repo_kind=kind)
|
||||
repo.description = req['description']
|
||||
repo.save()
|
||||
|
||||
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,
|
||||
|
@ -134,7 +131,6 @@ class RepositoryList(ApiResource):
|
|||
|
||||
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:
|
||||
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/<apirepopath: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:
|
||||
if not model.repo_exists(namespace, repository):
|
||||
raise NotFound()
|
||||
|
||||
values = request.get_json()
|
||||
repo.description = values['description']
|
||||
repo.save()
|
||||
model.set_description(namespace, repository, values['description'])
|
||||
|
||||
log_action('set_repo_description', namespace,
|
||||
{'repo': repository, 'namespace': namespace, 'description': values['description']},
|
||||
repo=repo)
|
||||
repo_name=repository)
|
||||
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'})
|
||||
|
||||
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}
|
||||
|
|
252
endpoints/api/repository_models_interface.py
Normal file
252
endpoints/api/repository_models_interface.py
Normal 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
|
||||
"""
|
167
endpoints/api/repository_models_pre_oci.py
Normal file
167
endpoints/api/repository_models_pre_oci.py
Normal 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()
|
|
@ -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'}
|
||||
|
|
|
@ -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'))
|
||||
|
||||
|
|
Reference in a new issue