diff --git a/CHANGELOG.md b/CHANGELOG.md index 1284b51429..48e6e02daa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ENHANCEMENTS: * Specify default_outbound_access_enabled = false setting for all subnets ([#4757](https://github.com/microsoft/AzureTRE/pull/4757)) * Pin all GitHub Actions workflow steps to full commit SHAs to prevent supply chain attacks plus update to latest releases ([#4886](https://github.com/microsoft/AzureTRE/pull/4886)) +* Pass `isEnabled` in the Service Bus resource request message payload and expose it as the `is_enabled` Porter parameter, enabling bundle authors to stop or deallocate Azure resources on disable ([#4889](https://github.com/microsoft/AzureTRE/issues/4889)) ## (0.28.0) (March 2, 2026) **BREAKING CHANGES** diff --git a/api_app/_version.py b/api_app/_version.py index 2cb28789f2..7923a95d33 100644 --- a/api_app/_version.py +++ b/api_app/_version.py @@ -1 +1 @@ -__version__ = "0.25.15" +__version__ = "0.25.16" diff --git a/api_app/models/domain/resource.py b/api_app/models/domain/resource.py index 1e660059ba..9c4b1c1b01 100644 --- a/api_app/models/domain/resource.py +++ b/api_app/models/domain/resource.py @@ -61,7 +61,8 @@ def get_resource_request_message_payload(self, operation_id: str, step_id: str, "id": self.id, "name": self.templateName, "version": self.templateVersion, - "parameters": self.properties + "parameters": self.properties, + "isEnabled": self.isEnabled } if self.resourceType == ResourceType.WorkspaceService: diff --git a/api_app/tests_ma/test_models/test_resource.py b/api_app/tests_ma/test_models/test_resource.py index cde5b7f764..fdbf45a141 100644 --- a/api_app/tests_ma/test_models/test_resource.py +++ b/api_app/tests_ma/test_models/test_resource.py @@ -43,3 +43,12 @@ def test_workspace_service_get_resource_request_message_payload_augments_payload message_payload = workspace_service.get_resource_request_message_payload(OPERATION_ID, STEP_ID, RequestAction.Install) assert message_payload["workspaceId"] == workspace_id + + +@pytest.mark.parametrize('is_enabled', [True, False]) +def test_get_resource_request_message_payload_includes_is_enabled(is_enabled): + resource = Resource(templateName="", templateVersion="", isEnabled=is_enabled, etag="", properties={}, id="1234", resourceType=ResourceType.Workspace, resourcePath="test") + + message_payload = resource.get_resource_request_message_payload(OPERATION_ID, STEP_ID, RequestAction.Install) + + assert message_payload["isEnabled"] == is_enabled diff --git a/docs/tre-workspace-authors/authoring-workspace-templates.md b/docs/tre-workspace-authors/authoring-workspace-templates.md index 163a2358bf..a4113363dd 100644 --- a/docs/tre-workspace-authors/authoring-workspace-templates.md +++ b/docs/tre-workspace-authors/authoring-workspace-templates.md @@ -92,6 +92,22 @@ The required actions are the main two of CNAB spec: * `install` - Deploys/repairs the workspace Azure resources, and must be **idempotent** * `uninstall` - Tears down (deletes) the Azure resources of the workspace and its services +## Handling resource enable/disable + +When a resource is enabled or disabled via the TRE API, the framework runs the `upgrade` action on the Porter bundle and passes the current value of `isEnabled` as the `is_enabled` parameter. Bundle authors can use this parameter to stop, deallocate, or remove Azure resources when a resource is disabled, and to restore them when re-enabled. + +To opt in, declare the `is_enabled` parameter in your bundle's `porter.yaml`: + +```yaml +parameters: + - name: is_enabled + type: boolean + default: true + description: "Indicates whether the resource is enabled. When false, cost-incurring Azure resources should be stopped or deleted." +``` + +Then reference it in your `upgrade` action and pass it through to Terraform (or any other provisioner) to handle lifecycle accordingly. It is up to the bundle author to decide the exact behaviour — for example, a VM bundle might delete the VM (but keep its disks) when `is_enabled` is `false`. + ## Workspace service bundle manifests Workspace service bundles are generated in the same way as workspace bundles. diff --git a/resource_processor/_version.py b/resource_processor/_version.py index 26c36ca79a..f2b93b13de 100644 --- a/resource_processor/_version.py +++ b/resource_processor/_version.py @@ -1 +1 @@ -__version__ = "0.13.3" +__version__ = "0.13.4" diff --git a/resource_processor/helpers/commands.py b/resource_processor/helpers/commands.py index 35047d309a..330e918692 100644 --- a/resource_processor/helpers/commands.py +++ b/resource_processor/helpers/commands.py @@ -207,6 +207,8 @@ def get_special_porter_param_value(config, parameter_name: str, msg_body): return msg_body.get("parentWorkspaceServiceId") # not included in all messages if parameter_name == "owner_id": return msg_body.get("ownerId") # not included in all messages + if parameter_name == "is_enabled": + return msg_body.get("isEnabled") if (value := config["bundle_params"].get(parameter_name.lower())) is not None: return value # Parameters that relate to the cloud type diff --git a/resource_processor/tests_rp/test_commands.py b/resource_processor/tests_rp/test_commands.py index 2caab5c0bd..3db16c1670 100644 --- a/resource_processor/tests_rp/test_commands.py +++ b/resource_processor/tests_rp/test_commands.py @@ -299,7 +299,8 @@ def test_get_special_porter_param_value(): msg_body = { "workspaceId": "ws-123", "parentWorkspaceServiceId": "parent-123", - "ownerId": "owner-123" + "ownerId": "owner-123", + "isEnabled": False } # Test ACR name extraction @@ -320,6 +321,9 @@ def test_get_special_porter_param_value(): # Test owner ID assert get_special_porter_param_value(config, "owner_id", msg_body) == "owner-123" + # Test is_enabled + assert get_special_porter_param_value(config, "is_enabled", msg_body) is False + # Test bundle params assert get_special_porter_param_value(config, "custom_param", msg_body) == "custom_value"