diff --git a/data_safe_haven/pulumi/common/__init__.py b/data_safe_haven/pulumi/common/__init__.py index e69de29bb2..fa5e1a5a32 100644 --- a/data_safe_haven/pulumi/common/__init__.py +++ b/data_safe_haven/pulumi/common/__init__.py @@ -0,0 +1,23 @@ +from .enums import NetworkingPriorities +from .transformations import ( + get_available_ips_from_subnet, + get_id_from_rg, + get_id_from_subnet, + get_ip_address_from_container_group, + get_ip_addresses_from_private_endpoint, + get_name_from_rg, + get_name_from_subnet, + get_name_from_vnet, +) + +__all__ = [ + "get_available_ips_from_subnet", + "get_id_from_rg", + "get_id_from_subnet", + "get_ip_address_from_container_group", + "get_ip_addresses_from_private_endpoint", + "get_name_from_rg", + "get_name_from_subnet", + "get_name_from_vnet", + "NetworkingPriorities", +] diff --git a/data_safe_haven/pulumi/components/automation_dsc_node.py b/data_safe_haven/pulumi/components/automation_dsc_node.py index e487c61285..7acffe1ce0 100644 --- a/data_safe_haven/pulumi/components/automation_dsc_node.py +++ b/data_safe_haven/pulumi/components/automation_dsc_node.py @@ -55,7 +55,7 @@ def __init__( opts: ResourceOptions | None = None, ) -> None: super().__init__("dsh:common:AutomationDscNode", name, {}, opts) - child_opts = ResourceOptions.merge(ResourceOptions(parent=self), opts) + child_opts = ResourceOptions.merge(opts, ResourceOptions(parent=self)) # Upload the primary domain controller DSC dsc = automation.DscConfiguration( @@ -77,10 +77,10 @@ def __init__( ), ), opts=ResourceOptions.merge( + child_opts, ResourceOptions( delete_before_replace=True, replace_on_changes=["source.hash"] ), - child_opts, ), ) dsc_compiled = CompiledDsc( @@ -97,7 +97,7 @@ def __init__( required_modules=props.dsc_required_modules, subscription_name=props.subscription_name, ), - opts=ResourceOptions.merge(ResourceOptions(depends_on=[dsc]), child_opts), + opts=ResourceOptions.merge(child_opts, ResourceOptions(depends_on=[dsc])), ) compute.VirtualMachineExtension( f"{self._name}_dsc_extension", @@ -130,7 +130,7 @@ def __init__( vm_name=props.vm_name, vm_extension_name="Microsoft.Powershell.DSC", opts=ResourceOptions.merge( - ResourceOptions(depends_on=[dsc_compiled]), child_opts, + ResourceOptions(depends_on=[dsc_compiled]), ), ) diff --git a/data_safe_haven/pulumi/components/shm_bastion.py b/data_safe_haven/pulumi/components/shm_bastion.py index b63b6c44f2..fc627b77df 100644 --- a/data_safe_haven/pulumi/components/shm_bastion.py +++ b/data_safe_haven/pulumi/components/shm_bastion.py @@ -29,7 +29,7 @@ def __init__( opts: ResourceOptions | None = None, ) -> None: super().__init__("dsh:shm:BastionComponent", name, {}, opts) - child_opts = ResourceOptions.merge(ResourceOptions(parent=self), opts) + child_opts = ResourceOptions.merge(opts, ResourceOptions(parent=self)) # Deploy IP address public_ip = network.PublicIPAddress( diff --git a/data_safe_haven/pulumi/components/shm_data.py b/data_safe_haven/pulumi/components/shm_data.py index fb04442a6c..dc617e4549 100644 --- a/data_safe_haven/pulumi/components/shm_data.py +++ b/data_safe_haven/pulumi/components/shm_data.py @@ -54,7 +54,7 @@ def __init__( opts: ResourceOptions | None = None, ) -> None: super().__init__("dsh:shm:DataComponent", name, {}, opts) - child_opts = ResourceOptions.merge(ResourceOptions(parent=self), opts) + child_opts = ResourceOptions.merge(opts, ResourceOptions(parent=self)) # Deploy resource group resource_group = resources.ResourceGroup( diff --git a/data_safe_haven/pulumi/components/shm_domain_controllers.py b/data_safe_haven/pulumi/components/shm_domain_controllers.py index 68738787e0..6926c41e3f 100644 --- a/data_safe_haven/pulumi/components/shm_domain_controllers.py +++ b/data_safe_haven/pulumi/components/shm_domain_controllers.py @@ -5,7 +5,7 @@ from pulumi import ComponentResource, Input, Output, ResourceOptions from pulumi_azure_native import network, resources -from data_safe_haven.pulumi.common.transformations import get_name_from_subnet +from data_safe_haven.pulumi.common import get_name_from_subnet from data_safe_haven.pulumi.dynamic.remote_powershell import ( RemoteScript, RemoteScriptProps, @@ -87,7 +87,7 @@ def __init__( opts: ResourceOptions | None = None, ) -> None: super().__init__("dsh:shm:DomainControllersComponent", name, {}, opts) - child_opts = ResourceOptions.merge(ResourceOptions(parent=self), opts) + child_opts = ResourceOptions.merge(opts, ResourceOptions(parent=self)) resources_path = pathlib.Path(__file__).parent.parent.parent / "resources" # Deploy resource group @@ -157,8 +157,8 @@ def __init__( vm_resource_group_name=resource_group.name, ), opts=ResourceOptions.merge( - ResourceOptions(depends_on=[primary_domain_controller]), child_opts, + ResourceOptions(depends_on=[primary_domain_controller]), ), ) # Extract the domain SID @@ -177,13 +177,13 @@ def __init__( vm_resource_group_name=resource_group.name, ), opts=ResourceOptions.merge( + child_opts, ResourceOptions( depends_on=[ primary_domain_controller, primary_domain_controller_dsc_node, ] ), - child_opts, ), ) diff --git a/data_safe_haven/pulumi/components/shm_firewall.py b/data_safe_haven/pulumi/components/shm_firewall.py index 5ce5b78c4e..f610a86a74 100644 --- a/data_safe_haven/pulumi/components/shm_firewall.py +++ b/data_safe_haven/pulumi/components/shm_firewall.py @@ -2,7 +2,7 @@ from pulumi import ComponentResource, Input, Output, ResourceOptions from pulumi_azure_native import network -from data_safe_haven.pulumi.common.transformations import get_id_from_subnet +from data_safe_haven.pulumi.common import get_id_from_subnet class SHMFirewallProps: @@ -46,7 +46,7 @@ def __init__( opts: ResourceOptions | None = None, ) -> None: super().__init__("dsh:shm:FirewallComponent", name, {}, opts) - child_opts = ResourceOptions.merge(ResourceOptions(parent=self), opts) + child_opts = ResourceOptions.merge(opts, ResourceOptions(parent=self)) # Important IP addresses # https://docs.microsoft.com/en-us/azure/virtual-network/what-is-ip-address-168-63-129-16 diff --git a/data_safe_haven/pulumi/components/shm_monitoring.py b/data_safe_haven/pulumi/components/shm_monitoring.py index 037ab425d0..82cecddf77 100644 --- a/data_safe_haven/pulumi/components/shm_monitoring.py +++ b/data_safe_haven/pulumi/components/shm_monitoring.py @@ -14,7 +14,7 @@ replace_separators, time_as_string, ) -from data_safe_haven.pulumi.common.transformations import get_id_from_subnet +from data_safe_haven.pulumi.common import get_id_from_subnet class SHMMonitoringProps: @@ -48,7 +48,7 @@ def __init__( opts: ResourceOptions | None = None, ) -> None: super().__init__("dsh:shm:MonitoringComponent", name, {}, opts) - child_opts = ResourceOptions.merge(ResourceOptions(parent=self), opts) + child_opts = ResourceOptions.merge(opts, ResourceOptions(parent=self)) # Deploy resource group resource_group = resources.ResourceGroup( @@ -285,8 +285,8 @@ def __init__( ), ), opts=ResourceOptions.merge( - ResourceOptions(ignore_changes=["schedule_info"]), child_opts, + ResourceOptions(ignore_changes=["schedule_info"]), ), ) # Create Windows VM system update schedule: daily at 02:02 @@ -333,13 +333,13 @@ def __init__( ), ), opts=ResourceOptions.merge( + child_opts, ResourceOptions( ignore_changes=[ "schedule_info", # options are added after deployment "updateConfiguration.windows.included_package_classifications", # ordering might change ] ), - child_opts, ), ) # Create Linux VM system update schedule: daily at 02:02 @@ -383,13 +383,13 @@ def __init__( ), ), opts=ResourceOptions.merge( + child_opts, ResourceOptions( ignore_changes=[ "schedule_info", # options are added after deployment "updateConfiguration.linux.included_package_classifications", # ordering might change ] ), - child_opts, ), ) diff --git a/data_safe_haven/pulumi/components/shm_networking.py b/data_safe_haven/pulumi/components/shm_networking.py index 7c2b7d224a..837588444c 100644 --- a/data_safe_haven/pulumi/components/shm_networking.py +++ b/data_safe_haven/pulumi/components/shm_networking.py @@ -6,7 +6,7 @@ from data_safe_haven.external import AzureIPv4Range from data_safe_haven.functions import ordered_private_dns_zones -from data_safe_haven.pulumi.common.enums import NetworkingPriorities +from data_safe_haven.pulumi.common import NetworkingPriorities class SHMNetworkingProps: @@ -47,7 +47,7 @@ def __init__( opts: ResourceOptions | None = None, ) -> None: super().__init__("dsh:shm:NetworkingComponent", name, {}, opts) - child_opts = ResourceOptions.merge(ResourceOptions(parent=self), opts) + child_opts = ResourceOptions.merge(opts, ResourceOptions(parent=self)) # Deploy resource group resource_group = resources.ResourceGroup( @@ -325,10 +325,10 @@ def __init__( route_table_name=f"{stack_name}-route", routes=[], opts=ResourceOptions.merge( + child_opts, ResourceOptions( ignore_changes=["routes"] ), # allow routes to be created outside this definition - child_opts, ), ) @@ -392,10 +392,10 @@ def __init__( virtual_network_name=f"{stack_name}-vnet", virtual_network_peerings=[], opts=ResourceOptions.merge( + child_opts, ResourceOptions( ignore_changes=["virtual_network_peerings"] ), # allow SRE virtual networks to peer to this - child_opts, ), ) diff --git a/data_safe_haven/pulumi/components/shm_update_servers.py b/data_safe_haven/pulumi/components/shm_update_servers.py index fbdc4684c5..70e813f63a 100644 --- a/data_safe_haven/pulumi/components/shm_update_servers.py +++ b/data_safe_haven/pulumi/components/shm_update_servers.py @@ -5,7 +5,7 @@ from pulumi_azure_native import network from data_safe_haven.functions import b64encode -from data_safe_haven.pulumi.common.transformations import ( +from data_safe_haven.pulumi.common import ( get_available_ips_from_subnet, get_name_from_subnet, ) @@ -53,7 +53,7 @@ def __init__( opts: ResourceOptions | None = None, ) -> None: super().__init__("dsh:shm:UpdateServersComponent", name, {}, opts) - child_opts = ResourceOptions.merge(ResourceOptions(parent=self), opts) + child_opts = ResourceOptions.merge(opts, ResourceOptions(parent=self)) # Load cloud-init file b64cloudinit = self.read_cloudinit() diff --git a/data_safe_haven/pulumi/components/sre_application_gateway.py b/data_safe_haven/pulumi/components/sre_application_gateway.py index 9e1b45c30e..90b75df1d0 100644 --- a/data_safe_haven/pulumi/components/sre_application_gateway.py +++ b/data_safe_haven/pulumi/components/sre_application_gateway.py @@ -4,7 +4,7 @@ from pulumi import ComponentResource, Input, Output, ResourceOptions from pulumi_azure_native import managedidentity, network, resources -from data_safe_haven.pulumi.common.transformations import ( +from data_safe_haven.pulumi.common import ( get_available_ips_from_subnet, get_id_from_rg, get_id_from_subnet, @@ -55,7 +55,7 @@ def __init__( opts: ResourceOptions | None = None, ) -> None: super().__init__("dsh:sre:ApplicationGatewayComponent", name, {}, opts) - child_opts = ResourceOptions.merge(ResourceOptions(parent=self), opts) + child_opts = ResourceOptions.merge(opts, ResourceOptions(parent=self)) # Define public IP address public_ip = network.PublicIPAddress( diff --git a/data_safe_haven/pulumi/components/sre_data.py b/data_safe_haven/pulumi/components/sre_data.py index af1fd8ed59..6f5897557d 100644 --- a/data_safe_haven/pulumi/components/sre_data.py +++ b/data_safe_haven/pulumi/components/sre_data.py @@ -19,7 +19,7 @@ sha256hash, truncate_tokens, ) -from data_safe_haven.pulumi.common.transformations import ( +from data_safe_haven.pulumi.common import ( get_id_from_subnet, get_name_from_rg, ) @@ -105,7 +105,7 @@ def __init__( opts: ResourceOptions | None = None, ) -> None: super().__init__("dsh:sre:DataComponent", name, {}, opts) - child_opts = ResourceOptions.merge(ResourceOptions(parent=self), opts) + child_opts = ResourceOptions.merge(opts, ResourceOptions(parent=self)) # Deploy resource group resource_group = resources.ResourceGroup( @@ -223,10 +223,11 @@ def __init__( subscription_name=props.subscription_name, ), opts=ResourceOptions.merge( + child_opts, ResourceOptions( - depends_on=[props.dns_record] + depends_on=[props.dns_record], + parent=key_vault, ), # we need the delegation NS record to exist before generating the certificate - ResourceOptions(parent=key_vault), ), ) @@ -239,7 +240,7 @@ def __init__( resource_group_name=resource_group.name, secret_name="password-workspace-admin", vault_name=key_vault.name, - opts=ResourceOptions(parent=key_vault), + opts=ResourceOptions.merge(child_opts, ResourceOptions(parent=key_vault)), ) keyvault.Secret( f"{self._name}_kvs_password_gitea_database_admin", @@ -249,7 +250,7 @@ def __init__( resource_group_name=resource_group.name, secret_name="password-gitea-database-admin", vault_name=key_vault.name, - opts=ResourceOptions(parent=key_vault), + opts=ResourceOptions.merge(child_opts, ResourceOptions(parent=key_vault)), ) keyvault.Secret( f"{self._name}_kvs_password_hedgedoc_database_admin", @@ -259,7 +260,7 @@ def __init__( resource_group_name=resource_group.name, secret_name="password-hedgedoc-database-admin", vault_name=key_vault.name, - opts=ResourceOptions(parent=key_vault), + opts=ResourceOptions.merge(child_opts, ResourceOptions(parent=key_vault)), ) keyvault.Secret( f"{self._name}_kvs_password_nexus_admin", @@ -267,7 +268,7 @@ def __init__( resource_group_name=resource_group.name, secret_name="password-nexus-admin", vault_name=key_vault.name, - opts=ResourceOptions(parent=key_vault), + opts=ResourceOptions.merge(child_opts, ResourceOptions(parent=key_vault)), ) keyvault.Secret( f"{self._name}_kvs_password_user_database_admin", @@ -277,7 +278,7 @@ def __init__( resource_group_name=resource_group.name, secret_name="password-user-database-admin", vault_name=key_vault.name, - opts=ResourceOptions(parent=key_vault), + opts=ResourceOptions.merge(child_opts, ResourceOptions(parent=key_vault)), ) # Deploy state storage account @@ -363,7 +364,9 @@ def __init__( "/providers/Microsoft.Authorization/roleDefinitions/b7e6dc6d-f1e8-4753-8033-0f276bb0955b", ), scope=storage_account_securedata.id, - opts=ResourceOptions(parent=storage_account_securedata), + opts=ResourceOptions.merge( + child_opts, ResourceOptions(parent=storage_account_securedata) + ), ) # Deploy storage containers storage_container_egress = storage.BlobContainer( @@ -374,7 +377,9 @@ def __init__( deny_encryption_scope_override=False, public_access=storage.PublicAccess.NONE, resource_group_name=resource_group.name, - opts=ResourceOptions(parent=storage_account_securedata), + opts=ResourceOptions.merge( + child_opts, ResourceOptions(parent=storage_account_securedata) + ), ) storage_container_ingress = storage.BlobContainer( f"{self._name}_storage_container_ingress", @@ -384,7 +389,9 @@ def __init__( deny_encryption_scope_override=False, public_access=storage.PublicAccess.NONE, resource_group_name=resource_group.name, - opts=ResourceOptions(parent=storage_account_securedata), + opts=ResourceOptions.merge( + child_opts, ResourceOptions(parent=storage_account_securedata) + ), ) # Set storage container ACLs BlobContainerAcl( @@ -402,7 +409,9 @@ def __init__( storage_account_name=storage_account_securedata.name, subscription_name=props.subscription_name, ), - opts=ResourceOptions(parent=storage_container_egress), + opts=ResourceOptions.merge( + child_opts, ResourceOptions(parent=storage_container_egress) + ), ) BlobContainerAcl( f"{self._name}_storage_container_ingress_acl", @@ -418,7 +427,9 @@ def __init__( storage_account_name=storage_account_securedata.name, subscription_name=props.subscription_name, ), - opts=ResourceOptions(parent=storage_container_ingress), + opts=ResourceOptions.merge( + child_opts, ResourceOptions(parent=storage_container_ingress) + ), ) # Set up a private endpoint for the securedata data account storage_account_securedata_endpoint = network.PrivateEndpoint( @@ -434,7 +445,9 @@ def __init__( ], resource_group_name=resource_group.name, subnet=network.SubnetArgs(id=props.subnet_private_data_id), - opts=ResourceOptions(parent=storage_account_securedata), + opts=ResourceOptions.merge( + child_opts, ResourceOptions(parent=storage_account_securedata) + ), ) # Add a private DNS record for each securedata data custom DNS config network.PrivateDnsZoneGroup( @@ -454,7 +467,9 @@ def __init__( private_dns_zone_group_name=f"{stack_name}-dzg-storage-account-securedata", private_endpoint_name=storage_account_securedata_endpoint.name, resource_group_name=resource_group.name, - opts=ResourceOptions(parent=storage_account_securedata), + opts=ResourceOptions.merge( + child_opts, ResourceOptions(parent=storage_account_securedata) + ), ) # Deploy userdata files storage account @@ -502,7 +517,9 @@ def __init__( root_squash=storage.RootSquashType.NO_ROOT_SQUASH, share_name="home", share_quota=1024, - opts=ResourceOptions(parent=storage_account_userdata), + opts=ResourceOptions.merge( + child_opts, ResourceOptions(parent=storage_account_userdata) + ), ) storage.FileShare( f"{self._name}_storage_container_shared", @@ -513,7 +530,9 @@ def __init__( root_squash=storage.RootSquashType.ROOT_SQUASH, share_name="shared", share_quota=1024, - opts=ResourceOptions(parent=storage_account_userdata), + opts=ResourceOptions.merge( + child_opts, ResourceOptions(parent=storage_account_userdata) + ), ) # Set up a private endpoint for the userdata storage account storage_account_userdata_endpoint = network.PrivateEndpoint( @@ -529,7 +548,9 @@ def __init__( ], resource_group_name=resource_group.name, subnet=network.SubnetArgs(id=props.subnet_private_data_id), - opts=ResourceOptions(parent=storage_account_userdata), + opts=ResourceOptions.merge( + child_opts, ResourceOptions(parent=storage_account_userdata) + ), ) # Add a private DNS record for each userdata custom DNS config network.PrivateDnsZoneGroup( @@ -548,7 +569,9 @@ def __init__( private_dns_zone_group_name=f"{stack_name}-dzg-storage-account-userdata", private_endpoint_name=storage_account_userdata_endpoint.name, resource_group_name=resource_group.name, - opts=child_opts, + opts=ResourceOptions.merge( + child_opts, ResourceOptions(parent=storage_account_userdata) + ), ) # Register outputs diff --git a/data_safe_haven/pulumi/components/sre_gitea_server.py b/data_safe_haven/pulumi/components/sre_gitea_server.py index 18095eb41a..2fd244db4c 100644 --- a/data_safe_haven/pulumi/components/sre_gitea_server.py +++ b/data_safe_haven/pulumi/components/sre_gitea_server.py @@ -3,7 +3,7 @@ from pulumi import ComponentResource, Input, Output, ResourceOptions from pulumi_azure_native import containerinstance, dbforpostgresql, network, storage -from data_safe_haven.pulumi.common.transformations import ( +from data_safe_haven.pulumi.common import ( get_ip_address_from_container_group, get_ip_addresses_from_private_endpoint, ) @@ -75,7 +75,7 @@ def __init__( opts: ResourceOptions | None = None, ) -> None: super().__init__("dsh:sre:GiteaServerComponent", name, {}, opts) - child_opts = ResourceOptions.merge(ResourceOptions(parent=self), opts) + child_opts = ResourceOptions.merge(opts, ResourceOptions(parent=self)) # Define configuration file shares file_share_gitea_caddy = storage.FileShare( @@ -113,7 +113,9 @@ def __init__( storage_account_key=props.storage_account_key, storage_account_name=props.storage_account_name, ), - opts=child_opts, + opts=ResourceOptions.merge( + child_opts, ResourceOptions(parent=file_share_gitea_caddy) + ), ) # Upload Gitea configuration script @@ -143,7 +145,9 @@ def __init__( storage_account_key=props.storage_account_key, storage_account_name=props.storage_account_name, ), - opts=child_opts, + opts=ResourceOptions.merge( + child_opts, ResourceOptions(parent=file_share_gitea_gitea) + ), ) # Upload Gitea entrypoint script gitea_entrypoint_sh_reader = FileReader( @@ -158,7 +162,9 @@ def __init__( storage_account_key=props.storage_account_key, storage_account_name=props.storage_account_name, ), - opts=child_opts, + opts=ResourceOptions.merge( + child_opts, ResourceOptions(parent=file_share_gitea_gitea) + ), ) # Define a PostgreSQL server and default database @@ -198,7 +204,9 @@ def __init__( database_name=gitea_db_database_name, resource_group_name=props.user_services_resource_group_name, server_name=gitea_db_server.name, - opts=ResourceOptions(parent=gitea_db_server), + opts=ResourceOptions.merge( + child_opts, ResourceOptions(parent=gitea_db_server) + ), ) # Deploy a private endpoint to the PostgreSQL server gitea_db_private_endpoint = network.PrivateEndpoint( @@ -218,7 +226,9 @@ def __init__( ], resource_group_name=props.user_services_resource_group_name, subnet=network.SubnetArgs(id=props.database_subnet_id), - opts=child_opts, + opts=ResourceOptions.merge( + child_opts, ResourceOptions(parent=gitea_db_server) + ), ) gitea_db_private_ip_address = Output.from_input( get_ip_addresses_from_private_endpoint(gitea_db_private_endpoint) @@ -351,6 +361,7 @@ def __init__( ), ], opts=ResourceOptions.merge( + child_opts, ResourceOptions( delete_before_replace=True, depends_on=[ @@ -360,11 +371,10 @@ def __init__( ], replace_on_changes=["containers"], ), - child_opts, ), ) # Register the container group in the SRE private DNS zone - network.PrivateRecordSet( + private_dns_record_set = network.PrivateRecordSet( f"{self._name}_gitea_private_record_set", a_records=[ network.ARecordArgs( @@ -376,7 +386,9 @@ def __init__( relative_record_set_name="gitea", resource_group_name=props.networking_resource_group_name, ttl=3600, - opts=child_opts, + opts=ResourceOptions.merge( + child_opts, ResourceOptions(parent=container_group) + ), ) # Redirect the public DNS to private DNS network.RecordSet( @@ -389,5 +401,7 @@ def __init__( resource_group_name=props.networking_resource_group_name, ttl=3600, zone_name=props.sre_fqdn, - opts=child_opts, + opts=ResourceOptions.merge( + child_opts, ResourceOptions(parent=private_dns_record_set) + ), ) diff --git a/data_safe_haven/pulumi/components/sre_hedgedoc_server.py b/data_safe_haven/pulumi/components/sre_hedgedoc_server.py index a144137919..64ef209d0a 100644 --- a/data_safe_haven/pulumi/components/sre_hedgedoc_server.py +++ b/data_safe_haven/pulumi/components/sre_hedgedoc_server.py @@ -4,7 +4,7 @@ from pulumi_azure_native import containerinstance, dbforpostgresql, network, storage from data_safe_haven.functions import b64encode -from data_safe_haven.pulumi.common.transformations import ( +from data_safe_haven.pulumi.common import ( get_ip_address_from_container_group, get_ip_addresses_from_private_endpoint, ) @@ -88,7 +88,7 @@ def __init__( opts: ResourceOptions | None = None, ) -> None: super().__init__("dsh:sre:HedgeDocServerComponent", name, {}, opts) - child_opts = ResourceOptions.merge(ResourceOptions(parent=self), opts) + child_opts = ResourceOptions.merge(opts, ResourceOptions(parent=self)) # Define configuration file shares file_share_hedgedoc_caddy = storage.FileShare( @@ -117,7 +117,9 @@ def __init__( storage_account_key=props.storage_account_key, storage_account_name=props.storage_account_name, ), - opts=child_opts, + opts=ResourceOptions.merge( + child_opts, ResourceOptions(parent=file_share_hedgedoc_caddy) + ), ) # Load HedgeDoc configuration file for later use @@ -162,7 +164,9 @@ def __init__( database_name=hedgedoc_db_database_name, resource_group_name=props.user_services_resource_group_name, server_name=hedgedoc_db_server.name, - opts=ResourceOptions(parent=hedgedoc_db_server), + opts=ResourceOptions.merge( + child_opts, ResourceOptions(parent=hedgedoc_db_server) + ), ) # Deploy a private endpoint to the PostgreSQL server hedgedoc_db_private_endpoint = network.PrivateEndpoint( @@ -182,7 +186,9 @@ def __init__( ], resource_group_name=props.user_services_resource_group_name, subnet=network.SubnetArgs(id=props.database_subnet_id), - opts=child_opts, + opts=ResourceOptions.merge( + child_opts, ResourceOptions(parent=hedgedoc_db_server) + ), ) hedgedoc_db_private_ip_address = Output.from_input( get_ip_addresses_from_private_endpoint(hedgedoc_db_private_endpoint) @@ -348,6 +354,7 @@ def __init__( ), ], opts=ResourceOptions.merge( + child_opts, ResourceOptions( delete_before_replace=True, depends_on=[ @@ -355,11 +362,10 @@ def __init__( ], replace_on_changes=["containers"], ), - child_opts, ), ) # Register the container group in the SRE private DNS zone - network.PrivateRecordSet( + private_dns_record_set = network.PrivateRecordSet( f"{self._name}_hedgedoc_private_record_set", a_records=[ network.ARecordArgs( @@ -371,7 +377,9 @@ def __init__( relative_record_set_name="hedgedoc", resource_group_name=props.networking_resource_group_name, ttl=3600, - opts=child_opts, + opts=ResourceOptions.merge( + child_opts, ResourceOptions(parent=container_group) + ), ) # Redirect the public DNS to private DNS network.RecordSet( @@ -384,5 +392,7 @@ def __init__( resource_group_name=props.networking_resource_group_name, ttl=3600, zone_name=props.sre_fqdn, - opts=child_opts, + opts=ResourceOptions.merge( + child_opts, ResourceOptions(parent=private_dns_record_set) + ), ) diff --git a/data_safe_haven/pulumi/components/sre_monitoring.py b/data_safe_haven/pulumi/components/sre_monitoring.py index 6ebf02faf1..82aea96e23 100644 --- a/data_safe_haven/pulumi/components/sre_monitoring.py +++ b/data_safe_haven/pulumi/components/sre_monitoring.py @@ -36,7 +36,7 @@ def __init__( opts: ResourceOptions | None = None, ) -> None: super().__init__("dsh:sre:MonitoringComponent", name, {}, opts) - child_opts = ResourceOptions.merge(ResourceOptions(parent=self), opts) + child_opts = ResourceOptions.merge(opts, ResourceOptions(parent=self)) # Create Linux VM system update schedule: daily at 03:<index> automation.SoftwareUpdateConfigurationByName( @@ -85,12 +85,12 @@ def __init__( ), ), opts=ResourceOptions.merge( + child_opts, ResourceOptions( ignore_changes=[ "schedule_info", # options are added after deployment "updateConfiguration.linux.included_package_classifications", # ordering might change ] ), - child_opts, ), ) diff --git a/data_safe_haven/pulumi/components/sre_networking.py b/data_safe_haven/pulumi/components/sre_networking.py index 927ed0ea04..9117eb103e 100644 --- a/data_safe_haven/pulumi/components/sre_networking.py +++ b/data_safe_haven/pulumi/components/sre_networking.py @@ -4,7 +4,7 @@ from data_safe_haven.external import AzureIPv4Range from data_safe_haven.functions import alphanumeric, ordered_private_dns_zones -from data_safe_haven.pulumi.common.enums import NetworkingPriorities +from data_safe_haven.pulumi.common import NetworkingPriorities class SRENetworkingProps: @@ -31,23 +31,23 @@ def __init__( lambda r: r.next_subnet(256) ) self.subnet_guacamole_containers_iprange = self.vnet_iprange.apply( - lambda r: r.next_subnet(128) + lambda r: r.next_subnet(8) ) - self.subnet_guacamole_database_iprange = self.vnet_iprange.apply( - lambda r: r.next_subnet(128) + self.subnet_guacamole_containers_support_iprange = self.vnet_iprange.apply( + lambda r: r.next_subnet(8) ) self.subnet_private_data_iprange = self.vnet_iprange.apply( lambda r: r.next_subnet(16) ) - self.subnet_software_repositories_iprange = self.vnet_iprange.apply( - lambda r: r.next_subnet(8) - ) self.subnet_user_services_containers_iprange = self.vnet_iprange.apply( lambda r: r.next_subnet(8) ) - self.subnet_user_services_databases_iprange = self.vnet_iprange.apply( + self.subnet_user_services_containers_support_iprange = self.vnet_iprange.apply( lambda r: r.next_subnet(8) ) + self.subnet_user_services_software_repositories_iprange = ( + self.vnet_iprange.apply(lambda r: r.next_subnet(8)) + ) self.subnet_workspaces_iprange = self.vnet_iprange.apply( lambda r: r.next_subnet(256) ) @@ -75,7 +75,7 @@ def __init__( opts: ResourceOptions | None = None, ) -> None: super().__init__("dsh:sre:NetworkingComponent", name, {}, opts) - child_opts = ResourceOptions.merge(ResourceOptions(parent=self), opts) + child_opts = ResourceOptions.merge(opts, ResourceOptions(parent=self)) # Deploy resource group resource_group = resources.ResourceGroup( @@ -92,20 +92,24 @@ def __init__( subnet_guacamole_containers_prefix = ( props.subnet_guacamole_containers_iprange.apply(lambda r: str(r)) ) - subnet_guacamole_database_prefix = ( - props.subnet_guacamole_database_iprange.apply(lambda r: str(r)) + subnet_guacamole_containers_support_prefix = ( + props.subnet_guacamole_containers_support_iprange.apply(lambda r: str(r)) ) subnet_private_data_prefix = props.subnet_private_data_iprange.apply( lambda r: str(r) ) - subnet_software_repositories_prefix = ( - props.subnet_software_repositories_iprange.apply(lambda r: str(r)) - ) subnet_user_services_containers_prefix = ( props.subnet_user_services_containers_iprange.apply(lambda r: str(r)) ) - subnet_user_services_databases_prefix = ( - props.subnet_user_services_databases_iprange.apply(lambda r: str(r)) + subnet_user_services_containers_support_prefix = ( + props.subnet_user_services_containers_support_iprange.apply( + lambda r: str(r) + ) + ) + subnet_user_services_software_repositories_prefix = ( + props.subnet_user_services_software_repositories_iprange.apply( + lambda r: str(r) + ) ) subnet_workspaces_prefix = props.subnet_workspaces_iprange.apply( lambda r: str(r) @@ -162,9 +166,9 @@ def __init__( resource_group_name=resource_group.name, opts=child_opts, ) - nsg_guacamole_database = network.NetworkSecurityGroup( - f"{self._name}_nsg_guacamole_database", - network_security_group_name=f"{stack_name}-nsg-guacamole-database", + nsg_guacamole_containers_support = network.NetworkSecurityGroup( + f"{self._name}_nsg_guacamole_containers_support", + network_security_group_name=f"{stack_name}-nsg-guacamole-containers-support", resource_group_name=resource_group.name, opts=child_opts, ) @@ -174,21 +178,21 @@ def __init__( resource_group_name=resource_group.name, opts=child_opts, ) - nsg_software_repositories = network.NetworkSecurityGroup( - f"{self._name}_nsg_software_repositories", - network_security_group_name=f"{stack_name}-nsg-software-repositories", - resource_group_name=resource_group.name, - opts=child_opts, - ) nsg_user_services_containers = network.NetworkSecurityGroup( f"{self._name}_nsg_user_services_containers", network_security_group_name=f"{stack_name}-nsg-user-services-containers", resource_group_name=resource_group.name, opts=child_opts, ) - nsg_user_services_databases = network.NetworkSecurityGroup( - f"{self._name}_nsg_user_services_databases", - network_security_group_name=f"{stack_name}-nsg-user-services-databases", + nsg_user_services_containers_support = network.NetworkSecurityGroup( + f"{self._name}_nsg_user_services_containers_support", + network_security_group_name=f"{stack_name}-nsg-user-services-containers-support", + resource_group_name=resource_group.name, + opts=child_opts, + ) + nsg_user_services_software_repositories = network.NetworkSecurityGroup( + f"{self._name}_nsg_user_services_software_repositories", + network_security_group_name=f"{stack_name}-nsg-user-services-software-repositories", resource_group_name=resource_group.name, opts=child_opts, ) @@ -305,11 +309,15 @@ def __init__( # Define the virtual network and its subnets subnet_application_gateway_name = "ApplicationGatewaySubnet" subnet_guacamole_containers_name = "GuacamoleContainersSubnet" - subnet_guacamole_database_name = "GuacamoleDatabaseSubnet" + subnet_guacamole_containers_support_name = "GuacamoleContainersSupportSubnet" subnet_private_data_name = "PrivateDataSubnet" - subnet_software_repositories_name = "SoftwareRepositoriesSubnet" subnet_user_services_containers_name = "UserServicesContainersSubnet" - subnet_user_services_databases_name = "UserServicesDatabasesSubnet" + subnet_user_services_containers_support_name = ( + "UserServicesContainersSupportSubnet" + ) + subnet_user_services_software_repositories_name = ( + "UserServicesSoftwareRepositoriesSubnet" + ) subnet_workspaces_name = "WorkspacesSubnet" sre_virtual_network = network.VirtualNetwork( f"{self._name}_virtual_network", @@ -341,12 +349,12 @@ def __init__( id=nsg_guacamole_containers.id ), ), - # Guacamole database + # Guacamole containers support network.SubnetArgs( - address_prefix=subnet_guacamole_database_prefix, - name=subnet_guacamole_database_name, + address_prefix=subnet_guacamole_containers_support_prefix, + name=subnet_guacamole_containers_support_name, network_security_group=network.NetworkSecurityGroupArgs( - id=nsg_guacamole_database.id + id=nsg_guacamole_containers_support.id ), private_endpoint_network_policies="Disabled", ), @@ -364,9 +372,9 @@ def __init__( ) ], ), - # Software repositories + # User services containers network.SubnetArgs( - address_prefix=subnet_software_repositories_prefix, + address_prefix=subnet_user_services_containers_prefix, delegations=[ network.DelegationArgs( name="SubnetDelegationContainerGroups", @@ -374,14 +382,22 @@ def __init__( type="Microsoft.Network/virtualNetworks/subnets/delegations", ), ], - name=subnet_software_repositories_name, + name=subnet_user_services_containers_name, network_security_group=network.NetworkSecurityGroupArgs( - id=nsg_software_repositories.id + id=nsg_user_services_containers.id ), ), - # User services containers + # User services containers support network.SubnetArgs( - address_prefix=subnet_user_services_containers_prefix, + address_prefix=subnet_user_services_containers_support_prefix, + name=subnet_user_services_containers_support_name, + network_security_group=network.NetworkSecurityGroupArgs( + id=nsg_user_services_containers_support.id + ), + ), + # User services software repositories + network.SubnetArgs( + address_prefix=subnet_user_services_software_repositories_prefix, delegations=[ network.DelegationArgs( name="SubnetDelegationContainerGroups", @@ -389,17 +405,9 @@ def __init__( type="Microsoft.Network/virtualNetworks/subnets/delegations", ), ], - name=subnet_user_services_containers_name, - network_security_group=network.NetworkSecurityGroupArgs( - id=nsg_user_services_containers.id - ), - ), - # User services databases - network.SubnetArgs( - address_prefix=subnet_user_services_databases_prefix, - name=subnet_user_services_databases_name, + name=subnet_user_services_software_repositories_name, network_security_group=network.NetworkSecurityGroupArgs( - id=nsg_user_services_databases.id + id=nsg_user_services_software_repositories.id ), ), # Workspaces @@ -414,10 +422,10 @@ def __init__( virtual_network_name=f"{stack_name}-vnet", virtual_network_peerings=[], opts=ResourceOptions.merge( + child_opts, ResourceOptions( ignore_changes=["virtual_network_peerings"] ), # allow peering to SHM virtual network - child_opts, ), ) @@ -439,7 +447,9 @@ def __init__( virtual_network_peering_name=Output.concat( "peer_sre_", props.sre_name, "_to_shm" ), - opts=child_opts, + opts=ResourceOptions.merge( + child_opts, ResourceOptions(parent=sre_virtual_network) + ), ) network.VirtualNetworkPeering( f"{self._name}_shm_to_sre_peering", @@ -450,7 +460,9 @@ def __init__( virtual_network_peering_name=Output.concat( "peer_shm_to_sre_", props.sre_name ), - opts=child_opts, + opts=ResourceOptions.merge( + child_opts, ResourceOptions(parent=sre_virtual_network) + ), ) # Link to SHM private DNS zones @@ -465,7 +477,9 @@ def __init__( virtual_network_link_name=Output.concat( "link-to-", sre_virtual_network.name ), - opts=child_opts, + opts=ResourceOptions.merge( + child_opts, ResourceOptions(parent=sre_virtual_network) + ), ) # Define SRE DNS zone @@ -500,7 +514,9 @@ def __init__( resource_group_name=props.shm_networking_resource_group_name, ttl=3600, zone_name=shm_dns_zone.name, - opts=child_opts, + opts=ResourceOptions.merge( + child_opts, ResourceOptions(parent=sre_dns_zone) + ), ) network.RecordSet( f"{self._name}_caa_record", @@ -516,7 +532,9 @@ def __init__( resource_group_name=resource_group.name, ttl=30, zone_name=sre_dns_zone.name, - opts=child_opts, + opts=ResourceOptions.merge( + child_opts, ResourceOptions(parent=sre_dns_zone) + ), ) # Define SRE internal DNS zone @@ -525,10 +543,12 @@ def __init__( location="Global", private_zone_name=Output.concat("privatelink.", sre_fqdn), resource_group_name=resource_group.name, - opts=child_opts, + opts=ResourceOptions.merge( + child_opts, ResourceOptions(parent=sre_dns_zone) + ), ) network.VirtualNetworkLink( - f"{self._name}_private_zone_vnet_link", + f"{self._name}_private_zone_internal_vnet_link", location="Global", private_zone_name=sre_private_dns_zone.name, registration_enabled=False, @@ -537,7 +557,9 @@ def __init__( virtual_network_link_name=Output.concat( "link-to-", sre_virtual_network.name ), - opts=child_opts, + opts=ResourceOptions.merge( + child_opts, ResourceOptions(parent=sre_virtual_network) + ), ) # Register outputs @@ -556,8 +578,8 @@ def __init__( resource_group_name=resource_group.name, virtual_network_name=sre_virtual_network.name, ) - self.subnet_guacamole_database = network.get_subnet_output( - subnet_name=subnet_guacamole_database_name, + self.subnet_guacamole_containers_support = network.get_subnet_output( + subnet_name=subnet_guacamole_containers_support_name, resource_group_name=resource_group.name, virtual_network_name=sre_virtual_network.name, ) @@ -566,18 +588,18 @@ def __init__( resource_group_name=resource_group.name, virtual_network_name=sre_virtual_network.name, ) - self.subnet_software_repositories = network.get_subnet_output( - subnet_name=subnet_software_repositories_name, + self.subnet_user_services_containers = network.get_subnet_output( + subnet_name=subnet_user_services_containers_name, resource_group_name=resource_group.name, virtual_network_name=sre_virtual_network.name, ) - self.subnet_user_services_containers = network.get_subnet_output( - subnet_name=subnet_user_services_containers_name, + self.subnet_user_services_containers_support = network.get_subnet_output( + subnet_name=subnet_user_services_containers_support_name, resource_group_name=resource_group.name, virtual_network_name=sre_virtual_network.name, ) - self.subnet_user_services_databases = network.get_subnet_output( - subnet_name=subnet_user_services_databases_name, + self.subnet_user_services_software_repositories = network.get_subnet_output( + subnet_name=subnet_user_services_software_repositories_name, resource_group_name=resource_group.name, virtual_network_name=sre_virtual_network.name, ) diff --git a/data_safe_haven/pulumi/components/sre_remote_desktop.py b/data_safe_haven/pulumi/components/sre_remote_desktop.py index b475ca0616..879e32b15c 100644 --- a/data_safe_haven/pulumi/components/sre_remote_desktop.py +++ b/data_safe_haven/pulumi/components/sre_remote_desktop.py @@ -11,7 +11,7 @@ ) from data_safe_haven.external import AzureIPv4Range -from data_safe_haven.pulumi.common.transformations import ( +from data_safe_haven.pulumi.common import ( get_id_from_subnet, get_ip_address_from_container_group, ) @@ -49,7 +49,7 @@ def __init__( storage_account_name: Input[str], storage_account_resource_group_name: Input[str], subnet_guacamole_containers: Input[network.GetSubnetResult], - subnet_guacamole_database: Input[network.GetSubnetResult], + subnet_guacamole_containers_support: Input[network.GetSubnetResult], virtual_network: Input[network.VirtualNetwork], virtual_network_resource_group_name: Input[str], database_username: Input[str] | None = "postgresadmin", @@ -86,11 +86,11 @@ def __init__( if s.address_prefix else [] ) - self.subnet_guacamole_database_id = Output.from_input( - subnet_guacamole_database + self.subnet_guacamole_containers_support_id = Output.from_input( + subnet_guacamole_containers_support ).apply(get_id_from_subnet) - self.subnet_guacamole_database_ip_addresses = Output.from_input( - subnet_guacamole_database + self.subnet_guacamole_containers_support_ip_addresses = Output.from_input( + subnet_guacamole_containers_support ).apply( lambda s: [ str(ip) for ip in AzureIPv4Range.from_cidr(s.address_prefix).available() @@ -113,7 +113,7 @@ def __init__( opts: ResourceOptions | None = None, ) -> None: super().__init__("dsh:sre:RemoteDesktopComponent", name, {}, opts) - child_opts = ResourceOptions.merge(ResourceOptions(parent=self), opts) + child_opts = ResourceOptions.merge(opts, ResourceOptions(parent=self)) # Deploy resource group resource_group = resources.ResourceGroup( @@ -157,7 +157,7 @@ def __init__( storage_account_key=props.storage_account_key, storage_account_name=props.storage_account_name, ), - opts=child_opts, + opts=ResourceOptions.merge(child_opts, ResourceOptions(parent=file_share)), ) # Define a PostgreSQL server @@ -194,7 +194,9 @@ def __init__( f"{self._name}_connection_db_private_endpoint", custom_dns_configs=[ network.CustomDnsConfigPropertiesFormatArgs( - ip_addresses=[props.subnet_guacamole_database_ip_addresses[0]], + ip_addresses=[ + props.subnet_guacamole_containers_support_ip_addresses[0] + ], ) ], private_endpoint_name=f"{stack_name}-endpoint-guacamole-db", @@ -211,8 +213,10 @@ def __init__( ) ], resource_group_name=resource_group.name, - subnet=network.SubnetArgs(id=props.subnet_guacamole_database_id), - opts=child_opts, + subnet=network.SubnetArgs(id=props.subnet_guacamole_containers_support_id), + opts=ResourceOptions.merge( + child_opts, ResourceOptions(parent=connection_db_server) + ), ) connection_db_name = "guacamole" connection_db = dbforpostgresql.Database( @@ -221,7 +225,9 @@ def __init__( database_name=connection_db_name, resource_group_name=resource_group.name, server_name=connection_db_server.name, - opts=child_opts, + opts=ResourceOptions.merge( + child_opts, ResourceOptions(parent=connection_db_server) + ), ) # Define a network profile @@ -243,13 +249,13 @@ def __init__( network_profile_name=f"{stack_name}-np-guacamole", resource_group_name=props.virtual_network_resource_group_name, opts=ResourceOptions.merge( + child_opts, ResourceOptions( depends_on=[props.virtual_network], ignore_changes=[ "container_network_interface_configurations" ], # allow container groups to be registered to this interface ), - child_opts, ), ) @@ -321,7 +327,9 @@ def __init__( ), containerinstance.EnvironmentVariableArgs( name="POSTGRES_HOSTNAME", - value=props.subnet_guacamole_database_ip_addresses[0], + value=props.subnet_guacamole_containers_support_ip_addresses[ + 0 + ], ), containerinstance.EnvironmentVariableArgs( name="POSTGRES_PASSWORD", @@ -401,7 +409,9 @@ def __init__( ), containerinstance.EnvironmentVariableArgs( name="POSTGRES_HOST", - value=props.subnet_guacamole_database_ip_addresses[0], + value=props.subnet_guacamole_containers_support_ip_addresses[ + 0 + ], ), containerinstance.EnvironmentVariableArgs( name="POSTGRES_PASSWORD", @@ -451,10 +461,10 @@ def __init__( ), ], opts=ResourceOptions.merge( + child_opts, ResourceOptions( delete_before_replace=True, replace_on_changes=["containers"] ), - child_opts, ), ) diff --git a/data_safe_haven/pulumi/components/sre_software_repositories.py b/data_safe_haven/pulumi/components/sre_software_repositories.py index 4d9df1d097..00fc4002c7 100644 --- a/data_safe_haven/pulumi/components/sre_software_repositories.py +++ b/data_safe_haven/pulumi/components/sre_software_repositories.py @@ -1,20 +1,17 @@ """Pulumi component for SRE monitoring""" import pathlib -from contextlib import suppress from pulumi import ComponentResource, Input, Output, ResourceOptions -from pulumi_azure_native import containerinstance, network, resources, storage +from pulumi_azure_native import containerinstance, network, storage -from data_safe_haven.pulumi.common.transformations import ( - get_available_ips_from_subnet, - get_id_from_subnet, +from data_safe_haven.pulumi.common import ( get_ip_address_from_container_group, ) from data_safe_haven.pulumi.dynamic.file_share_file import ( FileShareFile, FileShareFileProps, ) -from data_safe_haven.utility import FileReader +from data_safe_haven.utility import FileReader, SoftwarePackageCategory class SRESoftwareRepositoriesProps: @@ -25,31 +22,30 @@ def __init__( location: Input[str], networking_resource_group_name: Input[str], nexus_admin_password: Input[str], - software_packages: str, + resource_group_name: Input[str], + software_packages: SoftwarePackageCategory, sre_fqdn: Input[str], storage_account_key: Input[str], storage_account_name: Input[str], storage_account_resource_group_name: Input[str], - subnet: Input[network.GetSubnetResult], + subnet_id: Input[str], virtual_network: Input[network.VirtualNetwork], virtual_network_resource_group_name: Input[str], ) -> None: self.location = location self.networking_resource_group_name = networking_resource_group_name self.nexus_admin_password = Output.secret(nexus_admin_password) - self.nexus_packages: str | None = None - with suppress(KeyError): - self.nexus_packages = {"any": "all", "pre-approved": "selected"}[ - software_packages - ] + self.nexus_packages: str | None = { + SoftwarePackageCategory.ANY: "all", + SoftwarePackageCategory.PRE_APPROVED: "selected", + SoftwarePackageCategory.NONE: None, + }[software_packages] + self.resource_group_name = resource_group_name self.sre_fqdn = sre_fqdn self.storage_account_key = storage_account_key self.storage_account_name = storage_account_name self.storage_account_resource_group_name = storage_account_resource_group_name - self.subnet_id = Output.from_input(subnet).apply(get_id_from_subnet) - self.subnet_ip_addresses = Output.from_input(subnet).apply( - get_available_ips_from_subnet - ) + self.subnet_id = subnet_id self.virtual_network = virtual_network self.virtual_network_resource_group_name = virtual_network_resource_group_name @@ -65,15 +61,7 @@ def __init__( opts: ResourceOptions | None = None, ) -> None: super().__init__("dsh:sre:SRESoftwareRepositoriesComponent", name, {}, opts) - child_opts = ResourceOptions.merge(ResourceOptions(parent=self), opts) - - # Deploy resource group - resource_group = resources.ResourceGroup( - f"{self._name}_resource_group", - location=props.location, - resource_group_name=f"{stack_name}-rg-software-repositories", - opts=child_opts, - ) + child_opts = ResourceOptions.merge(opts, ResourceOptions(parent=self)) # Define configuration file shares file_share_caddy = storage.FileShare( @@ -118,7 +106,9 @@ def __init__( storage_account_key=props.storage_account_key, storage_account_name=props.storage_account_name, ), - opts=child_opts, + opts=ResourceOptions.merge( + child_opts, ResourceOptions(parent=file_share_caddy) + ), ) # Upload Nexus allowlists @@ -134,7 +124,9 @@ def __init__( storage_account_key=props.storage_account_key, storage_account_name=props.storage_account_name, ), - opts=child_opts, + opts=ResourceOptions.merge( + child_opts, ResourceOptions(parent=file_share_nexus) + ), ) pypi_reader = FileReader( resources_path / "software_repositories" / "allowlists" / "pypi.allowlist" @@ -148,7 +140,9 @@ def __init__( storage_account_key=props.storage_account_key, storage_account_name=props.storage_account_name, ), - opts=child_opts, + opts=ResourceOptions.merge( + child_opts, ResourceOptions(parent=file_share_nexus) + ), ) # Define a network profile @@ -170,13 +164,13 @@ def __init__( network_profile_name=f"{stack_name}-np-software-repositories", resource_group_name=props.virtual_network_resource_group_name, opts=ResourceOptions.merge( + child_opts, ResourceOptions( depends_on=[props.virtual_network], ignore_changes=[ "container_network_interface_configurations" ], # allow container groups to be registered to this interface ), - child_opts, ), ) @@ -283,7 +277,7 @@ def __init__( id=container_network_profile.id, ), os_type=containerinstance.OperatingSystemTypes.LINUX, - resource_group_name=resource_group.name, + resource_group_name=props.resource_group_name, restart_policy=containerinstance.ContainerGroupRestartPolicy.ALWAYS, sku=containerinstance.ContainerGroupSku.STANDARD, volumes=[ @@ -313,14 +307,14 @@ def __init__( ), ], opts=ResourceOptions.merge( + child_opts, ResourceOptions( delete_before_replace=True, replace_on_changes=["containers"] ), - child_opts, ), ) # Register the container group in the SRE private DNS zone - network.PrivateRecordSet( + private_dns_record_set = network.PrivateRecordSet( f"{self._name}_nexus_private_record_set", a_records=[ network.ARecordArgs( @@ -334,7 +328,9 @@ def __init__( relative_record_set_name="nexus", resource_group_name=props.networking_resource_group_name, ttl=3600, - opts=child_opts, + opts=ResourceOptions.merge( + child_opts, ResourceOptions(parent=container_group) + ), ) # Redirect the public DNS to private DNS network.RecordSet( @@ -347,5 +343,7 @@ def __init__( resource_group_name=props.networking_resource_group_name, ttl=3600, zone_name=props.sre_fqdn, - opts=child_opts, + opts=ResourceOptions.merge( + child_opts, ResourceOptions(parent=private_dns_record_set) + ), ) diff --git a/data_safe_haven/pulumi/components/sre_user_services.py b/data_safe_haven/pulumi/components/sre_user_services.py index c611c167ac..cb67408111 100644 --- a/data_safe_haven/pulumi/components/sre_user_services.py +++ b/data_safe_haven/pulumi/components/sre_user_services.py @@ -1,10 +1,15 @@ from pulumi import ComponentResource, Input, Output, ResourceOptions from pulumi_azure_native import network, resources -from data_safe_haven.pulumi.common.transformations import get_id_from_subnet +from data_safe_haven.pulumi.common import get_id_from_subnet +from data_safe_haven.utility import SoftwarePackageCategory from .sre_gitea_server import SREGiteaServerComponent, SREGiteaServerProps from .sre_hedgedoc_server import SREHedgeDocServerComponent, SREHedgeDocServerProps +from .sre_software_repositories import ( + SRESoftwareRepositoriesComponent, + SRESoftwareRepositoriesProps, +) class SREUserServicesProps: @@ -23,13 +28,16 @@ def __init__( ldap_user_security_group_name: Input[str], location: Input[str], networking_resource_group_name: Input[str], + nexus_admin_password: Input[str], + software_packages: SoftwarePackageCategory, sre_fqdn: Input[str], sre_private_dns_zone_id: Input[str], storage_account_key: Input[str], storage_account_name: Input[str], storage_account_resource_group_name: Input[str], subnet_containers: Input[network.GetSubnetResult], - subnet_databases: Input[network.GetSubnetResult], + subnet_containers_support: Input[network.GetSubnetResult], + subnet_software_repositories: Input[network.GetSubnetResult], virtual_network: Input[network.VirtualNetwork], virtual_network_resource_group_name: Input[str], ) -> None: @@ -44,6 +52,8 @@ def __init__( self.ldap_user_security_group_name = ldap_user_security_group_name self.location = location self.networking_resource_group_name = networking_resource_group_name + self.nexus_admin_password = Output.secret(nexus_admin_password) + self.software_packages = software_packages self.sre_fqdn = sre_fqdn self.sre_private_dns_zone_id = sre_private_dns_zone_id self.storage_account_key = storage_account_key @@ -52,9 +62,12 @@ def __init__( self.subnet_containers_id = Output.from_input(subnet_containers).apply( get_id_from_subnet ) - self.subnet_databases_id = Output.from_input(subnet_databases).apply( - get_id_from_subnet - ) + self.subnet_containers_support_id = Output.from_input( + subnet_containers_support + ).apply(get_id_from_subnet) + self.subnet_software_repositories_id = Output.from_input( + subnet_software_repositories + ).apply(get_id_from_subnet) self.virtual_network = virtual_network self.virtual_network_resource_group_name = virtual_network_resource_group_name @@ -70,7 +83,7 @@ def __init__( opts: ResourceOptions | None = None, ) -> None: super().__init__("dsh:sre:UserServicesComponent", name, {}, opts) - child_opts = ResourceOptions.merge(ResourceOptions(parent=self), opts) + child_opts = ResourceOptions.merge(opts, ResourceOptions(parent=self)) # Deploy resource group resource_group = resources.ResourceGroup( @@ -99,13 +112,13 @@ def __init__( network_profile_name=f"{stack_name}-np-user-services", resource_group_name=props.virtual_network_resource_group_name, opts=ResourceOptions.merge( + child_opts, ResourceOptions( depends_on=[props.virtual_network], ignore_changes=[ "container_network_interface_configurations" ], # allow container groups to be registered to this interface ), - child_opts, ), ) @@ -114,7 +127,7 @@ def __init__( "sre_gitea_server", stack_name, SREGiteaServerProps( - database_subnet_id=props.subnet_databases_id, + database_subnet_id=props.subnet_containers_support_id, database_password=props.gitea_database_password, ldap_bind_dn=props.ldap_bind_dn, ldap_root_dn=props.ldap_root_dn, @@ -142,7 +155,7 @@ def __init__( "sre_hedgedoc_server", stack_name, SREHedgeDocServerProps( - database_subnet_id=props.subnet_databases_id, + database_subnet_id=props.subnet_containers_support_id, database_password=props.hedgedoc_database_password, domain_netbios_name=props.domain_netbios_name, ldap_bind_dn=props.ldap_bind_dn, @@ -165,3 +178,24 @@ def __init__( ), opts=child_opts, ) + + # Deploy software repository servers + SRESoftwareRepositoriesComponent( + "sre_software_repositories", + stack_name, + SRESoftwareRepositoriesProps( + location=props.location, + networking_resource_group_name=props.networking_resource_group_name, + nexus_admin_password=props.nexus_admin_password, + resource_group_name=resource_group.name, + sre_fqdn=props.sre_fqdn, + software_packages=props.software_packages, + storage_account_key=props.storage_account_key, + storage_account_name=props.storage_account_name, + storage_account_resource_group_name=props.storage_account_resource_group_name, + subnet_id=props.subnet_software_repositories_id, + virtual_network=props.virtual_network, + virtual_network_resource_group_name=props.virtual_network_resource_group_name, + ), + opts=child_opts, + ) diff --git a/data_safe_haven/pulumi/components/sre_workspace.py b/data_safe_haven/pulumi/components/sre_workspace.py index 98d8486bed..f1774add9f 100644 --- a/data_safe_haven/pulumi/components/sre_workspace.py +++ b/data_safe_haven/pulumi/components/sre_workspace.py @@ -7,7 +7,7 @@ from data_safe_haven.exceptions import DataSafeHavenPulumiError from data_safe_haven.functions import b64encode, replace_separators -from data_safe_haven.pulumi.common.transformations import ( +from data_safe_haven.pulumi.common import ( get_available_ips_from_subnet, get_name_from_rg, get_name_from_subnet, @@ -97,7 +97,7 @@ def __init__( opts: ResourceOptions | None = None, ) -> None: super().__init__("dsh:sre:WorkspaceComponent", name, {}, opts) - child_opts = ResourceOptions.merge(ResourceOptions(parent=self), opts) + child_opts = ResourceOptions.merge(opts, ResourceOptions(parent=self)) # Deploy resource group resource_group = resources.ResourceGroup( diff --git a/data_safe_haven/pulumi/components/virtual_machine.py b/data_safe_haven/pulumi/components/virtual_machine.py index fdfbe9dd72..5d7b7ca939 100644 --- a/data_safe_haven/pulumi/components/virtual_machine.py +++ b/data_safe_haven/pulumi/components/virtual_machine.py @@ -127,7 +127,7 @@ class VMComponent(ComponentResource): def __init__(self, name: str, props: VMProps, opts: ResourceOptions | None = None): super().__init__("dsh:common:VMComponent", name, {}, opts) - child_opts = ResourceOptions.merge(ResourceOptions(parent=self), opts) + child_opts = ResourceOptions.merge(opts, ResourceOptions(parent=self)) # Retrieve existing resources subnet = network.get_subnet_output( @@ -207,10 +207,10 @@ def __init__(self, name: str, props: VMProps, opts: ResourceOptions | None = Non ), vm_name=props.vm_name, opts=ResourceOptions.merge( + child_opts, ResourceOptions( delete_before_replace=True, replace_on_changes=["os_profile"] ), - child_opts, ), ) @@ -233,7 +233,9 @@ def __init__(self, name: str, props: VMProps, opts: ResourceOptions | None = Non type_handler_version=props.log_analytics_extension_version, vm_extension_name=props.log_analytics_extension_name, vm_name=virtual_machine.name, - opts=child_opts, + opts=ResourceOptions.merge( + child_opts, ResourceOptions(parent=virtual_machine) + ), ) # Register outputs diff --git a/data_safe_haven/pulumi/declarative_sre.py b/data_safe_haven/pulumi/declarative_sre.py index ef8c073879..eb2e969d85 100644 --- a/data_safe_haven/pulumi/declarative_sre.py +++ b/data_safe_haven/pulumi/declarative_sre.py @@ -14,10 +14,6 @@ SRERemoteDesktopComponent, SRERemoteDesktopProps, ) -from .components.sre_software_repositories import ( - SRESoftwareRepositoriesComponent, - SRESoftwareRepositoriesProps, -) from .components.sre_user_services import SREUserServicesComponent, SREUserServicesProps from .components.sre_workspace import ( SREWorkspaceComponent, @@ -163,7 +159,7 @@ def run(self) -> None: ldap_user_security_group_name=ldap_user_security_group_name, location=self.cfg.azure.location, subnet_guacamole_containers=networking.subnet_guacamole_containers, - subnet_guacamole_database=networking.subnet_guacamole_database, + subnet_guacamole_containers_support=networking.subnet_guacamole_containers_support, storage_account_key=data.storage_account_state_key, storage_account_name=data.storage_account_state_name, storage_account_resource_group_name=data.resource_group_name, @@ -209,25 +205,6 @@ def run(self) -> None: ), ) - # Deploy software repository servers - SRESoftwareRepositoriesComponent( - "shm_update_servers", - self.stack_name, - SRESoftwareRepositoriesProps( - location=self.cfg.azure.location, - networking_resource_group_name=networking.resource_group.name, - nexus_admin_password=data.password_nexus_admin, - sre_fqdn=networking.sre_fqdn, - software_packages=self.cfg.sres[self.sre_name].software_packages, - storage_account_key=data.storage_account_state_key, - storage_account_name=data.storage_account_state_name, - storage_account_resource_group_name=data.resource_group_name, - subnet=networking.subnet_software_repositories, - virtual_network=networking.virtual_network, - virtual_network_resource_group_name=networking.resource_group.name, - ), - ) - # Deploy containerised user services SREUserServicesComponent( "sre_user_services", @@ -246,13 +223,16 @@ def run(self) -> None: ldap_user_search_base=ldap_user_search_base, location=self.cfg.azure.location, networking_resource_group_name=networking.resource_group.name, + nexus_admin_password=data.password_nexus_admin, + software_packages=self.cfg.sres[self.sre_name].software_packages, sre_fqdn=networking.sre_fqdn, sre_private_dns_zone_id=networking.sre_private_dns_zone_id, storage_account_key=data.storage_account_state_key, storage_account_name=data.storage_account_state_name, storage_account_resource_group_name=data.resource_group_name, subnet_containers=networking.subnet_user_services_containers, - subnet_databases=networking.subnet_user_services_databases, + subnet_containers_support=networking.subnet_user_services_containers_support, + subnet_software_repositories=networking.subnet_user_services_software_repositories, virtual_network=networking.virtual_network, virtual_network_resource_group_name=networking.resource_group.name, ),