Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support resource templateVersion update #2908

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
6351172
support templateVersion when patching resources
guybartal Oct 1, 2022
33f08d6
suppport resources templateVersion patching
guybartal Oct 1, 2022
e6f3e9c
Revert "suppport resources templateVersion patching"
guybartal Oct 1, 2022
e5c5d65
Merge branch 'main' into guybartal/support-template-version-patching
guybartal Oct 1, 2022
7efa18d
bump api version
guybartal Oct 1, 2022
bd3488b
Merge branch 'main' into guybartal/support-template-version-patching
guybartal Nov 22, 2022
124b77d
fix UT
guybartal Nov 22, 2022
35f04bd
resource update mechanism
guybartal Nov 24, 2022
1ea672d
force version update + shared services unit tests
guybartal Nov 24, 2022
5b6fb80
Merge branch 'main' into guybartal/support-template-version-patching
guybartal Nov 24, 2022
439ac50
bump api version
guybartal Nov 24, 2022
58724a6
update PR template
guybartal Nov 24, 2022
1c3551c
Major rule rephrased
guybartal Nov 24, 2022
e02f6e3
Merge branch 'main' into guybartal/support-template-version-patching
guybartal Nov 24, 2022
8378d40
bump api version
guybartal Nov 24, 2022
3deff17
Update api_app/_version.py
guybartal Nov 28, 2022
8db059b
Update docs/tre-workspace-authors/authoring-workspace-templates.md
guybartal Nov 28, 2022
c4661ce
Update docs/tre-workspace-authors/authoring-workspace-templates.md
guybartal Nov 28, 2022
9109aaf
Update docs/tre-workspace-authors/authoring-workspace-templates.md
guybartal Nov 28, 2022
bd8a51b
Merge branch 'main' into guybartal/support-template-version-patching
guybartal Nov 28, 2022
feda19c
disable linter rule MD046 inline
guybartal Nov 28, 2022
8525fdc
linter issues
guybartal Nov 28, 2022
c3d9a0d
linter
guybartal Nov 28, 2022
215fe45
remove logging
guybartal Nov 28, 2022
d4250b8
move forceVersionUpdate fiield to query string
guybartal Nov 28, 2022
65b1243
linter errors
guybartal Nov 28, 2022
e5b1836
fix unit test
guybartal Nov 28, 2022
d483f4b
PR review comments - rephrasing and typo fixes
guybartal Nov 29, 2022
aabffbe
replace semver lib with existing semantic-version
guybartal Nov 29, 2022
da1afef
Merge branch 'main' into guybartal/support-template-version-patching
guybartal Nov 29, 2022
58fa9ac
shorten pr template + add changelog
guybartal Nov 29, 2022
e09cef7
Merge branch 'guybartal/support-template-version-patching' of github.…
guybartal Nov 29, 2022
b9b0c37
Merge branch 'main' into guybartal/support-template-version-patching
guybartal Nov 30, 2022
23ffbd9
Merge branch 'main' into guybartal/support-template-version-patching
tamirkamara Dec 1, 2022
5829f16
Merge branch 'main' into guybartal/support-template-version-patching
guybartal Dec 4, 2022
0626ddd
Merge branch 'main' into guybartal/support-template-version-patching
guybartal Dec 4, 2022
2b0f1df
Merge branch 'main' into guybartal/support-template-version-patching
tamirkamara Dec 4, 2022
fde4d3e
Merge branch 'main' into guybartal/support-template-version-patching
tamirkamara Dec 4, 2022
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
1 change: 1 addition & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
2 changes: 1 addition & 1 deletion api_app/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.5.23"
__version__ = "0.6.0"
8 changes: 5 additions & 3 deletions api_app/api/routes/shared_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
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
Expand Down Expand Up @@ -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,
Expand All @@ -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)])
Expand Down
21 changes: 14 additions & 7 deletions api_app/api/routes/workspaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
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
Expand Down Expand Up @@ -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,
Expand All @@ -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)])
Expand Down Expand Up @@ -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,
Expand All @@ -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)])
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down
12 changes: 12 additions & 0 deletions api_app/db/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
33 changes: 30 additions & 3 deletions api_app/db/repositories/resources.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -96,15 +97,16 @@ def validate_input_against_template(self, template_name: str, resource_input, re

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(
isEnabled=resource_copy.isEnabled,
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)

Expand All @@ -116,6 +118,10 @@ def patch_resource(self, resource: Resource, resource_patch: ResourcePatch, reso
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)

Expand All @@ -125,6 +131,27 @@ def patch_resource(self, resource: Resource, resource_patch: ResourcePatch, reso
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)
Expand Down
4 changes: 2 additions & 2 deletions api_app/db/repositories/shared_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ def create_shared_service_item(self, shared_service_input: SharedServiceTemplate

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)
4 changes: 2 additions & 2 deletions api_app/db/repositories/user_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def get_user_resource_by_id(self, workspace_id: str, service_id: str, resource_i
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)
4 changes: 2 additions & 2 deletions api_app/db/repositories/workspace_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def create_workspace_service_item(self, workspace_service_input: WorkspaceServic

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