Add basic user interface for application repos
Adds support for creating app repos, viewing app repos and seeing the list of app repos in the Quay UI.
This commit is contained in:
parent
3dd6e6919d
commit
f9e6110f73
47 changed files with 1009 additions and 106 deletions
|
@ -4,12 +4,13 @@ import logging
|
|||
import datetime
|
||||
import features
|
||||
|
||||
from collections import defaultdict
|
||||
from datetime import timedelta, datetime
|
||||
|
||||
from flask import request, abort
|
||||
|
||||
from app import dockerfile_build_queue
|
||||
from data import model
|
||||
from data import model, oci_model
|
||||
from endpoints.api import (truthy_bool, format_date, nickname, log_action, validate_json_request,
|
||||
require_repo_read, require_repo_write, require_repo_admin,
|
||||
RepositoryParamResource, resource, query_param, parse_args, ApiResource,
|
||||
|
@ -77,10 +78,10 @@ class RepositoryList(ApiResource):
|
|||
'type': 'string',
|
||||
'description': 'Markdown encoded description for the repository',
|
||||
},
|
||||
'kind': {
|
||||
'type': 'string',
|
||||
'repo_kind': {
|
||||
'type': ['string', 'null'],
|
||||
'description': 'The kind of repository',
|
||||
'enum': ['image', 'application'],
|
||||
'enum': ['image', 'application', None],
|
||||
}
|
||||
},
|
||||
},
|
||||
|
@ -116,7 +117,7 @@ class RepositoryList(ApiResource):
|
|||
if not REPOSITORY_NAME_REGEX.match(repository_name):
|
||||
raise InvalidRequest('Invalid repository name')
|
||||
|
||||
kind = req.get('kind', 'image')
|
||||
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']
|
||||
|
@ -126,7 +127,8 @@ class RepositoryList(ApiResource):
|
|||
'namespace': namespace_name}, repo=repo)
|
||||
return {
|
||||
'namespace': namespace_name,
|
||||
'name': repository_name
|
||||
'name': repository_name,
|
||||
'kind': kind,
|
||||
}, 201
|
||||
|
||||
raise Unauthorized()
|
||||
|
@ -144,6 +146,7 @@ class RepositoryList(ApiResource):
|
|||
type=truthy_bool, default=False)
|
||||
@query_param('popularity', 'Whether to include the repository\'s popularity metric.',
|
||||
type=truthy_bool, default=False)
|
||||
@query_param('repo_kind', 'The kind of repositories to return', type=str, default='image')
|
||||
@page_support()
|
||||
def get(self, page_token, parsed_args):
|
||||
""" Fetch the list of repositories visible to the current user under a variety of situations.
|
||||
|
@ -158,6 +161,7 @@ class RepositoryList(ApiResource):
|
|||
username = user.username if user else None
|
||||
next_page_token = None
|
||||
repos = None
|
||||
repo_kind = parsed_args['repo_kind']
|
||||
|
||||
# Lookup the requested repositories (either starred or non-starred.)
|
||||
if parsed_args['starred']:
|
||||
|
@ -169,14 +173,15 @@ class RepositoryList(ApiResource):
|
|||
def can_view_repo(repo):
|
||||
return ReadRepositoryPermission(repo.namespace_user.username, repo.name).can()
|
||||
|
||||
unfiltered_repos = model.repository.get_user_starred_repositories(user)
|
||||
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']))
|
||||
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
|
||||
|
@ -188,7 +193,8 @@ class RepositoryList(ApiResource):
|
|||
repo_query = model.repository.get_visible_repositories(username=username,
|
||||
include_public=parsed_args['public'],
|
||||
start_id=start_id,
|
||||
limit=REPOS_PER_PAGE+1)
|
||||
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')
|
||||
|
@ -217,6 +223,7 @@ class RepositoryList(ApiResource):
|
|||
'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
|
||||
|
@ -266,6 +273,55 @@ class Repository(RepositoryParamResource):
|
|||
"""Fetch the specified repository."""
|
||||
logger.debug('Get repo: %s/%s' % (namespace, repository))
|
||||
|
||||
repo = model.repository.get_repository(namespace, repository)
|
||||
if repo is None:
|
||||
raise NotFound()
|
||||
|
||||
can_write = ModifyRepositoryPermission(namespace, repository).can()
|
||||
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, manifest):
|
||||
tag_info = {
|
||||
'name': tag.name,
|
||||
|
@ -282,63 +338,54 @@ class Repository(RepositoryParamResource):
|
|||
|
||||
return tag_info
|
||||
|
||||
repo = model.repository.get_repository(namespace, repository)
|
||||
stats = None
|
||||
if repo:
|
||||
tags = model.tag.list_repository_tags(namespace, repository, include_storage=True)
|
||||
manifests = model.tag.get_tag_manifests(tags)
|
||||
tags = model.tag.list_repository_tags(namespace, repository, include_storage=True)
|
||||
manifests = model.tag.get_tag_manifests(tags)
|
||||
|
||||
tag_dict = {tag.name: tag_view(tag, manifests.get(tag.id)) for tag in tags}
|
||||
can_write = ModifyRepositoryPermission(namespace, repository).can()
|
||||
can_admin = AdministerRepositoryPermission(namespace, repository).can()
|
||||
tag_dict = {tag.name: tag_view(tag, manifests.get(tag.id)) for tag in tags}
|
||||
if parsed_args['includeStats']:
|
||||
stats = []
|
||||
found_dates = {}
|
||||
|
||||
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)
|
||||
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,
|
||||
})
|
||||
|
||||
if parsed_args['includeStats']:
|
||||
stats = []
|
||||
found_dates = {}
|
||||
found_dates['%s/%s' % (count.date.month, count.date.day)] = True
|
||||
|
||||
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:
|
||||
# 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:
|
||||
stats.append({
|
||||
'date': count.date.isoformat(),
|
||||
'count': count.count,
|
||||
'date': day_date.date().isoformat(),
|
||||
'count': 0,
|
||||
})
|
||||
|
||||
found_dates['%s/%s' % (count.date.month, count.date.day)] = True
|
||||
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 '',
|
||||
}
|
||||
|
||||
# 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:
|
||||
stats.append({
|
||||
'date': day_date.date().isoformat(),
|
||||
'count': 0,
|
||||
})
|
||||
if stats is not None:
|
||||
repo_data['stats'] = stats
|
||||
|
||||
repo_data = {
|
||||
'namespace': namespace,
|
||||
'name': repository,
|
||||
'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 '',
|
||||
}
|
||||
return repo_data
|
||||
|
||||
if stats is not None:
|
||||
repo_data['stats'] = stats
|
||||
|
||||
return repo_data
|
||||
|
||||
raise NotFound()
|
||||
|
||||
@require_repo_write
|
||||
@nickname('updateRepo')
|
||||
|
@ -361,7 +408,6 @@ class Repository(RepositoryParamResource):
|
|||
|
||||
@require_repo_admin
|
||||
@nickname('deleteRepository')
|
||||
@disallow_for_app_repositories
|
||||
def delete(self, namespace, repository):
|
||||
""" Delete a repository. """
|
||||
model.repository.purge_repository(namespace, repository)
|
||||
|
|
Reference in a new issue