diff --git a/CHANGELOG.md b/CHANGELOG.md index f99328b247..cf0f58684e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,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)) +* Allow numeric CIDR masks in `address_space_size` (e.g. "25") when requesting auto-assigned address spaces; accepts numeric strings and validates the mask range. ([#4733](https://github.com/microsoft/AzureTRE/issues/4733)) * Add Windows Server 2025 image support to Guacamole. ([#4890](https://github.com/microsoft/AzureTRE/issues/4890)) ## (0.28.0) (March 2, 2026) diff --git a/api_app/_version.py b/api_app/_version.py index 7923a95d33..57a322dfc2 100644 --- a/api_app/_version.py +++ b/api_app/_version.py @@ -1 +1 @@ -__version__ = "0.25.16" +__version__ = "0.25.17" diff --git a/api_app/db/repositories/workspaces.py b/api_app/db/repositories/workspaces.py index 013c1d54ae..26db14d337 100644 --- a/api_app/db/repositories/workspaces.py +++ b/api_app/db/repositories/workspaces.py @@ -140,16 +140,28 @@ def automatically_create_application_registration(self, workspace_properties: di async def get_address_space_based_on_size(self, workspace_properties: dict): # Default the address space to 'small' if not supplied. - address_space_size = workspace_properties.get("address_space_size", "small").lower() + address_space_size = workspace_properties.get("address_space_size", "small").strip().lower() # 773 allow custom sized networks to be requested - if (address_space_size == "custom"): + if address_space_size == "custom": if (await self.validate_address_space(workspace_properties.get("address_space"))): return workspace_properties.get("address_space") else: raise InvalidInput("The custom 'address_space' you requested does not fit in the current network.") - # Default mask is 24 (small) + # If a numeric cidr was provided (e.g. as a string like "25"), accept it + try: + if address_space_size.isdigit(): + cidr_netmask = int(address_space_size) + # basic validation for reasonable CIDR mask values + if cidr_netmask < 16 or cidr_netmask > 29: + raise InvalidInput("'address_space_size' numeric value must be between 16 and 29") + return await self.get_new_address_space(cidr_netmask) + except ValueError: + # fall through to predefined handling + pass + + # Default mask is 24 (small). Keep backwards compatibility with presets. cidr_netmask = WorkspaceRepository.predefined_address_spaces.get(address_space_size, 24) return await self.get_new_address_space(cidr_netmask) diff --git a/api_app/models/schemas/workspace_template.py b/api_app/models/schemas/workspace_template.py index bc20955217..9d68a3f1fd 100644 --- a/api_app/models/schemas/workspace_template.py +++ b/api_app/models/schemas/workspace_template.py @@ -22,7 +22,7 @@ def get_sample_workspace_template_object(template_name: str = "tre-workspace-bas "address_space_size": Property( type="string", default="small", - description="This can have a value of small, medium, large or custom. If you specify custom, then you need to specify a VNet address space in 'address_space' (e.g. 10.2.1.0/24)") + description="This can have a value of small, medium, large, a numeric CIDR mask (e.g. \"25\") or custom. If you specify custom, then you need to specify a VNet address space in 'address_space' (e.g. 10.2.1.0/24)") }, customActions=[ CustomAction() @@ -73,7 +73,7 @@ class Config: "address_space_size": { "type": "string", "title": "Address space size", - "description": "Network address size (small, medium, large or custom) to be used by the workspace" + "description": "This can have a value of small, medium, large, a numeric CIDR mask (e.g. \"25\") or custom. If you specify custom, then you need to specify a VNet address space in 'address_space' (e.g. 10.2.1.0/24)" }, "address_space": { "type": "string", diff --git a/api_app/tests_ma/test_db/test_repositories/test_workpaces_repository.py b/api_app/tests_ma/test_db/test_repositories/test_workpaces_repository.py index e71c98cd12..474ab4f3f5 100644 --- a/api_app/tests_ma/test_db/test_repositories/test_workpaces_repository.py +++ b/api_app/tests_ma/test_db/test_repositories/test_workpaces_repository.py @@ -342,3 +342,43 @@ async def test_is_workspace_storage_account_available_when_name_not_available(mo mock_storage_client.return_value.storage_accounts.check_name_availability.assert_called_once_with({"name": f"stgws{workspace_id[-4:]}"}) assert result is False + + +@pytest.mark.asyncio +@patch('core.config.RESOURCE_LOCATION', "useast2") +@patch('core.config.TRE_ID', "9876") +@patch('core.config.CORE_ADDRESS_SPACE', "10.1.0.0/22") +@patch('core.config.TRE_ADDRESS_SPACE', "10.0.0.0/12") +async def test_get_address_space_based_on_size_with_string_19(workspace_repo, basic_workspace_request): + workspace_to_create = basic_workspace_request + # request a /19 + workspace_to_create.properties["address_space_size"] = "19" + address_space = await workspace_repo.get_address_space_based_on_size(workspace_to_create.properties) + assert address_space.endswith('/19') + + +@pytest.mark.asyncio +@patch('core.config.RESOURCE_LOCATION', "useast2") +@patch('core.config.TRE_ID', "9876") +@patch('core.config.CORE_ADDRESS_SPACE', "10.1.0.0/22") +@patch('core.config.TRE_ADDRESS_SPACE', "10.0.0.0/12") +async def test_get_address_space_based_on_size_with_string_29(workspace_repo, basic_workspace_request): + workspace_to_create = basic_workspace_request + # request a /29 + workspace_to_create.properties["address_space_size"] = "29" + address_space = await workspace_repo.get_address_space_based_on_size(workspace_to_create.properties) + assert address_space.endswith('/29') + + +@pytest.mark.asyncio +@patch('core.config.RESOURCE_LOCATION', "useast2") +@patch('core.config.TRE_ID', "9876") +@patch('core.config.CORE_ADDRESS_SPACE', "10.1.0.0/22") +@patch('core.config.TRE_ADDRESS_SPACE', "10.0.0.0/12") +@pytest.mark.parametrize("invalid_size", ["15", "30"]) +async def test_get_address_space_based_on_size_with_invalid_string_raises_error(workspace_repo, basic_workspace_request, invalid_size): + workspace_to_create = basic_workspace_request + workspace_to_create.properties["address_space_size"] = invalid_size + with pytest.raises(InvalidInput) as ex: + await workspace_repo.get_address_space_based_on_size(workspace_to_create.properties) + assert str(ex.value) == "'address_space_size' numeric value must be between 16 and 29" diff --git a/docs/tre-workspace-authors/authoring-workspace-templates.md b/docs/tre-workspace-authors/authoring-workspace-templates.md index 163a2358bf..40b43af65a 100644 --- a/docs/tre-workspace-authors/authoring-workspace-templates.md +++ b/docs/tre-workspace-authors/authoring-workspace-templates.md @@ -110,6 +110,7 @@ Some workspace services may require additional address spaces to be provisioned. To request an additional address space, the workspace service bundle must define an `address_space` parameter in the `porter.yaml` file. The value of this parameter will be provided by API to the resource processor. The size of the `address_space` will default to `/24`, however other sizes can be requested by including an `address_space_size` as part of the workspace service template. +This parameter accepts the presets `small` (/24), `medium` (/22), `large` (/16), the literal value `custom` together with an explicit `address_space` CIDR (e.g. `10.2.1.0/25`), or a numeric CIDR mask as a string from "16" to "29" (e.g. `"25"`) to ask the system to auto-select an available `/25`. The `address_space` allocation will only take place during the install phase of a deployment, as this is a breaking change to your template you should increment the major version of your template, this means a you must deploy a new resource instead of upgrading an existing one.