From 3d4ece31f3da7a39cc6c0e22b8dc2216fdda354b Mon Sep 17 00:00:00 2001 From: jakedt Date: Fri, 14 Mar 2014 13:06:58 -0400 Subject: [PATCH] Port over images, permissions, and tags. --- endpoints/api/__init__.py | 3 + endpoints/api/image.py | 90 ++++++++++++++ endpoints/api/legacy.py | 13 ++ endpoints/api/permission.py | 235 ++++++++++++++++++++++++++++++++++++ endpoints/api/tag.py | 49 ++++++++ 5 files changed, 390 insertions(+) create mode 100644 endpoints/api/image.py create mode 100644 endpoints/api/permission.py create mode 100644 endpoints/api/tag.py diff --git a/endpoints/api/__init__.py b/endpoints/api/__init__.py index 225ca31ac..be56f2d6a 100644 --- a/endpoints/api/__init__.py +++ b/endpoints/api/__init__.py @@ -180,3 +180,6 @@ import endpoints.api.search import endpoints.api.build import endpoints.api.webhook import endpoints.api.trigger +import endpoints.api.image +import endpoints.api.tag +import endpoints.api.permission \ No newline at end of file diff --git a/endpoints/api/image.py b/endpoints/api/image.py new file mode 100644 index 000000000..700891121 --- /dev/null +++ b/endpoints/api/image.py @@ -0,0 +1,90 @@ +import json + +from collections import defaultdict +from flask.ext.restful import abort + +from app import app +from endpoints.api import resource, nickname, require_repo_read, RepositoryParamResource +from data import model +from util.cache import cache_control + + +store = app.config['STORAGE'] + + +def image_view(image): + extended_props = image + if image.storage and image.storage.id: + extended_props = image.storage + + command = extended_props.command + return { + 'id': image.docker_image_id, + 'created': extended_props.created, + 'comment': extended_props.comment, + 'command': json.loads(command) if command else None, + 'ancestors': image.ancestors, + 'dbid': image.id, + 'size': extended_props.image_size, + } + + +@resource('/v1/repository//image/') +class RepositoryImageList(RepositoryParamResource): + """ Resource for listing repository images. """ + @require_repo_read + @nickname('listRepositoryImages') + def get(self, namespace, repository): + """ List the images for the specified repository. """ + all_images = model.get_repository_images(namespace, repository) + all_tags = model.list_repository_tags(namespace, repository) + + tags_by_image_id = defaultdict(list) + for tag in all_tags: + tags_by_image_id[tag.image.docker_image_id].append(tag.name) + + + def add_tags(image_json): + image_json['tags'] = tags_by_image_id[image_json['id']] + return image_json + + + return { + 'images': [add_tags(image_view(image)) for image in all_images] + } + + +@resource('/v1/repository//image/') +class RepositoryImage(RepositoryParamResource): + """ Resource for handling repository images. """ + @require_repo_read + @nickname('getImage') + def get(self, namespace, repository, image_id): + image = model.get_repo_image(namespace, repository, image_id) + if not image: + abort(404) + + return image_view(image) + + +@resource('/v1/repository//image//changes') +class RepositoryImageChanges(RepositoryParamResource): + """ Resource for handling repository image change lists. """ + + @cache_control(max_age=60*60) # Cache for one hour + @require_repo_read + @nickname('getImageChanges') + def get(self, namespace, repository, image_id): + image = model.get_repo_image(namespace, repository, image_id) + + if not image: + abort(404) + + uuid = image.storage and image.storage.uuid + diffs_path = store.image_file_diffs_path(namespace, repository, image_id, uuid) + + try: + response_json = store.get_content(diffs_path) + return response_json + except IOError: + abort(404) diff --git a/endpoints/api/legacy.py b/endpoints/api/legacy.py index 73b32c9a1..d287271e6 100644 --- a/endpoints/api/legacy.py +++ b/endpoints/api/legacy.py @@ -1635,6 +1635,7 @@ def wrap_role_view_org(role_json, user, org_members): return role_json +# Ported @api_bp.route('/repository//image/', methods=['GET']) @parse_repository_name def list_repository_images(namespace, repository): @@ -1660,6 +1661,7 @@ def list_repository_images(namespace, repository): abort(403) +# Ported @api_bp.route('/repository//image/', methods=['GET']) @parse_repository_name @@ -1674,6 +1676,7 @@ def get_image(namespace, repository, image_id): abort(403) +# Ported @api_bp.route('/repository//image//changes', methods=['GET']) @cache_control(max_age=60*60) # Cache for one hour @@ -1699,6 +1702,7 @@ def get_image_changes(namespace, repository, image_id): abort(403) +# Ported @api_bp.route('/repository//tag/', methods=['DELETE']) @parse_repository_name @@ -1718,6 +1722,7 @@ def delete_full_tag(namespace, repository, tag): abort(403) # Permission denied +# Ported @api_bp.route('/repository//tag//images', methods=['GET']) @parse_repository_name @@ -1742,6 +1747,7 @@ def list_tag_images(namespace, repository, tag): abort(403) # Permission denied +# Ported @api_bp.route('/repository//permissions/team/', methods=['GET']) @api_login_required @@ -1759,6 +1765,7 @@ def list_repo_team_permissions(namespace, repository): abort(403) # Permission denied +# Ported @api_bp.route('/repository//permissions/user/', methods=['GET']) @api_login_required @@ -1800,6 +1807,7 @@ def list_repo_user_permissions(namespace, repository): abort(403) # Permission denied +# Ported @api_bp.route('/repository//permissions/user/', methods=['GET']) @api_login_required @@ -1825,6 +1833,7 @@ def get_user_permissions(namespace, repository, username): abort(403) # Permission denied +# Ported @api_bp.route('/repository//permissions/team/', methods=['GET']) @api_login_required @@ -1840,6 +1849,7 @@ def get_team_permissions(namespace, repository, teamname): abort(403) # Permission denied +# Ported @api_bp.route('/repository//permissions/user/', methods=['PUT', 'POST']) @api_login_required @@ -1879,6 +1889,7 @@ def change_user_permissions(namespace, repository, username): abort(403) # Permission denied +# Ported @api_bp.route('/repository//permissions/team/', methods=['PUT', 'POST']) @api_login_required @@ -1907,6 +1918,7 @@ def change_team_permissions(namespace, repository, teamname): abort(403) # Permission denied +# Ported @api_bp.route('/repository//permissions/user/', methods=['DELETE']) @api_login_required @@ -1928,6 +1940,7 @@ def delete_user_permissions(namespace, repository, username): abort(403) # Permission denied +# Ported @api_bp.route('/repository//permissions/team/', methods=['DELETE']) @api_login_required diff --git a/endpoints/api/permission.py b/endpoints/api/permission.py new file mode 100644 index 000000000..8db4baae3 --- /dev/null +++ b/endpoints/api/permission.py @@ -0,0 +1,235 @@ +import logging + +from flask import request + +from endpoints.api import (resource, nickname, require_repo_admin, RepositoryParamResource, + log_action, request_error, validate_json_request) +from data import model + + +logger = logging.getLogger(__name__) + + +def role_view(repo_perm_obj): + return { + 'role': repo_perm_obj.role.name, + } + +def wrap_role_view_user(role_json, user): + role_json['is_robot'] = user.robot + return role_json + + +def wrap_role_view_org(role_json, user, org_members): + role_json['is_org_member'] = user.robot or user.username in org_members + return role_json + + +@resource('/v1/repository//permissions/team/') +class RepositoryTeamPermissionList(RepositoryParamResource): + """ Resource for repository team permissions. """ + @require_repo_admin + @nickname('listRepoTeamPermissions') + def get(self, namespace, repository): + """ List all team permission. """ + repo_perms = model.get_all_repo_teams(namespace, repository) + + return { + 'permissions': {repo_perm.team.name: role_view(repo_perm) + for repo_perm in repo_perms} + } + + +@resource('/v1/repository//permissions/user/') +class RepositoryUserPermissionList(RepositoryParamResource): + """ Resource for repository user permissions. """ + @require_repo_admin + @nickname('listRepoUserPermissions') + def get(self, namespace, repository): + """ List all user permissions. """ + # Lookup the organization (if any). + org = None + try: + org = model.get_organization(namespace) # Will raise an error if not org + except model.InvalidOrganizationException: + # This repository isn't under an org + pass + + # Determine how to wrap the role(s). + def wrapped_role_view(repo_perm): + return wrap_role_view_user(role_view(repo_perm), repo_perm.user) + + role_view_func = wrapped_role_view + + if org: + org_members = model.get_organization_member_set(namespace) + current_func = role_view_func + + def wrapped_role_org_view(repo_perm): + return wrap_role_view_org(current_func(repo_perm), repo_perm.user, + org_members) + + role_view_func = wrapped_role_org_view + + # Load and return the permissions. + repo_perms = model.get_all_repo_users(namespace, repository) + return { + 'permissions': {perm.user.username: role_view_func(perm) + for perm in repo_perms} + } + + +@resource('/v1/repository//permissions/user/', + methods=['GET']) +class RepositoryUserPermission(RepositoryParamResource): + """ Resource for managing individual user permissions. """ + schemas = { + 'UserPermission': { + 'id': 'UserPermission', + 'type': 'object', + 'description': 'Description of a user permission.', + 'required': True, + 'properties': { + 'role': { + 'type': 'string', + 'description': 'Visibility which the repository will start with', + 'enum': [ + 'read', + 'write', + 'admin', + ], + 'required': True, + }, + }, + }, + } + + @require_repo_admin + @nickname('getUserPermission') + def get(self, namespace, repository, username): + logger.debug('Get repo: %s/%s permissions for user %s' % + (namespace, repository, username)) + perm = model.get_user_reponame_permission(username, namespace, repository) + perm_view = wrap_role_view_user(role_view(perm), perm.user) + + try: + model.get_organization(namespace) + org_members = model.get_organization_member_set(namespace) + perm_view = wrap_role_view_org(perm_view, perm.user, org_members) + except model.InvalidOrganizationException: + # This repository is not part of an organization + pass + + return perm_view + + @require_repo_admin + @nickname('changeUserPermissions') + @validate_json_request('UserPermission') + def put(self, namespace, repository, username): # Also needs to respond to post + """ Update the perimssions for an existing repository. """ + new_permission = request.get_json() + + logger.debug('Setting permission to: %s for user %s' % + (new_permission['role'], username)) + + perm = model.set_user_repo_permission(username, namespace, repository, + new_permission['role']) + perm_view = wrap_role_view_user(role_view(perm), perm.user) + + try: + model.get_organization(namespace) + org_members = model.get_organization_member_set(namespace) + perm_view = wrap_role_view_org(perm_view, perm.user, org_members) + except model.InvalidOrganizationException: + # This repository is not part of an organization + pass + except model.DataModelException as ex: + return request_error(exception=ex) + + log_action('change_repo_permission', namespace, + {'username': username, 'repo': repository, + 'role': new_permission['role']}, + repo=model.get_repository(namespace, repository)) + + return perm_view, 200 # 201 for post + + @require_repo_admin + @nickname('deleteUserPermissions') + def delete(namespace, repository, username): + """ Delete the permission for the user. """ + try: + model.delete_user_permission(username, namespace, repository) + except model.DataModelException as ex: + return request_error(exception=ex) + + log_action('delete_repo_permission', namespace, + {'username': username, 'repo': repository}, + repo=model.get_repository(namespace, repository)) + + return 'Deleted', 204 + + +@resource('/v1/repository//permissions/team/') +class RepositoryTeamPermission(RepositoryParamResource): + """ Resource for managing individual team permissions. """ + schemas = { + 'TeamPermission': { + 'id': 'TeamPermission', + 'type': 'object', + 'description': 'Description of a user permission.', + 'required': True, + 'properties': { + 'role': { + 'type': 'string', + 'description': 'Visibility which the repository will start with', + 'enum': [ + 'read', + 'write', + 'admin', + ], + 'required': True, + }, + }, + }, + } + + @require_repo_admin + @nickname('getTeamPermissions') + def get(self, namespace, repository, teamname): + """ Fetch the permission for the specified team. """ + logger.debug('Get repo: %s/%s permissions for team %s' % + (namespace, repository, teamname)) + perm = model.get_team_reponame_permission(teamname, namespace, repository) + return role_view(perm) + + @require_repo_admin + @nickname('changeTeamPermissions') + @validate_json_request('TeamPermission') + def put(self, namespace, repository, teamname): + """ Update the existing team permission. """ + new_permission = request.get_json() + + logger.debug('Setting permission to: %s for team %s' % + (new_permission['role'], teamname)) + + perm = model.set_team_repo_permission(teamname, namespace, repository, + new_permission['role']) + + log_action('change_repo_permission', namespace, + {'team': teamname, 'repo': repository, + 'role': new_permission['role']}, + repo=model.get_repository(namespace, repository)) + + return role_view(perm), 200 # Should be 201 for post + + @require_repo_admin + @nickname('deleteTeamPermissions') + def delete(self, namespace, repository, teamname): + """ Delete the permission for the specified team. """ + model.delete_team_permission(teamname, namespace, repository) + + log_action('delete_repo_permission', namespace, + {'team': teamname, 'repo': repository}, + repo=model.get_repository(namespace, repository)) + + return 'Deleted', 204 diff --git a/endpoints/api/tag.py b/endpoints/api/tag.py new file mode 100644 index 000000000..a175458ff --- /dev/null +++ b/endpoints/api/tag.py @@ -0,0 +1,49 @@ +from flask.ext.restful import abort + +from endpoints.api import (resource, nickname, require_repo_read, require_repo_admin, + RepositoryParamResource, log_action) +from endpoints.api.image import image_view +from data import model +from auth.auth_context import get_authenticated_user + + +@resource('/v1/repository//tag/') +class RepositoryTag(RepositoryParamResource): + """ Resource for managing repository tags. """ + + @require_repo_admin + @nickname('deleteFullTag') + def delete(self, namespace, repository, tag): + """ Delete the specified repository tag. """ + model.delete_tag(namespace, repository, tag) + model.garbage_collect_repository(namespace, repository) + + username = get_authenticated_user().username + log_action('delete_tag', namespace, + {'username': username, 'repo': repository, 'tag': tag}, + repo=model.get_repository(namespace, repository)) + + return 'Deleted', 204 + + +@resource('/v1/repository//tag//images') +class RepositoryTagImages(RepositoryParamResource): + """ Resource for listing the images in a specific repository tag. """ + @require_repo_read + @nickname('listTagImages') + def get(self, namespace, repository, tag): + """ List the images for the specified repository tag. """ + try: + tag_image = model.get_tag_image(namespace, repository, tag) + except model.DataModelException: + abort(404) + + parent_images = model.get_parent_images(tag_image) + + parents = list(parent_images) + parents.reverse() + all_images = [tag_image] + parents + + return { + 'images': [image_view(image) for image in all_images] + }