Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion api_app/api/routes/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from api.helpers import get_repository
from db.repositories.workspaces import WorkspaceRepository
from api.routes import health, ping, workspaces, workspace_templates, workspace_service_templates, user_resource_templates, \
shared_services, shared_service_templates, migrations, costs, airlock, operations, metadata, requests, workspace_users
shared_services, shared_service_templates, migrations, costs, airlock, operations, metadata, requests, workspace_users, templates
from core import config
from resources import strings

Expand Down Expand Up @@ -42,6 +42,7 @@
core_router.include_router(workspace_service_templates.workspace_service_templates_core_router, tags=["workspace service templates"])
core_router.include_router(user_resource_templates.user_resource_templates_core_router, tags=["user resource templates"])
core_router.include_router(shared_service_templates.shared_service_templates_core_router, tags=["shared service templates"])
core_router.include_router(templates.templates_admin_router, tags=["templates"])
core_router.include_router(shared_services.shared_services_router, tags=["shared services"])
core_router.include_router(operations.operations_router, tags=["operations"])
core_router.include_router(workspaces.workspaces_core_router, tags=["workspaces"])
Expand Down
11 changes: 11 additions & 0 deletions api_app/api/routes/shared_service_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,14 @@ async def register_shared_service_template(template_input: SharedServiceTemplate
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=strings.SHARED_SERVICE_TEMPLATE_VERSION_EXISTS)
except InvalidInput as e:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(e))


@shared_service_templates_core_router.delete("/shared-service-templates/{shared_service_template_name}", status_code=status.HTTP_204_NO_CONTENT, name=strings.API_DELETE_SHARED_SERVICE_TEMPLATE)
async def delete_shared_service_template(shared_service_template_name: str, version: Optional[str] = None, template_repo=Depends(get_repository(ResourceTemplateRepository))):
try:
if version:
await template_repo.delete_template_by_version(shared_service_template_name, version, ResourceType.SharedService)
else:
await template_repo.delete_template(shared_service_template_name, ResourceType.SharedService)
except EntityDoesNotExist:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=strings.SHARED_SERVICE_TEMPLATE_DOES_NOT_EXIST)
16 changes: 16 additions & 0 deletions api_app/api/routes/templates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from fastapi import APIRouter, Depends

from api.helpers import get_repository
from db.repositories.resource_templates import ResourceTemplateRepository
from models.schemas.resource_template import ResourceTemplateInformationInList
from resources import strings
from services.authentication import get_current_admin_user


templates_admin_router = APIRouter(dependencies=[Depends(get_current_admin_user)])


@templates_admin_router.get("/templates", response_model=ResourceTemplateInformationInList, name=strings.API_GET_TEMPLATES)
async def get_templates(template_repo=Depends(get_repository(ResourceTemplateRepository))) -> ResourceTemplateInformationInList:
templates_infos = await template_repo.get_all_templates_information()
return ResourceTemplateInformationInList(templates=templates_infos)
11 changes: 11 additions & 0 deletions api_app/api/routes/user_resource_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,14 @@ async def register_user_resource_template(template_input: UserResourceTemplateIn
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=strings.WORKSPACE_TEMPLATE_VERSION_EXISTS)
except InvalidInput as e:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(e))


@user_resource_templates_core_router.delete("/workspace-service-templates/{service_template_name}/user-resource-templates/{user_resource_template_name}", status_code=status.HTTP_204_NO_CONTENT, name=strings.API_DELETE_USER_RESOURCE_TEMPLATE)
async def delete_user_resource_template(service_template_name: str, user_resource_template_name: str, version: Optional[str] = None, template_repo=Depends(get_repository(ResourceTemplateRepository))):
try:
if version:
await template_repo.delete_template_by_version(user_resource_template_name, version, ResourceType.UserResource, parent_service_name=service_template_name)
else:
await template_repo.delete_template(user_resource_template_name, ResourceType.UserResource, parent_service_name=service_template_name)
except EntityDoesNotExist:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=strings.USER_RESOURCE_TEMPLATE_DOES_NOT_EXIST)
11 changes: 11 additions & 0 deletions api_app/api/routes/workspace_service_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,14 @@ async def register_workspace_service_template(template_input: WorkspaceServiceTe
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=strings.WORKSPACE_TEMPLATE_VERSION_EXISTS)
except InvalidInput as e:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(e))


