Support resource templateVersion update (#2908)

* resource update mechanism

Co-authored-by: Tamir Kamara <26870601+tamirkamara@users.noreply.github.com>
This commit is contained in:
Guy Bertental 2022-12-04 23:15:32 +02:00 коммит произвёл GitHub
Родитель 099d5f9a42
Коммит 21caf4c607
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
20 изменённых файлов: 544 добавлений и 37 удалений

1
.github/pull_request_template.md поставляемый
Просмотреть файл

@ -10,3 +10,4 @@ Describe the current behavior you are modifying. Please also remember to update
- Note any pending work (with links to the issues that will address them)
- Update documentation
- Update CHANGELOG.md if needed
- Increment template version if needed, for guidelines see [Authoring templates - versioning](https://microsoft.github.io/AzureTRE/tre-workspace-authors/authoring-workspace-templates/#versioning)

Просмотреть файл

@ -10,6 +10,7 @@ FEATURES:
ENHANCEMENTS:
* Remove Porter's Docker mixin as it's not in use ([#2889](https://github.com/microsoft/AzureTRE/pull/2889))
* Support template version update ([#2908](https://github.com/microsoft/AzureTRE/pull/2908))
BUG FIXES:
* Private endpoints for AppInsights are now provisioning successfully and consistently ([#2841](https://github.com/microsoft/AzureTRE/pull/2841))

Просмотреть файл

@ -1 +1 @@
__version__ = "0.5.23"
__version__ = "0.6.0"

Просмотреть файл

@ -4,7 +4,7 @@ from fastapi import APIRouter, Depends, HTTPException, Header, status, Response
from jsonschema.exceptions import ValidationError
from db.repositories.operations import OperationRepository
from db.errors import DuplicateEntity, UserNotAuthorizedToUseTemplate
from db.errors import DuplicateEntity, MajorVersionUpdateDenied, UserNotAuthorizedToUseTemplate, TargetTemplateVersionDoesNotExist, VersionDowngradeDenied
from api.dependencies.database import get_repository
from api.dependencies.shared_services import get_shared_service_by_id_from_path, get_operation_by_id_from_path
from db.repositories.resource_templates import ResourceTemplateRepository
@ -75,9 +75,9 @@ async def create_shared_service(response: Response, shared_service_input: Shared
response_model=OperationInResponse,
name=strings.API_UPDATE_SHARED_SERVICE,
dependencies=[Depends(get_current_admin_user), Depends(get_shared_service_by_id_from_path)])
async def patch_shared_service(shared_service_patch: ResourcePatch, response: Response, user=Depends(get_current_admin_user), shared_service_repo=Depends(get_repository(SharedServiceRepository)), shared_service=Depends(get_shared_service_by_id_from_path), resource_template_repo=Depends(get_repository(ResourceTemplateRepository)), operations_repo=Depends(get_repository(OperationRepository)), etag: str = Header(...)) -> SharedServiceInResponse:
async def patch_shared_service(shared_service_patch: ResourcePatch, response: Response, user=Depends(get_current_admin_user), shared_service_repo=Depends(get_repository(SharedServiceRepository)), shared_service=Depends(get_shared_service_by_id_from_path), resource_template_repo=Depends(get_repository(ResourceTemplateRepository)), operations_repo=Depends(get_repository(OperationRepository)), etag: str = Header(...), force_version_update: bool = False) -> SharedServiceInResponse:
try:
patched_shared_service, resource_template = shared_service_repo.patch_shared_service(shared_service, shared_service_patch, etag, resource_template_repo, user)
patched_shared_service, resource_template = shared_service_repo.patch_shared_service(shared_service, shared_service_patch, etag, resource_template_repo, user, force_version_update)
operation = await send_resource_request_message(
resource=patched_shared_service,
operations_repo=operations_repo,
@ -93,6 +93,8 @@ async def patch_shared_service(shared_service_patch: ResourcePatch, response: Re
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=strings.ETAG_CONFLICT)
except ValidationError as v:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=v.message)
except (MajorVersionUpdateDenied, TargetTemplateVersionDoesNotExist, VersionDowngradeDenied) as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
@shared_services_router.delete("/shared-services/{shared_service_id}", response_model=OperationInResponse, name=strings.API_DELETE_SHARED_SERVICE, dependencies=[Depends(get_current_admin_user)])

Просмотреть файл

@ -7,7 +7,7 @@ from jsonschema.exceptions import ValidationError
from api.dependencies.database import get_repository
from api.dependencies.workspaces import get_operation_by_id_from_path, get_workspace_by_id_from_path, get_deployed_workspace_by_id_from_path, get_deployed_workspace_service_by_id_from_path, get_workspace_service_by_id_from_path, get_user_resource_by_id_from_path
from db.errors import UserNotAuthorizedToUseTemplate
from db.errors import MajorVersionUpdateDenied, TargetTemplateVersionDoesNotExist, UserNotAuthorizedToUseTemplate, VersionDowngradeDenied
from db.repositories.operations import OperationRepository
from db.repositories.resource_templates import ResourceTemplateRepository
from db.repositories.user_resources import UserResourceRepository
@ -116,9 +116,9 @@ async def create_workspace(workspace_create: WorkspaceInCreate, response: Respon
@workspaces_core_router.patch("/workspaces/{workspace_id}", status_code=status.HTTP_202_ACCEPTED, response_model=OperationInResponse, name=strings.API_UPDATE_WORKSPACE, dependencies=[Depends(get_current_admin_user)])
async def patch_workspace(workspace_patch: ResourcePatch, response: Response, user=Depends(get_current_admin_user), workspace=Depends(get_workspace_by_id_from_path), workspace_repo=Depends(get_repository(WorkspaceRepository)), resource_template_repo=Depends(get_repository(ResourceTemplateRepository)), operations_repo=Depends(get_repository(OperationRepository)), etag: str = Header(...)) -> OperationInResponse:
async def patch_workspace(workspace_patch: ResourcePatch, response: Response, user=Depends(get_current_admin_user), workspace=Depends(get_workspace_by_id_from_path), workspace_repo: WorkspaceRepository = Depends(get_repository(WorkspaceRepository)), resource_template_repo=Depends(get_repository(ResourceTemplateRepository)), operations_repo=Depends(get_repository(OperationRepository)), etag: str = Header(...), force_version_update: bool = False) -> OperationInResponse:
try:
patched_workspace, resource_template = workspace_repo.patch_workspace(workspace, workspace_patch, etag, resource_template_repo, user)
patched_workspace, resource_template = workspace_repo.patch_workspace(workspace, workspace_patch, etag, resource_template_repo, user, force_version_update)
operation = await send_resource_request_message(
resource=patched_workspace,
operations_repo=operations_repo,
@ -133,6 +133,8 @@ async def patch_workspace(workspace_patch: ResourcePatch, response: Response, us
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=strings.ETAG_CONFLICT)
except ValidationError as v:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=v.message)
except (MajorVersionUpdateDenied, TargetTemplateVersionDoesNotExist, VersionDowngradeDenied) as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
@workspaces_core_router.delete("/workspaces/{workspace_id}", response_model=OperationInResponse, name=strings.API_DELETE_WORKSPACE, dependencies=[Depends(get_current_admin_user)])
@ -242,9 +244,9 @@ async def create_workspace_service(response: Response, workspace_service_input:
@workspace_services_workspace_router.patch("/workspaces/{workspace_id}/workspace-services/{service_id}", status_code=status.HTTP_202_ACCEPTED, response_model=OperationInResponse, name=strings.API_UPDATE_WORKSPACE_SERVICE, dependencies=[Depends(get_current_workspace_owner_or_researcher_user), Depends(get_workspace_by_id_from_path)])
async def patch_workspace_service(workspace_service_patch: ResourcePatch, response: Response, user=Depends(get_current_workspace_owner_user), workspace_service_repo=Depends(get_repository(WorkspaceServiceRepository)), workspace_service=Depends(get_workspace_service_by_id_from_path), resource_template_repo=Depends(get_repository(ResourceTemplateRepository)), operations_repo=Depends(get_repository(OperationRepository)), etag: str = Header(...)) -> OperationInResponse:
async def patch_workspace_service(workspace_service_patch: ResourcePatch, response: Response, user=Depends(get_current_workspace_owner_user), workspace_service_repo=Depends(get_repository(WorkspaceServiceRepository)), workspace_service=Depends(get_workspace_service_by_id_from_path), resource_template_repo=Depends(get_repository(ResourceTemplateRepository)), operations_repo=Depends(get_repository(OperationRepository)), etag: str = Header(...), force_version_update: bool = False) -> OperationInResponse:
try:
patched_workspace_service, resource_template = workspace_service_repo.patch_workspace_service(workspace_service, workspace_service_patch, etag, resource_template_repo, user)
patched_workspace_service, resource_template = workspace_service_repo.patch_workspace_service(workspace_service, workspace_service_patch, etag, resource_template_repo, user, force_version_update)
operation = await send_resource_request_message(
resource=patched_workspace_service,
operations_repo=operations_repo,
@ -259,6 +261,8 @@ async def patch_workspace_service(workspace_service_patch: ResourcePatch, respon
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=strings.ETAG_CONFLICT)
except ValidationError as v:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=v.message)
except (MajorVersionUpdateDenied, TargetTemplateVersionDoesNotExist, VersionDowngradeDenied) as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
@workspace_services_workspace_router.delete("/workspaces/{workspace_id}/workspace-services/{service_id}", response_model=OperationInResponse, name=strings.API_DELETE_WORKSPACE_SERVICE, dependencies=[Depends(get_current_workspace_owner_user)])
@ -417,11 +421,12 @@ async def patch_user_resource(
user_resource_repo=Depends(get_repository(UserResourceRepository)),
resource_template_repo=Depends(get_repository(ResourceTemplateRepository)),
operations_repo=Depends(get_repository(OperationRepository)),
etag: str = Header(...)) -> OperationInResponse:
etag: str = Header(...),
force_version_update: bool = False) -> OperationInResponse:
validate_user_has_valid_role_for_user_resource(user, user_resource)
try:
patched_user_resource, resource_template = user_resource_repo.patch_user_resource(user_resource, user_resource_patch, etag, resource_template_repo, workspace_service.templateName, user)
patched_user_resource, resource_template = user_resource_repo.patch_user_resource(user_resource, user_resource_patch, etag, resource_template_repo, workspace_service.templateName, user, force_version_update)
operation = await send_resource_request_message(
resource=patched_user_resource,
operations_repo=operations_repo,
@ -437,6 +442,8 @@ async def patch_user_resource(
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=strings.ETAG_CONFLICT)
except ValidationError as v:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=v.message)
except (MajorVersionUpdateDenied, TargetTemplateVersionDoesNotExist, VersionDowngradeDenied) as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
# user resource actions

Просмотреть файл

@ -24,3 +24,15 @@ class InvalidInput(Exception):
class UserNotAuthorizedToUseTemplate(Exception):
"""Raised when user attempts to use a template they aren't authorized to use"""
class MajorVersionUpdateDenied(Exception):
"""Raised when user attempts to update a resource with a major version."""
class TargetTemplateVersionDoesNotExist(Exception):
"""Raised when user attempts to upgrade a resource to a version which was not registered."""
class VersionDowngradeDenied(Exception):
"""Raised when user attempts to downgrade a resource to a lower version."""

Просмотреть файл

@ -1,11 +1,12 @@
import copy
import semantic_version
from datetime import datetime
from typing import Tuple, List
from azure.cosmos import CosmosClient
from azure.cosmos.exceptions import CosmosResourceNotFoundError
from core import config
from db.errors import EntityDoesNotExist, UserNotAuthorizedToUseTemplate
from db.errors import VersionDowngradeDenied, EntityDoesNotExist, MajorVersionUpdateDenied, TargetTemplateVersionDoesNotExist, UserNotAuthorizedToUseTemplate
from db.repositories.base import BaseRepository
from db.repositories.resource_templates import ResourceTemplateRepository
from jsonschema import validate
@ -96,7 +97,7 @@ class ResourceRepository(BaseRepository):
return parse_obj_as(ResourceTemplate, template)
def patch_resource(self, resource: Resource, resource_patch: ResourcePatch, resource_template: ResourceTemplate, etag: str, resource_template_repo: ResourceTemplateRepository, user: User) -> Tuple[Resource, ResourceTemplate]:
def patch_resource(self, resource: Resource, resource_patch: ResourcePatch, resource_template: ResourceTemplate, etag: str, resource_template_repo: ResourceTemplateRepository, user: User, force_version_update: bool = False) -> Tuple[Resource, ResourceTemplate]:
# create a deep copy of the resource to use for history, create the history item + add to history list
resource_copy = copy.deepcopy(resource)
history_item = ResourceHistoryItem(
@ -104,7 +105,8 @@ class ResourceRepository(BaseRepository):
properties=resource_copy.properties,
resourceVersion=resource_copy.resourceVersion,
updatedWhen=resource_copy.updatedWhen,
user=resource_copy.user
user=resource_copy.user,
templateVersion=resource_copy.templateVersion
)
resource.history.append(history_item)
@ -116,6 +118,10 @@ class ResourceRepository(BaseRepository):
if resource_patch.isEnabled is not None:
resource.isEnabled = resource_patch.isEnabled
if resource_patch.templateVersion is not None:
self.validate_template_version_patch(resource, resource_patch, resource_template_repo, resource_template, force_version_update)
resource.templateVersion = resource_patch.templateVersion
if resource_patch.properties is not None and len(resource_patch.properties) > 0:
self.validate_patch(resource_patch, resource_template_repo, resource_template)
@ -125,6 +131,27 @@ class ResourceRepository(BaseRepository):
self.update_item_with_etag(resource, etag)
return resource, resource_template
def validate_template_version_patch(self, resource: Resource, resource_patch: ResourcePatch, resource_template_repo: ResourceTemplateRepository, resource_template: ResourceTemplate, force_version_update: bool = False):
parent_resource_id = None
if resource.resourceType == ResourceType.UserResource:
parent_resource_id = resource.parentWorkspaceServiceId
# validate Major upgrade
desired_version = semantic_version.Version(resource_patch.templateVersion)
current_version = semantic_version.Version(resource.templateVersion)
if not force_version_update:
if desired_version.major > current_version.major:
raise MajorVersionUpdateDenied(f'Attempt to upgrade from {current_version} to {desired_version} denied. major version upgrade is not allowed.')
elif desired_version < current_version:
raise VersionDowngradeDenied(f'Attempt to downgrade from {current_version} to {desired_version} denied. version downgrade is not allowed.')
# validate if target template with desired version is registered
try:
resource_template_repo.get_template_by_name_and_version(resource.templateName, resource_patch.templateVersion, resource_template.resourceType, parent_resource_id)
except EntityDoesNotExist:
raise TargetTemplateVersionDoesNotExist(f"Template '{resource_template.name}' not found for resource type '{resource_template.resourceType}' with target template version '{resource_patch.templateVersion}'")
def validate_patch(self, resource_patch: ResourcePatch, resource_template_repo: ResourceTemplateRepository, resource_template: ResourceTemplate):
# get the enriched (combined) template
enriched_template = resource_template_repo.enrich_template(resource_template, is_update=True)

Просмотреть файл

@ -83,7 +83,7 @@ class SharedServiceRepository(ResourceRepository):
return shared_service, template
def patch_shared_service(self, shared_service: SharedService, shared_service_patch: ResourcePatch, etag: str, resource_template_repo: ResourceTemplateRepository, user: User) -> Tuple[SharedService, ResourceTemplate]:
def patch_shared_service(self, shared_service: SharedService, shared_service_patch: ResourcePatch, etag: str, resource_template_repo: ResourceTemplateRepository, user: User, force_version_update: bool) -> Tuple[SharedService, ResourceTemplate]:
# get shared service template
shared_service_template = resource_template_repo.get_template_by_name_and_version(shared_service.templateName, shared_service.templateVersion, ResourceType.SharedService)
return self.patch_resource(shared_service, shared_service_patch, shared_service_template, etag, resource_template_repo, user)
return self.patch_resource(shared_service, shared_service_patch, shared_service_template, etag, resource_template_repo, user, force_version_update)

Просмотреть файл

@ -67,7 +67,7 @@ class UserResourceRepository(ResourceRepository):
def get_user_resource_spec_params(self):
return self.get_resource_base_spec_params()
def patch_user_resource(self, user_resource: UserResource, user_resource_patch: ResourcePatch, etag: str, resource_template_repo: ResourceTemplateRepository, parent_template_name: str, user: User) -> Tuple[UserResource, ResourceTemplate]:
def patch_user_resource(self, user_resource: UserResource, user_resource_patch: ResourcePatch, etag: str, resource_template_repo: ResourceTemplateRepository, parent_template_name: str, user: User, force_version_update: bool) -> Tuple[UserResource, ResourceTemplate]:
# get user resource template
user_resource_template = resource_template_repo.get_template_by_name_and_version(user_resource.templateName, user_resource.templateVersion, ResourceType.UserResource, parent_service_name=parent_template_name)
return self.patch_resource(user_resource, user_resource_patch, user_resource_template, etag, resource_template_repo, user)
return self.patch_resource(user_resource, user_resource_patch, user_resource_template, etag, resource_template_repo, user, force_version_update)

Просмотреть файл

@ -74,7 +74,7 @@ class WorkspaceServiceRepository(ResourceRepository):
return workspace_service, template
def patch_workspace_service(self, workspace_service: WorkspaceService, workspace_service_patch: ResourcePatch, etag: str, resource_template_repo: ResourceTemplateRepository, user: User) -> Tuple[WorkspaceService, ResourceTemplate]:
def patch_workspace_service(self, workspace_service: WorkspaceService, workspace_service_patch: ResourcePatch, etag: str, resource_template_repo: ResourceTemplateRepository, user: User, force_version_update: bool) -> Tuple[WorkspaceService, ResourceTemplate]:
# get workspace service template
workspace_service_template = resource_template_repo.get_template_by_name_and_version(workspace_service.templateName, workspace_service.templateVersion, ResourceType.WorkspaceService)
return self.patch_resource(workspace_service, workspace_service_patch, workspace_service_template, etag, resource_template_repo, user)
return self.patch_resource(workspace_service, workspace_service_patch, workspace_service_template, etag, resource_template_repo, user, force_version_update)

Просмотреть файл

@ -137,10 +137,10 @@ class WorkspaceRepository(ResourceRepository):
new_address_space = generate_new_cidr(networks, cidr_netmask)
return new_address_space
def patch_workspace(self, workspace: Workspace, workspace_patch: ResourcePatch, etag: str, resource_template_repo: ResourceTemplateRepository, user: User) -> Tuple[Workspace, ResourceTemplate]:
def patch_workspace(self, workspace: Workspace, workspace_patch: ResourcePatch, etag: str, resource_template_repo: ResourceTemplateRepository, user: User, force_version_update: bool) -> Tuple[Workspace, ResourceTemplate]:
# get the workspace template
workspace_template = resource_template_repo.get_template_by_name_and_version(workspace.templateName, workspace.templateVersion, ResourceType.Workspace)
return self.patch_resource(workspace, workspace_patch, workspace_template, etag, resource_template_repo, user)
return self.patch_resource(workspace, workspace_patch, workspace_template, etag, resource_template_repo, user, force_version_update)
def get_workspace_spec_params(self, full_workspace_id: str):
params = self.get_resource_base_spec_params()

Просмотреть файл

@ -25,6 +25,7 @@ class ResourceHistoryItem(AzureTREModel):
resourceVersion: int
updatedWhen: float
user: dict = {}
templateVersion: Optional[str]
class Resource(AzureTREModel):

Просмотреть файл

@ -5,11 +5,13 @@ from pydantic import BaseModel
class ResourcePatch(BaseModel):
isEnabled: Optional[bool]
properties: Optional[dict]
templateVersion: Optional[str]
class Config:
schema_extra = {
"example": {
"isEnabled": False,
"templateVersion": "1.0.1",
"properties": {
"display_name": "the display name",
"description": "a description",

Просмотреть файл

@ -163,7 +163,7 @@ class TestSharedServiceRoutesThatRequireAdminRights:
modified_shared_service = sample_shared_service()
modified_shared_service.isEnabled = False
modified_shared_service.history = [ResourceHistoryItem(properties=copy.deepcopy(modified_shared_service.properties), isEnabled=True, resourceVersion=0, updatedWhen=FAKE_CREATE_TIMESTAMP, user=create_admin_user())]
modified_shared_service.history = [ResourceHistoryItem(properties=copy.deepcopy(modified_shared_service.properties), isEnabled=True, resourceVersion=0, updatedWhen=FAKE_CREATE_TIMESTAMP, user=create_admin_user(), templateVersion=modified_shared_service.templateVersion)]
modified_shared_service.resourceVersion = 1
modified_shared_service.updatedWhen = FAKE_UPDATE_TIMESTAMP
modified_shared_service.user = create_admin_user()
@ -172,3 +172,93 @@ class TestSharedServiceRoutesThatRequireAdminRights:
update_item_mock.assert_called_once_with(modified_shared_service, etag)
assert response.status_code == status.HTTP_202_ACCEPTED
# [PATCH] /shared-services/{shared_service_id}
@patch("api.routes.shared_services.SharedServiceRepository.get_timestamp", return_value=FAKE_UPDATE_TIMESTAMP)
@patch("api.dependencies.shared_services.SharedServiceRepository.get_shared_service_by_id", return_value=sample_shared_service(SHARED_SERVICE_ID))
@patch("api.routes.shared_services.ResourceTemplateRepository.get_template_by_name_and_version", return_value=sample_shared_service())
@patch("api.routes.shared_services.SharedServiceRepository.update_item_with_etag", return_value=sample_shared_service())
@patch("api.routes.shared_services.send_resource_request_message", return_value=sample_resource_operation(resource_id=SHARED_SERVICE_ID, operation_id=OPERATION_ID))
async def test_patch_shared_service_with_upgrade_minor_version_patches_shared_service(self, _, update_item_mock, __, ___, ____, app, client):
etag = "some-etag-value"
shared_service_patch = {"templateVersion": "0.2.0"}
modified_shared_service = sample_shared_service()
modified_shared_service.isEnabled = True
modified_shared_service.history = [ResourceHistoryItem(properties=copy.deepcopy(modified_shared_service.properties), isEnabled=True, resourceVersion=0, updatedWhen=FAKE_CREATE_TIMESTAMP, user=create_admin_user(), templateVersion=modified_shared_service.templateVersion)]
modified_shared_service.resourceVersion = 1
modified_shared_service.updatedWhen = FAKE_UPDATE_TIMESTAMP
modified_shared_service.user = create_admin_user()
modified_shared_service.templateVersion = "0.2.0"
response = await client.patch(app.url_path_for(strings.API_UPDATE_SHARED_SERVICE, shared_service_id=SHARED_SERVICE_ID), json=shared_service_patch, headers={"etag": etag})
update_item_mock.assert_called_once_with(modified_shared_service, etag)
assert response.status_code == status.HTTP_202_ACCEPTED
# [PATCH] /shared-services/{shared_service_id}
@patch("api.routes.shared_services.SharedServiceRepository.get_timestamp", return_value=FAKE_UPDATE_TIMESTAMP)
@patch("api.dependencies.shared_services.SharedServiceRepository.get_shared_service_by_id", return_value=sample_shared_service(SHARED_SERVICE_ID))
@patch("api.routes.shared_services.ResourceTemplateRepository.get_template_by_name_and_version", return_value=sample_shared_service())
@patch("api.routes.shared_services.SharedServiceRepository.update_item_with_etag", return_value=sample_shared_service())
@patch("api.routes.shared_services.send_resource_request_message", return_value=sample_resource_operation(resource_id=SHARED_SERVICE_ID, operation_id=OPERATION_ID))
async def test_patch_shared_service_with_upgrade_major_version_and_force_update_patches_shared_service(self, _, update_item_mock, __, ___, ____, app, client):
etag = "some-etag-value"
shared_service_patch = {"templateVersion": "2.0.0"}
modified_shared_service = sample_shared_service()
modified_shared_service.isEnabled = True
modified_shared_service.history = [ResourceHistoryItem(properties=copy.deepcopy(modified_shared_service.properties), isEnabled=True, resourceVersion=0, updatedWhen=FAKE_CREATE_TIMESTAMP, user=create_admin_user(), templateVersion=modified_shared_service.templateVersion)]
modified_shared_service.resourceVersion = 1
modified_shared_service.updatedWhen = FAKE_UPDATE_TIMESTAMP
modified_shared_service.user = create_admin_user()
modified_shared_service.templateVersion = "2.0.0"
response = await client.patch(app.url_path_for(strings.API_UPDATE_SHARED_SERVICE, shared_service_id=SHARED_SERVICE_ID) + "?force_version_update=True", json=shared_service_patch, headers={"etag": etag})
update_item_mock.assert_called_once_with(modified_shared_service, etag)
assert response.status_code == status.HTTP_202_ACCEPTED
# [PATCH] /shared-services/{shared_service_id}
@patch("api.routes.shared_services.SharedServiceRepository.get_timestamp", return_value=FAKE_UPDATE_TIMESTAMP)
@patch("api.dependencies.shared_services.SharedServiceRepository.get_shared_service_by_id", return_value=sample_shared_service(SHARED_SERVICE_ID))
@patch("api.routes.shared_services.ResourceTemplateRepository.get_template_by_name_and_version", return_value=None)
@patch("api.routes.shared_services.SharedServiceRepository.update_item_with_etag", return_value=sample_shared_service())
@patch("api.routes.shared_services.send_resource_request_message", return_value=sample_resource_operation(resource_id=SHARED_SERVICE_ID, operation_id=OPERATION_ID))
async def test_patch_shared_service_with_upgrade_major_version_returns_bad_request(self, _, update_item_mock, __, ___, ____, app, client):
etag = "some-etag-value"
shared_service_patch = {"templateVersion": "2.0.0"}
modified_shared_service = sample_shared_service()
modified_shared_service.isEnabled = True
modified_shared_service.history = [ResourceHistoryItem(properties=copy.deepcopy(modified_shared_service.properties), isEnabled=True, resourceVersion=0, updatedWhen=FAKE_CREATE_TIMESTAMP, user=create_admin_user(), templateVersion=modified_shared_service.templateVersion)]
modified_shared_service.resourceVersion = 1
modified_shared_service.updatedWhen = FAKE_UPDATE_TIMESTAMP
modified_shared_service.user = create_admin_user()
response = await client.patch(app.url_path_for(strings.API_UPDATE_SHARED_SERVICE, shared_service_id=SHARED_SERVICE_ID), json=shared_service_patch, headers={"etag": etag})
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert response.text == 'Attempt to upgrade from 0.1.0 to 2.0.0 denied. major version upgrade is not allowed.'
# [PATCH] /shared-services/{shared_service_id}
@patch("api.routes.shared_services.SharedServiceRepository.get_timestamp", return_value=FAKE_UPDATE_TIMESTAMP)
@patch("api.dependencies.shared_services.SharedServiceRepository.get_shared_service_by_id", return_value=sample_shared_service(SHARED_SERVICE_ID))
@patch("api.routes.shared_services.ResourceTemplateRepository.get_template_by_name_and_version", return_value=None)
@patch("api.routes.shared_services.SharedServiceRepository.update_item_with_etag", return_value=sample_shared_service())
@patch("api.routes.shared_services.send_resource_request_message", return_value=sample_resource_operation(resource_id=SHARED_SERVICE_ID, operation_id=OPERATION_ID))
async def test_patch_shared_service_with_downgrade_version_returns_bad_request(self, _, update_item_mock, __, ___, ____, app, client):
etag = "some-etag-value"
shared_service_patch = {"templateVersion": "0.0.1"}
modified_shared_service = sample_shared_service()
modified_shared_service.isEnabled = True
modified_shared_service.history = [ResourceHistoryItem(properties=copy.deepcopy(modified_shared_service.properties), isEnabled=True, resourceVersion=0, updatedWhen=FAKE_CREATE_TIMESTAMP, user=create_admin_user(), templateVersion=modified_shared_service.templateVersion)]
modified_shared_service.resourceVersion = 1
modified_shared_service.updatedWhen = FAKE_UPDATE_TIMESTAMP
modified_shared_service.user = create_admin_user()
response = await client.patch(app.url_path_for(strings.API_UPDATE_SHARED_SERVICE, shared_service_id=SHARED_SERVICE_ID), json=shared_service_patch, headers={"etag": etag})
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert response.text == 'Attempt to downgrade from 0.1.0 to 0.0.1 denied. version downgrade is not allowed.'

Просмотреть файл

@ -452,7 +452,7 @@ class TestWorkspaceRoutesThatRequireAdminRights:
modified_workspace = sample_workspace()
modified_workspace.isEnabled = False
modified_workspace.history = [ResourceHistoryItem(properties={'client_id': '12345', 'scope_id': 'test_scope_id'}, isEnabled=True, resourceVersion=0, updatedWhen=FAKE_CREATE_TIMESTAMP, user=create_admin_user())]
modified_workspace.history = [ResourceHistoryItem(properties={'client_id': '12345', 'scope_id': 'test_scope_id'}, isEnabled=True, resourceVersion=0, updatedWhen=FAKE_CREATE_TIMESTAMP, user=create_admin_user(), templateVersion=modified_workspace.templateVersion)]
modified_workspace.resourceVersion = 1
modified_workspace.user = create_admin_user()
modified_workspace.updatedWhen = FAKE_UPDATE_TIMESTAMP
@ -462,6 +462,96 @@ class TestWorkspaceRoutesThatRequireAdminRights:
update_item_mock.assert_called_once_with(modified_workspace, etag)
assert response.status_code == status.HTTP_202_ACCEPTED
# [PATCH] /workspaces/{workspace_id}
@ patch("api.routes.workspaces.send_resource_request_message", return_value=sample_resource_operation(resource_id=WORKSPACE_ID, operation_id=OPERATION_ID))
@ patch("api.dependencies.workspaces.WorkspaceRepository.get_workspace_by_id", return_value=sample_workspace())
@ patch("api.routes.workspaces.WorkspaceRepository.update_item_with_etag", return_value=sample_workspace())
@ patch("api.routes.workspaces.ResourceTemplateRepository.get_template_by_name_and_version", return_value=None)
@ patch("api.routes.workspaces.WorkspaceRepository.get_timestamp", return_value=FAKE_UPDATE_TIMESTAMP)
async def test_patch_workspaces_with_upgrade_major_version_returns_bad_request(self, _, __, update_item_mock, ___, ____, app, client):
workspace_patch = {"templateVersion": "2.0.0"}
etag = "some-etag-value"
modified_workspace = sample_workspace()
modified_workspace.isEnabled = True
modified_workspace.history = [ResourceHistoryItem(properties={'client_id': '12345', 'scope_id': 'test_scope_id'}, isEnabled=True, resourceVersion=0, updatedWhen=FAKE_CREATE_TIMESTAMP, user=create_admin_user(), templateVersion=modified_workspace.templateVersion)]
modified_workspace.resourceVersion = 1
modified_workspace.user = create_admin_user()
modified_workspace.updatedWhen = FAKE_UPDATE_TIMESTAMP
response = await client.patch(app.url_path_for(strings.API_UPDATE_WORKSPACE, workspace_id=WORKSPACE_ID), json=workspace_patch, headers={"etag": etag})
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert response.text == 'Attempt to upgrade from 0.1.0 to 2.0.0 denied. major version upgrade is not allowed.'
# [PATCH] /workspaces/{workspace_id}
@ patch("api.routes.workspaces.send_resource_request_message", return_value=sample_resource_operation(resource_id=WORKSPACE_ID, operation_id=OPERATION_ID))
@ patch("api.dependencies.workspaces.WorkspaceRepository.get_workspace_by_id", return_value=sample_workspace())
@ patch("api.routes.workspaces.WorkspaceRepository.update_item_with_etag", return_value=sample_workspace())
@ patch("api.routes.workspaces.ResourceTemplateRepository.get_template_by_name_and_version", return_value=sample_workspace())
@ patch("api.routes.workspaces.WorkspaceRepository.get_timestamp", return_value=FAKE_UPDATE_TIMESTAMP)
async def test_patch_workspaces_with_upgrade_major_version_and_force_update_returns_patched_workspace(self, _, __, update_item_mock, ___, ____, app, client):
workspace_patch = {"templateVersion": "2.0.0"}
etag = "some-etag-value"
modified_workspace = sample_workspace()
modified_workspace.isEnabled = True
modified_workspace.history = [ResourceHistoryItem(properties={'client_id': '12345', 'scope_id': 'test_scope_id'}, isEnabled=True, resourceVersion=0, updatedWhen=FAKE_CREATE_TIMESTAMP, user=create_admin_user(), templateVersion=modified_workspace.templateVersion)]
modified_workspace.resourceVersion = 1
modified_workspace.user = create_admin_user()
modified_workspace.updatedWhen = FAKE_UPDATE_TIMESTAMP
modified_workspace.templateVersion = "2.0.0"
response = await client.patch(app.url_path_for(strings.API_UPDATE_WORKSPACE, workspace_id=WORKSPACE_ID) + "?force_version_update=True", json=workspace_patch, headers={"etag": etag})
update_item_mock.assert_called_once_with(modified_workspace, etag)
assert response.status_code == status.HTTP_202_ACCEPTED
# [PATCH] /workspaces/{workspace_id}
@ patch("api.routes.workspaces.send_resource_request_message", return_value=sample_resource_operation(resource_id=WORKSPACE_ID, operation_id=OPERATION_ID))
@ patch("api.dependencies.workspaces.WorkspaceRepository.get_workspace_by_id", return_value=sample_workspace())
@ patch("api.routes.workspaces.WorkspaceRepository.update_item_with_etag", return_value=sample_workspace())
@ patch("api.routes.workspaces.ResourceTemplateRepository.get_template_by_name_and_version", return_value=None)
@ patch("api.routes.workspaces.WorkspaceRepository.get_timestamp", return_value=FAKE_UPDATE_TIMESTAMP)
async def test_patch_workspaces_with_downgrade_version_returns_bad_request(self, _, __, update_item_mock, ___, ____, app, client):
workspace_patch = {"templateVersion": "0.0.1"}
etag = "some-etag-value"
modified_workspace = sample_workspace()
modified_workspace.isEnabled = True
modified_workspace.history = [ResourceHistoryItem(properties={'client_id': '12345', 'scope_id': 'test_scope_id'}, isEnabled=True, resourceVersion=0, updatedWhen=FAKE_CREATE_TIMESTAMP, user=create_admin_user(), templateVersion=modified_workspace.templateVersion)]
modified_workspace.resourceVersion = 1
modified_workspace.user = create_admin_user()
modified_workspace.updatedWhen = FAKE_UPDATE_TIMESTAMP
response = await client.patch(app.url_path_for(strings.API_UPDATE_WORKSPACE, workspace_id=WORKSPACE_ID), json=workspace_patch, headers={"etag": etag})
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert response.text == 'Attempt to downgrade from 0.1.0 to 0.0.1 denied. version downgrade is not allowed.'
# [PATCH] /workspaces/{workspace_id}
@ patch("api.routes.workspaces.send_resource_request_message", return_value=sample_resource_operation(resource_id=WORKSPACE_ID, operation_id=OPERATION_ID))
@ patch("api.dependencies.workspaces.WorkspaceRepository.get_workspace_by_id", return_value=sample_workspace())
@ patch("api.routes.workspaces.WorkspaceRepository.update_item_with_etag", return_value=sample_workspace())
@ patch("api.routes.workspaces.ResourceTemplateRepository.get_template_by_name_and_version", return_value=sample_workspace())
@ patch("api.routes.workspaces.WorkspaceRepository.get_timestamp", return_value=FAKE_UPDATE_TIMESTAMP)
async def test_patch_workspaces_with_upgrade_minor_version_patches_workspace(self, _, __, update_item_mock, ___, ____, app, client):
workspace_patch = {"templateVersion": "0.2.0"}
etag = "some-etag-value"
modified_workspace = sample_workspace()
modified_workspace.isEnabled = True
modified_workspace.history = [ResourceHistoryItem(properties={'client_id': '12345', 'scope_id': 'test_scope_id'}, isEnabled=True, resourceVersion=0, updatedWhen=FAKE_CREATE_TIMESTAMP, user=create_admin_user(), templateVersion=modified_workspace.templateVersion)]
modified_workspace.resourceVersion = 1
modified_workspace.user = create_admin_user()
modified_workspace.updatedWhen = FAKE_UPDATE_TIMESTAMP
modified_workspace.templateVersion = "0.2.0"
response = await client.patch(app.url_path_for(strings.API_UPDATE_WORKSPACE, workspace_id=WORKSPACE_ID), json=workspace_patch, headers={"etag": etag})
update_item_mock.assert_called_once_with(modified_workspace, etag)
assert response.status_code == status.HTTP_202_ACCEPTED
# [PATCH] /workspaces/{workspace_id}
@ patch("api.dependencies.workspaces.WorkspaceRepository.get_workspace_by_id", return_value=sample_workspace())
@ patch("api.routes.workspaces.WorkspaceRepository.update_item_with_etag", side_effect=CosmosAccessConditionFailedError)
@ -472,7 +562,7 @@ class TestWorkspaceRoutesThatRequireAdminRights:
etag = "some-etag-value"
modified_workspace = sample_workspace()
modified_workspace.isEnabled = False
modified_workspace.history = [ResourceHistoryItem(properties={'client_id': '12345', 'scope_id': 'test_scope_id'}, isEnabled=True, resourceVersion=0, updatedWhen=FAKE_CREATE_TIMESTAMP, user=create_admin_user())]
modified_workspace.history = [ResourceHistoryItem(properties={'client_id': '12345', 'scope_id': 'test_scope_id'}, isEnabled=True, resourceVersion=0, updatedWhen=FAKE_CREATE_TIMESTAMP, user=create_admin_user(), templateVersion=modified_workspace.templateVersion)]
modified_workspace.resourceVersion = 1
modified_workspace.user = create_admin_user()
modified_workspace.updatedWhen = FAKE_UPDATE_TIMESTAMP
@ -704,13 +794,13 @@ class TestWorkspaceServiceRoutesThatRequireOwnerRights:
@ patch("api.dependencies.workspaces.UserResourceRepository.get_user_resource_by_id", return_value=sample_user_resource_object())
@ patch("api.routes.workspaces.UserResourceRepository.update_item_with_etag", return_value=sample_user_resource_object())
@ patch("api.routes.workspaces.UserResourceRepository.get_timestamp", return_value=FAKE_UPDATE_TIMESTAMP)
async def test_patch_user_resources_patches_user_resource(self, _, update_item_mock, __, ___, ____, _____, ______, _______, app, client):
async def test_patch_user_resource_patches_user_resource(self, _, update_item_mock, __, ___, ____, _____, ______, _______, app, client):
user_resource_service_patch = {"isEnabled": False}
etag = "some-etag-value"
modified_user_resource = sample_user_resource_object()
modified_user_resource.isEnabled = False
modified_user_resource.history = [ResourceHistoryItem(properties={}, isEnabled=True, resourceVersion=0, updatedWhen=FAKE_CREATE_TIMESTAMP, user=create_workspace_researcher_user())]
modified_user_resource.history = [ResourceHistoryItem(properties={}, isEnabled=True, resourceVersion=0, updatedWhen=FAKE_CREATE_TIMESTAMP, user=create_workspace_researcher_user(), templateVersion=modified_user_resource.templateVersion)]
modified_user_resource.resourceVersion = 1
modified_user_resource.updatedWhen = FAKE_UPDATE_TIMESTAMP
modified_user_resource.user = create_workspace_owner_user()
@ -720,6 +810,108 @@ class TestWorkspaceServiceRoutesThatRequireOwnerRights:
update_item_mock.assert_called_once_with(modified_user_resource, etag)
assert response.status_code == status.HTTP_202_ACCEPTED
# [PATCH] /workspaces/{workspace_id}/workspace-services/{service_id}/user-resources/{resource_id}
@ patch("api.routes.workspaces.send_resource_request_message", return_value=sample_resource_operation(resource_id=USER_RESOURCE_ID, operation_id=OPERATION_ID))
@ patch("api.routes.workspaces.ResourceTemplateRepository.get_template_by_name_and_version", return_value=sample_workspace_service())
@ patch("api.routes.workspaces.validate_user_has_valid_role_for_user_resource")
@ patch("api.dependencies.workspaces.WorkspaceServiceRepository.get_workspace_service_by_id", return_value=sample_workspace_service())
@ patch("api.dependencies.workspaces.WorkspaceRepository.get_workspace_by_id", return_value=sample_workspace())
@ patch("api.dependencies.workspaces.UserResourceRepository.get_user_resource_by_id", return_value=sample_user_resource_object())
@ patch("api.routes.workspaces.UserResourceRepository.update_item_with_etag", return_value=sample_user_resource_object())
@ patch("api.routes.workspaces.UserResourceRepository.get_timestamp", return_value=FAKE_UPDATE_TIMESTAMP)
async def test_patch_user_resource_with_upgrade_major_version_returns_bad_request(self, _, update_item_mock, __, ___, ____, _____, ______, _______, app, client):
user_resource_service_patch = {"templateVersion": "2.0.0"}
etag = "some-etag-value"
modified_user_resource = sample_user_resource_object()
modified_user_resource.isEnabled = True
modified_user_resource.history = [ResourceHistoryItem(properties={}, isEnabled=True, resourceVersion=0, updatedWhen=FAKE_CREATE_TIMESTAMP, user=create_workspace_researcher_user(), templateVersion=modified_user_resource.templateVersion)]
modified_user_resource.resourceVersion = 1
modified_user_resource.updatedWhen = FAKE_UPDATE_TIMESTAMP
modified_user_resource.user = create_workspace_owner_user()
response = await client.patch(app.url_path_for(strings.API_UPDATE_USER_RESOURCE, workspace_id=WORKSPACE_ID, service_id=SERVICE_ID, resource_id=USER_RESOURCE_ID), json=user_resource_service_patch, headers={"etag": etag})
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert response.text == 'Attempt to upgrade from 0.1.0 to 2.0.0 denied. major version upgrade is not allowed.'
# [PATCH] /workspaces/{workspace_id}/workspace-services/{service_id}/user-resources/{resource_id}
@ patch("api.routes.workspaces.send_resource_request_message", return_value=sample_resource_operation(resource_id=USER_RESOURCE_ID, operation_id=OPERATION_ID))
@ patch("api.routes.workspaces.ResourceTemplateRepository.get_template_by_name_and_version", return_value=sample_workspace_service())
@ patch("api.routes.workspaces.validate_user_has_valid_role_for_user_resource")
@ patch("api.dependencies.workspaces.WorkspaceServiceRepository.get_workspace_service_by_id", return_value=sample_workspace_service())
@ patch("api.dependencies.workspaces.WorkspaceRepository.get_workspace_by_id", return_value=sample_workspace())
@ patch("api.dependencies.workspaces.UserResourceRepository.get_user_resource_by_id", return_value=sample_user_resource_object())
@ patch("api.routes.workspaces.UserResourceRepository.update_item_with_etag", return_value=sample_user_resource_object())
@ patch("api.routes.workspaces.UserResourceRepository.get_timestamp", return_value=FAKE_UPDATE_TIMESTAMP)
async def test_patch_user_resource_with_upgrade_major_version_and_force_update_returns_patched_user_resource(self, _, update_item_mock, __, ___, ____, _____, ______, _______, app, client):
user_resource_service_patch = {"templateVersion": "2.0.0"}
etag = "some-etag-value"
modified_user_resource = sample_user_resource_object()
modified_user_resource.isEnabled = True
modified_user_resource.history = [ResourceHistoryItem(properties={}, isEnabled=True, resourceVersion=0, updatedWhen=FAKE_CREATE_TIMESTAMP, user=create_workspace_researcher_user(), templateVersion=modified_user_resource.templateVersion)]
modified_user_resource.resourceVersion = 1
modified_user_resource.updatedWhen = FAKE_UPDATE_TIMESTAMP
modified_user_resource.user = create_workspace_owner_user()
modified_user_resource.templateVersion = "2.0.0"
response = await client.patch(app.url_path_for(strings.API_UPDATE_USER_RESOURCE, workspace_id=WORKSPACE_ID, service_id=SERVICE_ID, resource_id=USER_RESOURCE_ID) + "?force_version_update=True", json=user_resource_service_patch, headers={"etag": etag})
update_item_mock.assert_called_once_with(modified_user_resource, etag)
assert response.status_code == status.HTTP_202_ACCEPTED
# [PATCH] /workspaces/{workspace_id}/workspace-services/{service_id}/user-resources/{resource_id}
@ patch("api.routes.workspaces.send_resource_request_message", return_value=sample_resource_operation(resource_id=USER_RESOURCE_ID, operation_id=OPERATION_ID))
@ patch("api.routes.workspaces.ResourceTemplateRepository.get_template_by_name_and_version", return_value=sample_workspace_service())
@ patch("api.routes.workspaces.validate_user_has_valid_role_for_user_resource")
@ patch("api.dependencies.workspaces.WorkspaceServiceRepository.get_workspace_service_by_id", return_value=sample_workspace_service())
@ patch("api.dependencies.workspaces.WorkspaceRepository.get_workspace_by_id", return_value=sample_workspace())
@ patch("api.dependencies.workspaces.UserResourceRepository.get_user_resource_by_id", return_value=sample_user_resource_object())
@ patch("api.routes.workspaces.UserResourceRepository.update_item_with_etag", return_value=sample_user_resource_object())
@ patch("api.routes.workspaces.UserResourceRepository.get_timestamp", return_value=FAKE_UPDATE_TIMESTAMP)
async def test_patch_user_resource_with_downgrade_version_returns_bad_request(self, _, update_item_mock, __, ___, ____, _____, ______, _______, app, client):
user_resource_service_patch = {"templateVersion": "0.0.1"}
etag = "some-etag-value"
modified_user_resource = sample_user_resource_object()
modified_user_resource.isEnabled = True
modified_user_resource.history = [ResourceHistoryItem(properties={}, isEnabled=True, resourceVersion=0, updatedWhen=FAKE_CREATE_TIMESTAMP, user=create_workspace_researcher_user(), templateVersion=modified_user_resource.templateVersion)]
modified_user_resource.resourceVersion = 1
modified_user_resource.updatedWhen = FAKE_UPDATE_TIMESTAMP
modified_user_resource.user = create_workspace_owner_user()
response = await client.patch(app.url_path_for(strings.API_UPDATE_USER_RESOURCE, workspace_id=WORKSPACE_ID, service_id=SERVICE_ID, resource_id=USER_RESOURCE_ID), json=user_resource_service_patch, headers={"etag": etag})
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert response.text == 'Attempt to downgrade from 0.1.0 to 0.0.1 denied. version downgrade is not allowed.'
# [PATCH] /workspaces/{workspace_id}/workspace-services/{service_id}/user-resources/{resource_id}
@ patch("api.routes.workspaces.send_resource_request_message", return_value=sample_resource_operation(resource_id=USER_RESOURCE_ID, operation_id=OPERATION_ID))
@ patch("api.routes.workspaces.ResourceTemplateRepository.get_template_by_name_and_version", return_value=sample_workspace_service())
@ patch("api.routes.workspaces.validate_user_has_valid_role_for_user_resource")
@ patch("api.dependencies.workspaces.WorkspaceServiceRepository.get_workspace_service_by_id", return_value=sample_workspace_service())
@ patch("api.dependencies.workspaces.WorkspaceRepository.get_workspace_by_id", return_value=sample_workspace())
@ patch("api.dependencies.workspaces.UserResourceRepository.get_user_resource_by_id", return_value=sample_user_resource_object())
@ patch("api.routes.workspaces.UserResourceRepository.update_item_with_etag", return_value=sample_user_resource_object())
@ patch("api.routes.workspaces.UserResourceRepository.get_timestamp", return_value=FAKE_UPDATE_TIMESTAMP)
async def test_patch_user_resource_with_upgrade_minor_version_patches_user_resource(self, _, update_item_mock, __, ___, ____, _____, ______, _______, app, client):
user_resource_service_patch = {"templateVersion": "0.2.0"}
etag = "some-etag-value"
modified_user_resource = sample_user_resource_object()
modified_user_resource.isEnabled = True
modified_user_resource.history = [ResourceHistoryItem(properties={}, isEnabled=True, resourceVersion=0, updatedWhen=FAKE_CREATE_TIMESTAMP, user=create_workspace_researcher_user(), templateVersion=modified_user_resource.templateVersion)]
modified_user_resource.resourceVersion = 1
modified_user_resource.updatedWhen = FAKE_UPDATE_TIMESTAMP
modified_user_resource.user = create_workspace_owner_user()
modified_user_resource.templateVersion = "0.2.0"
response = await client.patch(app.url_path_for(strings.API_UPDATE_USER_RESOURCE, workspace_id=WORKSPACE_ID, service_id=SERVICE_ID, resource_id=USER_RESOURCE_ID), json=user_resource_service_patch, headers={"etag": etag})
update_item_mock.assert_called_once_with(modified_user_resource, etag)
assert response.status_code == status.HTTP_202_ACCEPTED
# [PATCH] /workspaces/{workspace_id}/workspace-services/{service_id}/user-resources/{resource_id}
@ patch("api.routes.workspaces.send_resource_request_message", return_value=sample_resource_operation(resource_id=USER_RESOURCE_ID, operation_id=OPERATION_ID))
@ patch("api.routes.workspaces.UserResourceRepository.update_item_with_etag", return_value=sample_user_resource_object())
@ -734,7 +926,7 @@ class TestWorkspaceServiceRoutesThatRequireOwnerRights:
modified_resource = sample_user_resource_object()
modified_resource.isEnabled = False
modified_resource.history = [ResourceHistoryItem(properties={}, isEnabled=True, resourceVersion=0, updatedWhen=FAKE_CREATE_TIMESTAMP, user=create_workspace_researcher_user())]
modified_resource.history = [ResourceHistoryItem(properties={}, isEnabled=True, resourceVersion=0, updatedWhen=FAKE_CREATE_TIMESTAMP, user=create_workspace_researcher_user(), templateVersion=modified_resource.templateVersion)]
modified_resource.resourceVersion = 1
modified_resource.properties["vm_size"] = "large"
modified_resource.updatedWhen = FAKE_UPDATE_TIMESTAMP
@ -812,7 +1004,7 @@ class TestWorkspaceServiceRoutesThatRequireOwnerRights:
modified_workspace_service = sample_workspace_service()
modified_workspace_service.isEnabled = False
modified_workspace_service.history = [ResourceHistoryItem(properties={}, isEnabled=True, resourceVersion=0, updatedWhen=FAKE_CREATE_TIMESTAMP, user=create_workspace_owner_user())]
modified_workspace_service.history = [ResourceHistoryItem(properties={}, isEnabled=True, resourceVersion=0, updatedWhen=FAKE_CREATE_TIMESTAMP, user=create_workspace_owner_user(), templateVersion=modified_workspace_service.templateVersion)]
modified_workspace_service.resourceVersion = 1
modified_workspace_service.user = create_workspace_owner_user()
modified_workspace_service.updatedWhen = FAKE_UPDATE_TIMESTAMP
@ -822,6 +1014,111 @@ class TestWorkspaceServiceRoutesThatRequireOwnerRights:
assert response.status_code == status.HTTP_202_ACCEPTED
# [PATCH] /workspaces/{workspace_id}/services/{service_id}
@ patch("api.routes.workspaces.send_resource_request_message", return_value=sample_resource_operation(resource_id=WORKSPACE_ID, operation_id=OPERATION_ID))
@ patch("api.routes.workspaces.ResourceTemplateRepository.get_template_by_name_and_version", return_value=sample_workspace_service())
@ patch("api.dependencies.workspaces.WorkspaceServiceRepository.get_workspace_service_by_id", return_value=sample_workspace_service())
@ patch("api.dependencies.workspaces.WorkspaceRepository.get_workspace_by_id")
@ patch("api.routes.workspaces.WorkspaceServiceRepository.update_item_with_etag", return_value=sample_workspace_service())
@ patch("api.routes.workspaces.WorkspaceServiceRepository.get_timestamp", return_value=FAKE_UPDATE_TIMESTAMP)
async def test_patch_workspace_service_with_upgrade_major_version_returns_bad_request(self, _, update_item_mock, get_workspace_mock, __, ___, ____, app, client):
auth_info_user_in_workspace_owner_role = {'sp_id': 'ab123', 'roles': {'WorkspaceOwner': 'ab124', 'WorkspaceResearcher': 'ab125'}}
get_workspace_mock.return_value = sample_deployed_workspace(WORKSPACE_ID, auth_info_user_in_workspace_owner_role)
etag = "some-etag-value"
workspace_service_patch = {"templateVersion": "2.0.0"}
modified_workspace_service = sample_workspace_service()
modified_workspace_service.isEnabled = True
modified_workspace_service.history = [ResourceHistoryItem(properties={}, isEnabled=True, resourceVersion=0, updatedWhen=FAKE_CREATE_TIMESTAMP, user=create_workspace_owner_user(), templateVersion=modified_workspace_service.templateVersion)]
modified_workspace_service.resourceVersion = 1
modified_workspace_service.user = create_workspace_owner_user()
modified_workspace_service.updatedWhen = FAKE_UPDATE_TIMESTAMP
response = await client.patch(app.url_path_for(strings.API_UPDATE_WORKSPACE_SERVICE, workspace_id=WORKSPACE_ID, service_id=SERVICE_ID), json=workspace_service_patch, headers={"etag": etag})
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert response.text == 'Attempt to upgrade from 0.1.0 to 2.0.0 denied. major version upgrade is not allowed.'
@ patch("api.routes.workspaces.send_resource_request_message", return_value=sample_resource_operation(resource_id=WORKSPACE_ID, operation_id=OPERATION_ID))
@ patch("api.routes.workspaces.ResourceTemplateRepository.get_template_by_name_and_version", return_value=sample_workspace_service())
@ patch("api.dependencies.workspaces.WorkspaceServiceRepository.get_workspace_service_by_id", return_value=sample_workspace_service())
@ patch("api.dependencies.workspaces.WorkspaceRepository.get_workspace_by_id")
@ patch("api.routes.workspaces.WorkspaceServiceRepository.update_item_with_etag", return_value=sample_workspace_service())
@ patch("api.routes.workspaces.WorkspaceServiceRepository.get_timestamp", return_value=FAKE_UPDATE_TIMESTAMP)
async def test_patch_workspace_service_with_upgrade_major_version_and_force_update_returns_patched_workspace_service(self, _, update_item_mock, get_workspace_mock, __, ___, ____, app, client):
auth_info_user_in_workspace_owner_role = {'sp_id': 'ab123', 'roles': {'WorkspaceOwner': 'ab124', 'WorkspaceResearcher': 'ab125'}}
get_workspace_mock.return_value = sample_deployed_workspace(WORKSPACE_ID, auth_info_user_in_workspace_owner_role)
etag = "some-etag-value"
workspace_service_patch = {"templateVersion": "2.0.0"}
modified_workspace_service = sample_workspace_service()
modified_workspace_service.isEnabled = True
modified_workspace_service.history = [ResourceHistoryItem(properties={}, isEnabled=True, resourceVersion=0, updatedWhen=FAKE_CREATE_TIMESTAMP, user=create_workspace_owner_user(), templateVersion=modified_workspace_service.templateVersion)]
modified_workspace_service.resourceVersion = 1
modified_workspace_service.user = create_workspace_owner_user()
modified_workspace_service.updatedWhen = FAKE_UPDATE_TIMESTAMP
modified_workspace_service.templateVersion = "2.0.0"
response = await client.patch(app.url_path_for(strings.API_UPDATE_WORKSPACE_SERVICE, workspace_id=WORKSPACE_ID, service_id=SERVICE_ID) + "?force_version_update=True", json=workspace_service_patch, headers={"etag": etag})
update_item_mock.assert_called_once_with(modified_workspace_service, etag)
assert response.status_code == status.HTTP_202_ACCEPTED
# [PATCH] /workspaces/{workspace_id}/services/{service_id}
@ patch("api.routes.workspaces.send_resource_request_message", return_value=sample_resource_operation(resource_id=WORKSPACE_ID, operation_id=OPERATION_ID))
@ patch("api.routes.workspaces.ResourceTemplateRepository.get_template_by_name_and_version", return_value=sample_workspace_service())
@ patch("api.dependencies.workspaces.WorkspaceServiceRepository.get_workspace_service_by_id", return_value=sample_workspace_service())
@ patch("api.dependencies.workspaces.WorkspaceRepository.get_workspace_by_id")
@ patch("api.routes.workspaces.WorkspaceServiceRepository.update_item_with_etag", return_value=sample_workspace_service())
@ patch("api.routes.workspaces.WorkspaceServiceRepository.get_timestamp", return_value=FAKE_UPDATE_TIMESTAMP)
async def test_patch_workspace_service_with_downgrade_version_returns_bad_request(self, _, update_item_mock, get_workspace_mock, __, ___, ____, app, client):
auth_info_user_in_workspace_owner_role = {'sp_id': 'ab123', 'roles': {'WorkspaceOwner': 'ab124', 'WorkspaceResearcher': 'ab125'}}
get_workspace_mock.return_value = sample_deployed_workspace(WORKSPACE_ID, auth_info_user_in_workspace_owner_role)
etag = "some-etag-value"
workspace_service_patch = {"templateVersion": "0.0.1"}
modified_workspace_service = sample_workspace_service()
modified_workspace_service.isEnabled = True
modified_workspace_service.history = [ResourceHistoryItem(properties={}, isEnabled=True, resourceVersion=0, updatedWhen=FAKE_CREATE_TIMESTAMP, user=create_workspace_owner_user(), templateVersion=modified_workspace_service.templateVersion)]
modified_workspace_service.resourceVersion = 1
modified_workspace_service.user = create_workspace_owner_user()
modified_workspace_service.updatedWhen = FAKE_UPDATE_TIMESTAMP
response = await client.patch(app.url_path_for(strings.API_UPDATE_WORKSPACE_SERVICE, workspace_id=WORKSPACE_ID, service_id=SERVICE_ID), json=workspace_service_patch, headers={"etag": etag})
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert response.text == 'Attempt to downgrade from 0.1.0 to 0.0.1 denied. version downgrade is not allowed.'
# [PATCH] /workspaces/{workspace_id}/services/{service_id}
@ patch("api.routes.workspaces.send_resource_request_message", return_value=sample_resource_operation(resource_id=WORKSPACE_ID, operation_id=OPERATION_ID))
@ patch("api.routes.workspaces.ResourceTemplateRepository.get_template_by_name_and_version", return_value=sample_workspace_service())
@ patch("api.dependencies.workspaces.WorkspaceServiceRepository.get_workspace_service_by_id", return_value=sample_workspace_service())
@ patch("api.dependencies.workspaces.WorkspaceRepository.get_workspace_by_id")
@ patch("api.routes.workspaces.WorkspaceServiceRepository.update_item_with_etag", return_value=sample_workspace_service())
@ patch("api.routes.workspaces.WorkspaceServiceRepository.get_timestamp", return_value=FAKE_UPDATE_TIMESTAMP)
async def test_patch_workspace_service_with_upgrade_minor_version_patches_workspace(self, _, update_item_mock, get_workspace_mock, __, ___, ____, app, client):
auth_info_user_in_workspace_owner_role = {'sp_id': 'ab123', 'roles': {'WorkspaceOwner': 'ab124', 'WorkspaceResearcher': 'ab125'}}
get_workspace_mock.return_value = sample_deployed_workspace(WORKSPACE_ID, auth_info_user_in_workspace_owner_role)
etag = "some-etag-value"
workspace_service_patch = {"templateVersion": "0.2.0"}
modified_workspace_service = sample_workspace_service()
modified_workspace_service.isEnabled = True
modified_workspace_service.history = [ResourceHistoryItem(properties={}, isEnabled=True, resourceVersion=0, updatedWhen=FAKE_CREATE_TIMESTAMP, user=create_workspace_owner_user(), templateVersion=modified_workspace_service.templateVersion)]
modified_workspace_service.resourceVersion = 1
modified_workspace_service.user = create_workspace_owner_user()
modified_workspace_service.updatedWhen = FAKE_UPDATE_TIMESTAMP
modified_workspace_service.templateVersion = "0.2.0"
response = await client.patch(app.url_path_for(strings.API_UPDATE_WORKSPACE_SERVICE, workspace_id=WORKSPACE_ID, service_id=SERVICE_ID), json=workspace_service_patch, headers={"etag": etag})
update_item_mock.assert_called_once_with(modified_workspace_service, etag)
assert response.status_code == status.HTTP_202_ACCEPTED
class TestWorkspaceServiceRoutesThatRequireOwnerOrResearcherRights:
@pytest.fixture(autouse=True, scope='class')
@ -1066,7 +1363,7 @@ class TestWorkspaceServiceRoutesThatRequireOwnerOrResearcherRights:
modified_user_resource = sample_user_resource_object()
modified_user_resource.isEnabled = False
modified_user_resource.history = [ResourceHistoryItem(properties={}, isEnabled=True, resourceVersion=0, updatedWhen=FAKE_CREATE_TIMESTAMP, user=create_workspace_researcher_user())]
modified_user_resource.history = [ResourceHistoryItem(properties={}, isEnabled=True, resourceVersion=0, updatedWhen=FAKE_CREATE_TIMESTAMP, user=create_workspace_researcher_user(), templateVersion=modified_user_resource.templateVersion)]
modified_user_resource.resourceVersion = 1
modified_user_resource.updatedWhen = FAKE_UPDATE_TIMESTAMP
modified_user_resource.user = create_workspace_researcher_user()

Просмотреть файл

@ -330,7 +330,8 @@ def test_patch_resource_preserves_property_history(_, __, resource_repo):
resourceVersion=0,
updatedWhen=FAKE_CREATE_TIMESTAMP,
properties={'display_name': 'initial display name', 'description': 'initial description', 'computed_prop': 'computed_val'},
user=user)]
user=user,
templateVersion=resource.templateVersion)]
expected_resource.properties['display_name'] = 'updated name'
expected_resource.resourceVersion = 1
expected_resource.user = user
@ -348,7 +349,8 @@ def test_patch_resource_preserves_property_history(_, __, resource_repo):
resourceVersion=1,
updatedWhen=FAKE_UPDATE_TIMESTAMP,
properties={'display_name': 'updated name', 'description': 'initial description', 'computed_prop': 'computed_val'},
user=user
user=user,
templateVersion=resource.templateVersion
)
)

Двоичные данные
docs/assets/swagger_force_version_update.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 48 KiB

Просмотреть файл

@ -0,0 +1,55 @@
<!-- markdownlint-disable-file MD046 -->
# Upgrading Resources Version
Azure TRE workspaces, workspace services, workspace shared services, and user resources are [Porter](https://porter.sh/) bundles. Porter bundles are based on [Cloud Native Application Bundles (CNAB)](https://cnab.io/).
When a new bundle version becomes available, users can upgrade their resources to a newer version after building, publishing and registering the bundle template.
Upgrades (and downgrades) are based on [CNAB bundle upgrade action](https://getporter.org/bundle/manifest/#bundle-actions).
Bundle template versions follow [semantic versioning rules](../tre-workspace-authors/authoring-workspace-templates.md#versioning).
!!! Note
Only minor and patch version upgrades are automatically allowed within the Azure TRE upgrade mechanism. Major versions upgrades and any version downgrades are blocked as they are assumed to contain breaking changes or changes that require additional consideration.
For users who wish to upgrade a major version, we highly recommend to read the changelog, review what has changed and take some appropriate action before upgrading using [force version update](#force-version-update).
## How to upgrade a resource using Swagger UI
Resources can be upgrade using Swagger UI, in the following example we show how to upgrade a workspace version from 1.0.0 to 1.0.1, other resources upgrades are similar.
1. First make sure the desired template version is registered, [follow these steps if not](../tre-admins/registering-templates.md).
1. Navigate to the Swagger UI at `/api/docs`.
1. Log into the Swagger UI using `Authorize`.
1. Click `Try it out` on the `GET` `/api/workspace/{workspace_id}` operation.
1. Provide your `workspace_id` in the parameters section and click `Execute`.
1. Copy the `_etag` property from the response body.
1. Click `Try it out` on the `PATCH` `/api/workspace/{workspace_id}` operation.
1. Provide your `workspace_id` and `_etag` parameters which you've just copied.
1. Provide the following payload with the desired version in the `Request body` parameter and click `Execute`.
```json
{
"templateVersion": "1.0.1",
}
```
1. Review server response, it should include a new `operation` document with `upgrade` as an `action` and `updating` as `status` for upgrading the workspace and a message states that the Job is starting.
1. Once the upgrade is complete another operation will be created and can be viewed by executing `GET` `/api/workspace/{workspace_id}/operations`, review it and make sure its `status` is `updated`.
### Force version update
If you wish to upgrade a major version, or downgrade to any version, you can override the blocking in the upgrade mechanism by passing `force_version_update=true` query parameter to the resource `Patch` action.
For example force version patching a workspace:
![Force version update](../assets/swagger_force_version_update.png)

Просмотреть файл

@ -1,8 +1,8 @@
# Authoring workspaces templates
# Authoring templates
Azure TRE workspaces, workspace services, and user resources are [Porter](https://porter.sh/) bundles. Porter bundles are based on [Cloud Native Application Bundles (CNAB)](https://cnab.io/).
Azure TRE workspaces, workspace services, shared services, and user resources are [Porter](https://porter.sh/) bundles. Porter bundles are based on [Cloud Native Application Bundles (CNAB)](https://cnab.io/).
Workspace authors are free to choose the technology stack for provisioning resources (e.g., ARM templates, Terraform etc.), but the Azure TRE framework sets certain requirements for the bundle manifests, which specify the credentials, input and output parameters, deployment actions among other things.
Authors are free to choose the technology stack for provisioning resources (e.g., ARM templates, Terraform etc.), but the Azure TRE framework sets certain requirements for the bundle manifests, which specify the credentials, input and output parameters, deployment actions among other things.
This document describes the requirements, and the process to author a template.
@ -120,7 +120,16 @@ Templates authors need to make sure that underling Azure resources are tagged wi
Workspace versions are the bundle versions specified in [the metadata](https://porter.sh/author-bundles/#bundle-metadata). The bundle versions should match the image tags in the container registry (see [Publishing workspace bundle](#publishing-workspace-bundle)).
TRE does not provide means to update an existing workspace to a newer version. Instead, the user has to first uninstall the old version and then install the new one. The CNAB **upgrade** or a Porter custom ("`update`") action may be used in the future version of TRE to do this automatically.
Bundle versions should follow [Semantic Versioning](https://semver.org/), given a version number **MAJOR.MINOR.PATCH**, increment the:
1. **MAJOR** version when you make a breaking change, potential data loss, changes that don't easily/automatically upgrade, or significant changes which require someone to review what has changed and take some appropriate action, or functionality of the component has significantly changed and users might need training.
2. **MINOR** version when you add minor functionality which can be automatically upgraded.
3. **PATCH** version when you make backward-compatible bug or typo fixes.
For resource version upgrades see [Upgrading Resources Version](../tre-admins/upgrading-resources.md).
## Publishing workspace bundle

Просмотреть файл

@ -125,7 +125,8 @@ nav:
- Install Resources via API:
- Install Base Workspace: tre-admins/setup-instructions/installing-base-workspace.md
- Install Workspace Service and User Resource: tre-admins/setup-instructions/installing-workspace-service-and-user-resource.md
- Upgrading AzureTRE version: tre-admins/upgrading-tre.md
- Upgrading AzureTRE Version: tre-admins/upgrading-tre.md
- Upgrading Resources Version: tre-admins/upgrading-resources.md
- Configuring Airlock Reviews: tre-admins/configure-airlock-review.md
- Development: # Docs related to the developing code for the AzureTRE