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:
Joseph Schorr 2017-03-23 17:16:19 -04:00
parent 3dd6e6919d
commit f9e6110f73
47 changed files with 1009 additions and 106 deletions

View file

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