@workspace_service_templates_core_router.delete("/workspace-service-templates/{service_template_name}", status_code=status.HTTP_204_NO_CONTENT, name=strings.API_DELETE_WORKSPACE_SERVICE_TEMPLATE)
async def delete_workspace_service_template(service_template_name: str, version: Optional[str] = None, template_repo=Depends(get_repository(ResourceTemplateRepository))):
try:
if version:
await template_repo.delete_template_by_version(service_template_name, version, ResourceType.WorkspaceService)
else:
await template_repo.delete_template(service_template_name, ResourceType.WorkspaceService)
except EntityDoesNotExist:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=strings.WORKSPACE_SERVICE_TEMPLATE_DOES_NOT_EXIST)
11 changes: 11 additions & 0 deletions api_app/api/routes/workspace_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,14 @@ async def register_workspace_template(template_input: WorkspaceTemplateInCreate,
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=strings.WORKSPACE_TEMPLATE_VERSION_EXISTS)
except InvalidInput as e:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, detail=str(e))


@workspace_templates_admin_router.delete("/workspace-templates/{workspace_template_name}", status_code=status.HTTP_204_NO_CONTENT, name=strings.API_DELETE_WORKSPACE_TEMPLATE)
async def delete_workspace_template(workspace_template_name: str, version: Optional[str] = None, template_repo=Depends(get_repository(ResourceTemplateRepository))):
try:
if version:
await template_repo.delete_template_by_version(workspace_template_name, version, ResourceType.Workspace)
else:
await template_repo.delete_template(workspace_template_name, ResourceType.Workspace)
except EntityDoesNotExist:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=strings.WORKSPACE_TEMPLATE_DOES_NOT_EXIST)
24 changes: 23 additions & 1 deletion api_app/db/repositories/resource_templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ async def get_templates_information(self, resource_type: ResourceType, user_role
:param user_roles: If set, only return templates that the user is authorized to use.
template.authorizedRoles should contain at least one of user_roles
"""
query = 'SELECT c.name, c.title, c.description, c.authorizedRoles FROM c WHERE c.resourceType = @resourceType AND c.current = true'
query = 'SELECT c.name, c.title, c.description, c.authorizedRoles, c.version, c.resourceType, c.parentWorkspaceService FROM c WHERE c.resourceType = @resourceType AND c.current = true'
parameters = [
{'name': '@resourceType', 'value': resource_type}
]
Expand All @@ -62,6 +62,15 @@ async def get_templates_information(self, resource_type: ResourceType, user_role
# User can view template if they have at least one of authorizedRoles
return [t for t in templates if not t.authorizedRoles or len(set(t.authorizedRoles).intersection(set(user_roles))) > 0]

async def get_all_templates_information(self) -> List[ResourceTemplateInformation]:
"""
Returns name/title/description for all templates of all types
"""
query = 'SELECT c.name, c.title, c.description, c.authorizedRoles, c.version, c.resourceType, c.parentWorkspaceService FROM c'
template_infos = await self.query(query=query)
templates = [parse_obj_as(ResourceTemplateInformation, info) for info in template_infos]
return templates

async def get_current_template(self, template_name: str, resource_type: ResourceType, parent_service_name: str = "") -> Union[ResourceTemplate, UserResourceTemplate]:
"""
Returns full template for the current version of the 'template_name' template
Expand Down Expand Up @@ -192,3 +201,16 @@ def _validate_pipeline_has_unique_step_ids(self, pipeline):

if step_id != "main":
step_ids.append(step_id)

async def delete_template(self, template_name: str, resource_type: ResourceType, parent_service_name: Optional[str] = None) -> None:
query, parameters = self._template_by_name_query(template_name, resource_type)
if resource_type == ResourceType.UserResource:
query += ' AND c.parentWorkspaceService = @parentWorkspaceService'
parameters.append({'name': '@parentWorkspaceService', 'value': parent_service_name})
templates = await self.query(query=query, parameters=parameters)
for template in templates:
await self.delete_item(template["id"])

async def delete_template_by_version(self, template_name: str, version: str, resource_type: ResourceType, parent_service_name: Optional[str] = None) -> None:
template = await self.get_template_by_name_and_version(template_name, version, resource_type, parent_service_name)
await self.delete_item(template.id)
6 changes: 6 additions & 0 deletions api_app/models/schemas/resource_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,17 @@ class ResourceTemplateInResponse(ResourceTemplate):
system_properties: Dict[str, Property] = Field(title="System properties")


from models.domain.resource import ResourceType


class ResourceTemplateInformation(BaseModel):
name: str = Field(title="Template name")
title: str = Field(title="Template title", default="")
description: str = Field(title="Template description", default="")
authorizedRoles: Optional[List[str]] = Field(title="If not empty, the user is required to have one of these roles to install the template", default=[])
version: str
resourceType: ResourceType
parentWorkspaceService: Optional[str] = None


class ResourceTemplateInformationInList(BaseModel):
Expand Down
6 changes: 6 additions & 0 deletions api_app/resources/strings.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,18 @@
API_CREATE_WORKSPACE_TEMPLATES = "Register workspace template"
API_GET_WORKSPACE_TEMPLATES = "Get workspace templates"
API_GET_WORKSPACE_TEMPLATE_BY_NAME = "Get workspace template by name and optional version"
API_DELETE_WORKSPACE_TEMPLATE = "Delete workspace template"

API_CREATE_WORKSPACE_SERVICE_TEMPLATES = "Register workspace service template"
API_GET_WORKSPACE_SERVICE_TEMPLATES = "Get workspace service templates"
API_GET_WORKSPACE_SERVICE_TEMPLATES_IN_WORKSPACE = "Get workspace service templates (on workspace level)" # only returns templates that the authenticated user is authorized to use
API_GET_WORKSPACE_SERVICE_TEMPLATE_BY_NAME = "Get workspace service template by name and optional version"
API_DELETE_WORKSPACE_SERVICE_TEMPLATE = "Delete workspace service template"

API_CREATE_SHARED_SERVICE_TEMPLATES = "Register shared service template"
API_GET_SHARED_SERVICE_TEMPLATES = "Get shared service templates"
API_GET_SHARED_SERVICE_TEMPLATE_BY_NAME = "Get shared service template by name and optional version"
API_DELETE_SHARED_SERVICE_TEMPLATE = "Delete shared service template"

API_GET_ALL_SHARED_SERVICES = "Get all shared services"
API_GET_SHARED_SERVICE_BY_ID = "Get shared service by ID"
Expand All @@ -74,6 +77,9 @@
API_GET_USER_RESOURCE_TEMPLATES = "Get user resource templates applicable to the workspace service template"
API_GET_USER_RESOURCE_TEMPLATES_IN_WORKSPACE = "Get user resource templates applicable to the workspace service template (on workspace level)" # only returns templates that the authenticated user is authorized to use
API_GET_USER_RESOURCE_TEMPLATE_BY_NAME = "Get user resource template by name and workspace service and optional version"
API_DELETE_USER_RESOURCE_TEMPLATE = "Delete user resource template"

API_GET_TEMPLATES = "Get all templates"

# cost report
API_GET_COSTS = "Get overall costs"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ def _prepare(self, app, admin_user):
@patch("api.routes.shared_service_templates.ResourceTemplateRepository.get_templates_information")
async def test_get_shared_service_templates_returns_template_names_and_description(self, get_templates_info_mock, app, client):
expected_template_infos = [
ResourceTemplateInformation(name="template1", title="template 1", description="description1"),
ResourceTemplateInformation(name="template2", title="template 2", description="description2")
ResourceTemplateInformation(name="template1", title="template 1", description="description1", version="1.0", resourceType=ResourceType.SharedService),
ResourceTemplateInformation(name="template2", title="template 2", description="description2", version="1.0", resourceType=ResourceType.SharedService)
]
get_templates_info_mock.return_value = expected_template_infos

Expand Down Expand Up @@ -112,3 +112,17 @@ async def test_creating_a_shared_service_template_raises_http_422_if_step_ids_ar
response = await client.post(app.url_path_for(strings.API_CREATE_SHARED_SERVICE_TEMPLATES), json=input_shared_service_template.dict())

assert response.status_code == status.HTTP_422_UNPROCESSABLE_CONTENT

# DELETE /shared-service-templates/{shared_service_template_name}
@patch("api.routes.shared_service_templates.ResourceTemplateRepository.delete_template")
async def test_delete_shared_service_template_deletes_template(self, delete_template_mock, app, client):
response = await client.delete(app.url_path_for(strings.API_DELETE_SHARED_SERVICE_TEMPLATE, shared_service_template_name="template_name"))
assert response.status_code == status.HTTP_204_NO_CONTENT
delete_template_mock.assert_called_once_with("template_name", ResourceType.SharedService)

# DELETE /shared-service-templates/{shared_service_template_name}
@patch("api.routes.shared_service_templates.ResourceTemplateRepository.delete_template_by_version")
async def test_delete_shared_service_template_deletes_template_by_version(self, delete_template_by_version_mock, app, client):
response = await client.delete(app.url_path_for(strings.API_DELETE_SHARED_SERVICE_TEMPLATE, shared_service_template_name="template_name") + "?version=1.0")
assert response.status_code == status.HTTP_204_NO_CONTENT
delete_template_by_version_mock.assert_called_once_with("template_name", "1.0", ResourceType.SharedService)
2 changes: 2 additions & 0 deletions api_app/tests_ma/test_api/test_routes/test_shared_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@

from db.errors import EntityDoesNotExist
from models.domain.shared_service import SharedService
from models.schemas.resource_template import ResourceTemplateInformation
from resources import strings
from services.authentication import get_current_admin_user, get_current_tre_user_or_tre_admin
from azure.cosmos.exceptions import CosmosAccessConditionFailedError
from models.domain.resource import ResourceType


pytestmark = pytest.mark.asyncio
Expand Down
38 changes: 38 additions & 0 deletions api_app/tests_ma/test_api/test_routes/test_templates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import pytest
from mock import patch

from starlette import status

from services.authentication import get_current_admin_user
from models.domain.resource import ResourceType
from models.schemas.resource_template import ResourceTemplateInformation
from resources import strings

pytestmark = pytest.mark.asyncio


class TestTemplateRoutes:
@pytest.fixture(autouse=True, scope='class')
def _prepare(self, app, admin_user):
app.dependency_overrides[get_current_admin_user] = admin_user
yield
app.dependency_overrides = {}

# GET /templates
@patch("api.routes.templates.ResourceTemplateRepository.get_all_templates_information")
async def test_get_templates_returns_all_templates(self, get_all_templates_information_mock, app, client):
expected_templates = [
ResourceTemplateInformation(name="template1", title="template 1", description="description1", version="1.0", resourceType=ResourceType.Workspace),
ResourceTemplateInformation(name="template2", title="template 2", description="description2", version="1.0", resourceType=ResourceType.WorkspaceService),
ResourceTemplateInformation(name="template3", title="template 3", description="description3", version="1.0", resourceType=ResourceType.SharedService),
ResourceTemplateInformation(name="template4", title="template 4", description="description4", version="1.0", resourceType=ResourceType.UserResource, parentWorkspaceService="parent_service"),
]
get_all_templates_information_mock.return_value = expected_templates

response = await client.get(app.url_path_for(strings.API_GET_TEMPLATES))

assert response.status_code == status.HTTP_200_OK
actual_templates = response.json()["templates"]
assert len(actual_templates) == len(expected_templates)
for template in expected_templates:
assert template in actual_templates
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,8 @@ def _prepare(self, app, researcher_user):
@patch("api.routes.workspace_service_templates.ResourceTemplateRepository.get_templates_information")
async def test_get_user_resource_templates_returns_template_names_and_description(self, get_templates_information_mock, app, client):
expected_templates = [
ResourceTemplateInformation(name="template1", title="template 1", description="description1"),
ResourceTemplateInformation(name="template2", title="template 2", description="description2")
ResourceTemplateInformation(name="template1", title="template 1", description="description1", version="1.0", resourceType=ResourceType.UserResource),
ResourceTemplateInformation(name="template2", title="template 2", description="description2", version="1.0", resourceType=ResourceType.UserResource)
]
get_templates_information_mock.return_value = expected_templates

Expand Down Expand Up @@ -153,3 +153,17 @@ async def test_get_user_resource_templates_by_name_returns_returns_error_status_
response = await client.get(app.url_path_for(strings.API_GET_USER_RESOURCE_TEMPLATE_BY_NAME, service_template_name=service_template_name, user_resource_template_name=user_resource_template_name))

assert response.status_code == expected_status

# DELETE /workspace-service-templates/{service_template_name}/user-resource-templates/{user_resource_template_name}
@patch("api.routes.user_resource_templates.ResourceTemplateRepository.delete_template")
async def test_delete_user_resource_template_deletes_template(self, delete_template_mock, app, client):
response = await client.delete(app.url_path_for(strings.API_DELETE_USER_RESOURCE_TEMPLATE, service_template_name="service_template_name", user_resource_template_name="template_name"))
assert response.status_code == status.HTTP_204_NO_CONTENT
delete_template_mock.assert_called_once_with("template_name", ResourceType.UserResource, parent_service_name="service_template_name")

# DELETE /workspace-service-templates/{service_template_name}/user-resource-templates/{user_resource_template_name}
@patch("api.routes.user_resource_templates.ResourceTemplateRepository.delete_template_by_version")
async def test_delete_user_resource_template_deletes_template_by_version(self, delete_template_by_version_mock, app, client):
response = await client.delete(app.url_path_for(strings.API_DELETE_USER_RESOURCE_TEMPLATE, service_template_name="service_template_name", user_resource_template_name="template_name") + "?version=1.0")
assert response.status_code == status.HTTP_204_NO_CONTENT
delete_template_by_version_mock.assert_called_once_with("template_name", "1.0", ResourceType.UserResource, parent_service_name="service_template_name")
Loading