parent
d200d3d6cf
commit
a16ca8f5fd
@ -0,0 +1,22 @@ |
||||
from typing import List |
||||
|
||||
from zope.interface import Interface |
||||
|
||||
|
||||
class IContainerRegistryService(Interface): |
||||
"""Manage Harbor projects and users.""" |
||||
|
||||
def get_accounts() -> List[str]: |
||||
"""Get a list of Harbor account usernames.""" |
||||
|
||||
def create_project_for_user(username: str): |
||||
""" |
||||
Create a new Harbor project for a user add make them a Project Admin. |
||||
The user needs to have logged in to Harbor at least once. |
||||
""" |
||||
|
||||
def delete_project_for_user(username: str): |
||||
""" |
||||
Deletes the Harbor project for the given user, if it exists. |
||||
All repositories in the project will be deleted. |
||||
""" |
@ -0,0 +1,87 @@ |
||||
from typing import List |
||||
|
||||
import requests |
||||
from requests.auth import HTTPBasicAuth |
||||
from zope import component |
||||
from zope.interface import implementer |
||||
|
||||
from ceo_common.errors import UserNotFoundError |
||||
from ceo_common.interfaces import IContainerRegistryService, IConfig |
||||
|
||||
|
||||
@implementer(IContainerRegistryService) |
||||
class ContainerRegistryService: |
||||
def __init__(self): |
||||
cfg = component.getUtility(IConfig) |
||||
self.base_url = cfg.get('registry_base_url') |
||||
if self.base_url.endswith('/'): |
||||
self.base_url = self.base_url[:-1] |
||||
api_username = cfg.get('registry_username') |
||||
api_password = cfg.get('registry_password') |
||||
self.basic_auth = HTTPBasicAuth(api_username, api_password) |
||||
|
||||
def _http_request(self, method: str, path: str, **kwargs): |
||||
return requests.request( |
||||
method, self.base_url + path, **kwargs, auth=self.basic_auth) |
||||
|
||||
def _http_get(self, path: str, **kwargs): |
||||
return self._http_request('GET', path, **kwargs) |
||||
|
||||
def _http_post(self, path, **kwargs): |
||||
return self._http_request('POST', path, **kwargs) |
||||
|
||||
def _http_delete(self, path, **kwargs): |
||||
return self._http_request('DELETE', path, **kwargs) |
||||
|
||||
def _get_account(self, username: str): |
||||
resp = self._http_get('/users', params={'username': username}) |
||||
users = resp.json() |
||||
if len(users) < 1: |
||||
raise UserNotFoundError(username) |
||||
return users[0] |
||||
|
||||
def get_accounts(self) -> List[str]: |
||||
# We're only interested in accounts which have a project, so |
||||
# we're actually just going to get a list of projects |
||||
resp = self._http_get('/projects') |
||||
resp.raise_for_status() |
||||
# This project is only owned by the admin account, so we don't |
||||
# want to include it |
||||
project_exceptions = ['library'] |
||||
return [ |
||||
project['name'] for project in resp.json() |
||||
if project['name'] not in project_exceptions |
||||
] |
||||
|
||||
def create_project_for_user(self, username: str): |
||||
user_id = self._get_account(username)['user_id'] |
||||
|
||||
# Create the project |
||||
resp = self._http_post( |
||||
'/projects', json={'project_name': username, 'public': True}) |
||||
# 409 => project already exists (that is OK) |
||||
if resp.status_code != 409: |
||||
resp.raise_for_status() |
||||
|
||||
# Add the user as a project admin (role ID 1) |
||||
resp = self._http_post( |
||||
f'/projects/{username}/members', |
||||
json={'role_id': 1, 'member_user': {'user_id': user_id}}) |
||||
# 409 => project member already exists (that is OK) |
||||
if resp.status_code != 409: |
||||
resp.raise_for_status() |
||||
|
||||
def delete_project_for_user(self, username: str): |
||||
# Delete all of the repositories inside the project first |
||||
resp = self._http_get(f'/projects/{username}/repositories') |
||||
if resp.status_code == 403: |
||||
# For some reason a 403 is returned if the project doesn't exist |
||||
return |
||||
resp.raise_for_status() |
||||
repositories = [repo['name'] for repo in resp.json()] |
||||
for repo in repositories: |
||||
resp = self._http_delete(f'/projects/{username}/repositories/{repo}') |
||||
resp.raise_for_status() |
||||
# Delete the project now that it is empty |
||||
resp = self._http_delete(f'/projects/{username}') |
||||
resp.raise_for_status() |
@ -0,0 +1,94 @@ |
||||
from aiohttp import web |
||||
|
||||
from .MockHTTPServerBase import MockHTTPServerBase |
||||
|
||||
|
||||
class MockHarborServer(MockHTTPServerBase): |
||||
def __init__(self, port=8002): |
||||
prefix = '/api/v2.0' |
||||
routes = [ |
||||
web.get(prefix + '/users', self.users_get_handler), |
||||
web.post(prefix + '/projects', self.projects_post_handler), |
||||
web.post(prefix + '/projects/{project}/members', self.members_post_handler), |
||||
web.get(prefix + '/projects/{project}/repositories', self.repositories_get_handler), |
||||
web.delete(prefix + '/projects/{project}/repositories/{repository}', self.repositories_delete_handler), |
||||
web.delete(prefix + '/projects/{project}', self.projects_delete_handler), |
||||
# for debugging purposes |
||||
web.post('/reset', self.reset_handler), |
||||
web.post('/users/{username}', self.users_post_handler), |
||||
] |
||||
super().__init__(port, routes) |
||||
|
||||
self.users = ['ctdalek', 'regular1', 'exec1'] |
||||
self.projects = { |
||||
'ctdalek': ['repo1', 'repo2'], |
||||
'regular1': [], |
||||
'exec1': [], |
||||
} |
||||
|
||||
async def projects_delete_handler(self, request): |
||||
project_name = request.match_info['project'] |
||||
if project_name not in self.projects: |
||||
return web.json_response({"errors": [{ |
||||
"code": "FORBIDDEN", "message": "forbidden" |
||||
}]}, status=403) |
||||
del self.projects[project_name] |
||||
return web.Response(text='', status=200) |
||||
|
||||
async def repositories_delete_handler(self, request): |
||||
project_name = request.match_info['project'] |
||||
repository_name = request.match_info['repository'] |
||||
self.projects[project_name].remove(repository_name) |
||||
return web.Response(text='', status=200) |
||||
|
||||
async def repositories_get_handler(self, request): |
||||
project_name = request.match_info['project'] |
||||
if project_name not in self.projects: |
||||
return web.json_response({"errors": [{ |
||||
"code": "FORBIDDEN", "message": "forbidden" |
||||
}]}, status=403) |
||||
projects = self.projects[project_name] |
||||
return web.json_response([ |
||||
{'id': i, 'name': name} for i, name in enumerate(projects) |
||||
]) |
||||
|
||||
async def users_get_handler(self, request): |
||||
username = request.query['username'] |
||||
if username not in self.users: |
||||
return web.json_response([]) |
||||
return web.json_response([{ |
||||
'username': username, |
||||
'realname': username, |
||||
'user_id': self.users.index(username), |
||||
'email': username + '@csclub.internal', |
||||
}]) |
||||
|
||||
async def members_post_handler(self, request): |
||||
await request.json() |
||||
return web.Response(text='', status=201) |
||||
|
||||
async def projects_post_handler(self, request): |
||||
body = await request.json() |
||||
project_name = body['project_name'] |
||||
if project_name in self.projects: |
||||
return web.json_response({'errors': [{ |
||||
"code": "CONFLICT", |
||||
"message": f"The project named {project_name} already exists", |
||||
}]}, status=409) |
||||
self.projects[project_name] = ['repo1', 'repo2'] |
||||
return web.Response(text='', status=201) |
||||
|
||||
async def users_post_handler(self, request): |
||||
username = request.match_info['username'] |
||||
self.users.remove(username) |
||||
return web.Response(text='OK\n', status=201) |
||||
|
||||
async def reset_handler(self, request): |
||||
self.users.clear() |
||||
self.projects.clear() |
||||
return web.Response(text='OK\n') |
||||
|
||||
|
||||
if __name__ == '__main__': |
||||
server = MockHarborServer() |
||||
server.start() |
Loading…
Reference in new issue