diff --git a/modules/Microsoft.AVS/privateClouds/.bicep/nested_roleAssignments.bicep b/modules/Microsoft.AVS/privateClouds/.bicep/nested_roleAssignments.bicep deleted file mode 100644 index 33195bef44..0000000000 --- a/modules/Microsoft.AVS/privateClouds/.bicep/nested_roleAssignments.bicep +++ /dev/null @@ -1,68 +0,0 @@ -@sys.description('Required. The IDs of the principals to assign the role to.') -param principalIds array - -@sys.description('Required. The name of the role to assign. If it cannot be found you can specify the role definition ID instead.') -param roleDefinitionIdOrName string - -@sys.description('Required. The resource ID of the resource to apply the role assignment to.') -param resourceId string - -@sys.description('Optional. The principal type of the assigned principal ID.') -@allowed([ - 'ServicePrincipal' - 'Group' - 'User' - 'ForeignGroup' - 'Device' - '' -]) -param principalType string = '' - -@sys.description('Optional. The description of the role assignment.') -param description string = '' - -@sys.description('Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase "foo_storage_container"') -param condition string = '' - -@sys.description('Optional. Version of the condition.') -@allowed([ - '2.0' -]) -param conditionVersion string = '2.0' - -@sys.description('Optional. Id of the delegated managed identity resource.') -param delegatedManagedIdentityResourceId string = '' - -var builtInRoleNames = { - 'Contributor': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c') - 'Log Analytics Contributor': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '92aaf0da-9dab-42b6-94a3-d43ce8d16293') - 'Log Analytics Reader': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '73c42c96-874c-492b-b04d-ab87d138a893') - 'Managed Application Contributor Role': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '641177b8-a67a-45b9-a033-47bc880bb21e') - 'Managed Application Operator Role': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'c7393b34-138c-406f-901b-d8cf2b17e6ae') - 'Managed Applications Reader': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b9331d33-8a36-4f8c-b097-4f54124fdb44') - 'Monitoring Contributor': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '749f88d5-cbae-40b8-bcfc-e573ddc772fa') - 'Monitoring Reader': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '43d0d8ad-25c7-4714-9337-8ba259a9fe05') - 'Owner': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635') - 'Reader': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7') - 'Resource Policy Contributor': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '36243c78-bf99-498c-9df9-86d9f8d28608') - 'Role Based Access Control Administrator (Preview)': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168') - 'User Access Administrator': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9') -} - -resource privateCloud 'Microsoft.AVS/privateClouds@2021-12-01' existing = { - name: last(split(resourceId, '/')) -} - -resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for principalId in principalIds: { - name: guid(privateCloud.id, principalId, roleDefinitionIdOrName) - properties: { - description: description - roleDefinitionId: contains(builtInRoleNames, roleDefinitionIdOrName) ? builtInRoleNames[roleDefinitionIdOrName] : roleDefinitionIdOrName - principalId: principalId - principalType: !empty(principalType) ? any(principalType) : null - condition: !empty(condition) ? condition : null - conditionVersion: !empty(conditionVersion) && !empty(condition) ? conditionVersion : null - delegatedManagedIdentityResourceId: !empty(delegatedManagedIdentityResourceId) ? delegatedManagedIdentityResourceId : null - } - scope: privateCloud -}] diff --git a/modules/Microsoft.AVS/privateClouds/clusters/deploy.bicep b/modules/Microsoft.AVS/privateClouds/clusters/deploy.bicep index bc4e5059dc..f400449f8c 100644 --- a/modules/Microsoft.AVS/privateClouds/clusters/deploy.bicep +++ b/modules/Microsoft.AVS/privateClouds/clusters/deploy.bicep @@ -12,7 +12,7 @@ param sku object param privateCloudName string @description('Optional. The cluster size') -param clusterSize int = +param clusterSize int = @description('Optional. The datastores to create as part of the cluster.') param datastores array = [] @@ -26,6 +26,10 @@ param hosts array = [] @description('Optional. The placementPolicies to create as part of the cluster.') param placementPolicies array = [] +// ============= // +// Variables // +// ============= // + var enableReferencedModulesTelemetry = false // =============== // @@ -58,6 +62,18 @@ resource cluster 'Microsoft.AVS/privateClouds/clusters@2022-05-01' = { } } +module cluster_datastores 'datastores/deploy.bicep' = [for (datastore, index) in datastores: { + name: '${uniqueString(deployment().name)}-cluster-datastore-${index}' + params: { + privateCloudName: privateCloudName + clusterName: name + diskPoolVolume: contains(datastore, 'diskPoolVolume') ? datastore.diskPoolVolume : {} + name: datastore.name + netAppVolume: contains(datastore, 'netAppVolume') ? datastore.netAppVolume : {} + enableDefaultTelemetry: enableReferencedModulesTelemetry + } +}] + module cluster_placementPolicies 'placementPolicies/deploy.bicep' = [for (placementPolicy, index) in placementPolicies: { name: '${uniqueString(deployment().name)}-cluster-placementPolicy-${index}' params: { @@ -75,18 +91,6 @@ module cluster_placementPolicies 'placementPolicies/deploy.bicep' = [for (placem } }] -module cluster_datastores 'datastores/deploy.bicep' = [for (datastore, index) in datastores: { - name: '${uniqueString(deployment().name)}-cluster-datastore-${index}' - params: { - privateCloudName: privateCloudName - clusterName: name - diskPoolVolume: contains(datastore, 'diskPoolVolume') ? datastore.diskPoolVolume : {} - name: datastore.name - netAppVolume: contains(datastore, 'netAppVolume') ? datastore.netAppVolume : {} - enableDefaultTelemetry: enableReferencedModulesTelemetry - } -}] - // =========== // // Outputs // // =========== // diff --git a/modules/Microsoft.AVS/privateClouds/deploy.bicep b/modules/Microsoft.AVS/privateClouds/deploy.bicep index 258c9de77d..05be0abfd2 100644 --- a/modules/Microsoft.AVS/privateClouds/deploy.bicep +++ b/modules/Microsoft.AVS/privateClouds/deploy.bicep @@ -154,6 +154,10 @@ param vcenterPassword string = '' @description('Optional. The vmGroups to create as part of the privateCloud.') param vmGroups array = [] +// ============= // +// Variables // +// ============= // + var diagnosticsMetrics = [for metric in diagnosticMetricsToEnable: { category: metric timeGrain: null @@ -225,7 +229,7 @@ resource privateCloud_diagnosticSettings 'Microsoft.Insights/diagnosticsettings@ scope: privateCloud } -resource keyVault_lock 'Microsoft.Authorization/locks@2017-04-01' = if (!empty(lock)) { +resource privateCloud_lock 'Microsoft.Authorization/locks@2017-04-01' = if (!empty(lock)) { name: '${privateCloud.name}-${lock}-lock' properties: { level: any(lock) @@ -234,6 +238,16 @@ resource keyVault_lock 'Microsoft.Authorization/locks@2017-04-01' = if (!empty(l scope: privateCloud } +module privateCloud_addons 'addons/deploy.bicep' = [for (addon, index) in addons: { + name: '${uniqueString(deployment().name, location)}-privateCloud-addon-${index}' + params: { + privateCloudName: name + addonType: contains(addon, 'addonType') ? addon.addonType : '' + name: addon.name + enableDefaultTelemetry: enableReferencedModulesTelemetry + } +}] + module privateCloud_authorizations 'authorizations/deploy.bicep' = [for (authorization, index) in authorizations: { name: '${uniqueString(deployment().name, location)}-privateCloud-authorization-${index}' params: { @@ -253,12 +267,14 @@ module privateCloud_cloudLinks 'cloudLinks/deploy.bicep' = [for (cloudLink, inde } }] -module privateCloud_addons 'addons/deploy.bicep' = [for (addon, index) in addons: { - name: '${uniqueString(deployment().name, location)}-privateCloud-addon-${index}' +module privateCloud_clusters 'clusters/deploy.bicep' = [for (cluster, index) in clusters: { + name: '${uniqueString(deployment().name, location)}-privateCloud-cluster-${index}' params: { privateCloudName: name - addonType: contains(addon, 'addonType') ? addon.addonType : '' - name: addon.name + clusterSize: contains(cluster, 'clusterSize') ? cluster.clusterSize : + hosts: contains(cluster, 'hosts') ? cluster.hosts : [] + name: cluster.name + sku: cluster.sku enableDefaultTelemetry: enableReferencedModulesTelemetry } }] @@ -284,18 +300,6 @@ module privateCloud_hcxEnterpriseSites 'hcxEnterpriseSites/deploy.bicep' = [for } }] -module privateCloud_clusters 'clusters/deploy.bicep' = [for (cluster, index) in clusters: { - name: '${uniqueString(deployment().name, location)}-privateCloud-cluster-${index}' - params: { - privateCloudName: name - clusterSize: contains(cluster, 'clusterSize') ? cluster.clusterSize : - hosts: contains(cluster, 'hosts') ? cluster.hosts : [] - name: cluster.name - sku: cluster.sku - enableDefaultTelemetry: enableReferencedModulesTelemetry - } -}] - module privateCloud_scriptExecutions 'scriptExecutions/deploy.bicep' = [for (scriptExecution, index) in scriptExecutions: { name: '${uniqueString(deployment().name, location)}-privateCloud-scriptExecution-${index}' params: { @@ -313,6 +317,19 @@ module privateCloud_scriptExecutions 'scriptExecutions/deploy.bicep' = [for (scr } }] +module workloadNetworks_privateCloud_dhcpConfigurations 'workloadNetworks/dhcpConfigurations/deploy.bicep' = [for (dhcpConfiguration, index) in dhcpConfigurations: { + name: '${uniqueString(deployment().name, location)}-privateCloud-dhcpConfiguration-${index}' + params: { + privateCloudName: name + workloadNetworkName: 'default' + dhcpType: contains(dhcpConfiguration, 'dhcpType') ? dhcpConfiguration.dhcpType : '' + displayName: contains(dhcpConfiguration, 'displayName') ? dhcpConfiguration.displayName : '' + name: dhcpConfiguration.name + revision: contains(dhcpConfiguration, 'revision') ? dhcpConfiguration.revision : + enableDefaultTelemetry: enableReferencedModulesTelemetry + } +}] + module workloadNetworks_privateCloud_dnsServices 'workloadNetworks/dnsServices/deploy.bicep' = [for (dnsService, index) in dnsServices: { name: '${uniqueString(deployment().name, location)}-privateCloud-dnsService-${index}' params: { @@ -329,20 +346,6 @@ module workloadNetworks_privateCloud_dnsServices 'workloadNetworks/dnsServices/d } }] -module workloadNetworks_privateCloud_segments 'workloadNetworks/segments/deploy.bicep' = [for (segment, index) in segments: { - name: '${uniqueString(deployment().name, location)}-privateCloud-segment-${index}' - params: { - privateCloudName: name - workloadNetworkName: 'default' - connectedGateway: contains(segment, 'connectedGateway') ? segment.connectedGateway : '' - displayName: contains(segment, 'displayName') ? segment.displayName : '' - name: segment.name - revision: contains(segment, 'revision') ? segment.revision : - subnet: contains(segment, 'subnet') ? segment.subnet : {} - enableDefaultTelemetry: enableReferencedModulesTelemetry - } -}] - module workloadNetworks_privateCloud_dnsZones 'workloadNetworks/dnsZones/deploy.bicep' = [for (dnsZone, index) in dnsZones: { name: '${uniqueString(deployment().name, location)}-privateCloud-dnsZone-${index}' params: { @@ -359,55 +362,56 @@ module workloadNetworks_privateCloud_dnsZones 'workloadNetworks/dnsZones/deploy. } }] -module workloadNetworks_privateCloud_publicIPs 'workloadNetworks/publicIPs/deploy.bicep' = [for (publicIP, index) in publicIPs: { - name: '${uniqueString(deployment().name, location)}-privateCloud-publicIP-${index}' +module workloadNetworks_privateCloud_portMirroringProfiles 'workloadNetworks/portMirroringProfiles/deploy.bicep' = [for (portMirroringProfile, index) in portMirroringProfiles: { + name: '${uniqueString(deployment().name, location)}-privateCloud-portMirroringProfile-${index}' params: { privateCloudName: name workloadNetworkName: 'default' - displayName: contains(publicIP, 'displayName') ? publicIP.displayName : '' - name: publicIP.name - numberOfPublicIPs: contains(publicIP, 'numberOfPublicIPs') ? publicIP.numberOfPublicIPs : + destination: contains(portMirroringProfile, 'destination') ? portMirroringProfile.destination : '' + direction: contains(portMirroringProfile, 'direction') ? portMirroringProfile.direction : '' + displayName: contains(portMirroringProfile, 'displayName') ? portMirroringProfile.displayName : '' + name: portMirroringProfile.name + revision: contains(portMirroringProfile, 'revision') ? portMirroringProfile.revision : + source: contains(portMirroringProfile, 'source') ? portMirroringProfile.source : '' enableDefaultTelemetry: enableReferencedModulesTelemetry } }] -module workloadNetworks_privateCloud_vmGroups 'workloadNetworks/vmGroups/deploy.bicep' = [for (vmGroup, index) in vmGroups: { - name: '${uniqueString(deployment().name, location)}-privateCloud-vmGroup-${index}' +module workloadNetworks_privateCloud_publicIPs 'workloadNetworks/publicIPs/deploy.bicep' = [for (publicIP, index) in publicIPs: { + name: '${uniqueString(deployment().name, location)}-privateCloud-publicIP-${index}' params: { privateCloudName: name workloadNetworkName: 'default' - displayName: contains(vmGroup, 'displayName') ? vmGroup.displayName : '' - members: contains(vmGroup, 'members') ? vmGroup.members : [] - name: vmGroup.name - revision: contains(vmGroup, 'revision') ? vmGroup.revision : + displayName: contains(publicIP, 'displayName') ? publicIP.displayName : '' + name: publicIP.name + numberOfPublicIPs: contains(publicIP, 'numberOfPublicIPs') ? publicIP.numberOfPublicIPs : enableDefaultTelemetry: enableReferencedModulesTelemetry } }] -module workloadNetworks_privateCloud_portMirroringProfiles 'workloadNetworks/portMirroringProfiles/deploy.bicep' = [for (portMirroringProfile, index) in portMirroringProfiles: { - name: '${uniqueString(deployment().name, location)}-privateCloud-portMirroringProfile-${index}' +module workloadNetworks_privateCloud_segments 'workloadNetworks/segments/deploy.bicep' = [for (segment, index) in segments: { + name: '${uniqueString(deployment().name, location)}-privateCloud-segment-${index}' params: { privateCloudName: name workloadNetworkName: 'default' - destination: contains(portMirroringProfile, 'destination') ? portMirroringProfile.destination : '' - direction: contains(portMirroringProfile, 'direction') ? portMirroringProfile.direction : '' - displayName: contains(portMirroringProfile, 'displayName') ? portMirroringProfile.displayName : '' - name: portMirroringProfile.name - revision: contains(portMirroringProfile, 'revision') ? portMirroringProfile.revision : - source: contains(portMirroringProfile, 'source') ? portMirroringProfile.source : '' + connectedGateway: contains(segment, 'connectedGateway') ? segment.connectedGateway : '' + displayName: contains(segment, 'displayName') ? segment.displayName : '' + name: segment.name + revision: contains(segment, 'revision') ? segment.revision : + subnet: contains(segment, 'subnet') ? segment.subnet : {} enableDefaultTelemetry: enableReferencedModulesTelemetry } }] -module workloadNetworks_privateCloud_dhcpConfigurations 'workloadNetworks/dhcpConfigurations/deploy.bicep' = [for (dhcpConfiguration, index) in dhcpConfigurations: { - name: '${uniqueString(deployment().name, location)}-privateCloud-dhcpConfiguration-${index}' +module workloadNetworks_privateCloud_vmGroups 'workloadNetworks/vmGroups/deploy.bicep' = [for (vmGroup, index) in vmGroups: { + name: '${uniqueString(deployment().name, location)}-privateCloud-vmGroup-${index}' params: { privateCloudName: name workloadNetworkName: 'default' - dhcpType: contains(dhcpConfiguration, 'dhcpType') ? dhcpConfiguration.dhcpType : '' - displayName: contains(dhcpConfiguration, 'displayName') ? dhcpConfiguration.displayName : '' - name: dhcpConfiguration.name - revision: contains(dhcpConfiguration, 'revision') ? dhcpConfiguration.revision : + displayName: contains(vmGroup, 'displayName') ? vmGroup.displayName : '' + members: contains(vmGroup, 'members') ? vmGroup.members : [] + name: vmGroup.name + revision: contains(vmGroup, 'revision') ? vmGroup.revision : enableDefaultTelemetry: enableReferencedModulesTelemetry } }] diff --git a/modules/Microsoft.AVS/privateClouds/readme.md b/modules/Microsoft.AVS/privateClouds/readme.md deleted file mode 100644 index 51d26ff306..0000000000 --- a/modules/Microsoft.AVS/privateClouds/readme.md +++ /dev/null @@ -1,181 +0,0 @@ -# AVS PrivateClouds `[Microsoft.AVS/privateClouds]` - -This module deploys AVS PrivateClouds. -// TODO: Replace Resource and fill in description - -## Navigation - -- [Resource Types](#Resource-Types) -- [Parameters](#Parameters) -- [Outputs](#Outputs) -- [Cross-referenced modules](#Cross-referenced-modules) -- [Deployment examples](#Deployment-examples) - -## Resource Types - -| Resource Type | API Version | -| :-- | :-- | -| `Microsoft.Authorization/locks` | [2017-04-01](https://docs.microsoft.com/en-us/azure/templates/Microsoft.Authorization/2017-04-01/locks) | -| `Microsoft.Authorization/roleAssignments` | [2022-04-01](https://docs.microsoft.com/en-us/azure/templates/Microsoft.Authorization/2022-04-01/roleAssignments) | -| `Microsoft.AVS/privateClouds` | [2021-12-01](https://docs.microsoft.com/en-us/azure/templates/Microsoft.AVS/2021-12-01/privateClouds) | -| `Microsoft.Insights/diagnosticSettings` | [2021-05-01-preview](https://docs.microsoft.com/en-us/azure/templates/Microsoft.Insights/2021-05-01-preview/diagnosticSettings) | - -## Parameters - -**Required parameters** -| Parameter Name | Type | Description | -| :-- | :-- | :-- | -| `name` | string | Name of the private cloud | -| `sku` | object | The private cloud SKU | - -**Optional parameters** -| Parameter Name | Type | Default Value | Allowed Values | Description | -| :-- | :-- | :-- | :-- | :-- | -| `availability` | object | | | Properties describing how the cloud is distributed across availability zones | -| `circuit` | object | | | An ExpressRoute Circuit | -| `diagnosticEventHubAuthorizationRuleId` | string | | | Resource ID of the diagnostic event hub authorization rule for the Event Hubs namespace in which the event hub should be created or streamed to. | -| `diagnosticEventHubName` | string | | | Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub. | -| `diagnosticLogCategoriesToEnable` | array | `[CapacityLatest, DiskUsedPercentage, EffectiveCpuAverage, EffectiveMemAverage, OverheadAverage, TotalMbAverage, UsageAverage, UsedLatest]` | `[CapacityLatest, DiskUsedPercentage, EffectiveCpuAverage, EffectiveMemAverage, OverheadAverage, TotalMbAverage, UsageAverage, UsedLatest]` | The name of logs that will be streamed. | -| `diagnosticLogsRetentionInDays` | int | `365` | | Specifies the number of days that logs will be kept for; a value of 0 will retain data indefinitely. | -| `diagnosticMetricsToEnable` | array | `[AllMetrics]` | `[AllMetrics]` | The name of metrics that will be streamed. | -| `diagnosticSettingsName` | string | `[format('{0}-diagnosticSettings', parameters('name'))]` | | The name of the diagnostic setting, if deployed. | -| `diagnosticStorageAccountId` | string | | | Resource ID of the diagnostic storage account. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub. | -| `diagnosticWorkspaceId` | string | | | Resource ID of the diagnostic log analytics workspace. For security reasons, it is recommended to set diagnostic settings to send data to either storage account, log analytics workspace or event hub. | -| `enableDefaultTelemetry` | bool | `True` | | Enable telemetry via the Customer Usage Attribution ID (GUID). | -| `encryption` | object | | | Customer managed key encryption, can be enabled or disabled | -| `identity` | object | | | The identity of the private cloud, if configured. | -| `identitySources` | array | | | vCenter Single Sign On Identity Sources | -| `internet` | string | `'Disabled'` | `[Disabled, Enabled]` | Connectivity to internet is enabled or disabled | -| `location` | string | `[resourceGroup().location]` | | Location for all Resources. | -| `lock` | string | | `[CanNotDelete, ReadOnly]` | Specify the type of lock. | -| `managementCluster` | object | | | The default cluster used for management | -| `networkBlock` | string | | | The block of addresses should be unique across VNet in your subscription as well as on-premise. Make sure the CIDR format is conformed to (A.B.C.D/X) where A,B,C,D are between 0 and 255, and X is between 0 and 22 | -| `nsxtPassword` | secureString | | | Optionally, set the NSX-T Manager password when the private cloud is created | -| `roleAssignments` | array | | | Array of role assignment objects that contain the 'roleDefinitionIdOrName' and 'principalId' to define RBAC role assignments on this resource. In the roleDefinitionIdOrName attribute, you can provide either the display name of the role definition, or its fully qualified ID in the following format: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11'. | -| `secondaryCircuit` | object | | | A secondary expressRoute circuit from a separate AZ. Only present in a stretched private cloud | -| `tags` | object | | | Resource tags | -| `vcenterPassword` | secureString | | | Optionally, set the vCenter admin password when the private cloud is created | - - -### Parameter Usage: `` - -// TODO: Fill in Parameter usage - -### Parameter Usage: `roleAssignments` - -Create a role assignment for the given resource. If you want to assign a service principal / managed identity that is created in the same deployment, make sure to also specify the `'principalType'` parameter and set it to `'ServicePrincipal'`. This will ensure the role assignment waits for the principal's propagation in Azure. - -
- -Parameter JSON format - -```json -"roleAssignments": { - "value": [ - { - "roleDefinitionIdOrName": "Reader", - "description": "Reader Role Assignment", - "principalIds": [ - "12345678-1234-1234-1234-123456789012", // object 1 - "78945612-1234-1234-1234-123456789012" // object 2 - ] - }, - { - "roleDefinitionIdOrName": "/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11", - "principalIds": [ - "12345678-1234-1234-1234-123456789012" // object 1 - ], - "principalType": "ServicePrincipal" - } - ] -} -``` - -
- -
- -Bicep format - -```bicep -roleAssignments: [ - { - roleDefinitionIdOrName: 'Reader' - description: 'Reader Role Assignment' - principalIds: [ - '12345678-1234-1234-1234-123456789012' // object 1 - '78945612-1234-1234-1234-123456789012' // object 2 - ] - } - { - roleDefinitionIdOrName: '/providers/Microsoft.Authorization/roleDefinitions/c2f4ef07-c644-48eb-af81-4b1b4947fb11' - principalIds: [ - '12345678-1234-1234-1234-123456789012' // object 1 - ] - principalType: 'ServicePrincipal' - } -] -``` - -
-

- -### Parameter Usage: `tags` - -Tag names and tag values can be provided as needed. A tag can be left without a value. - -

- -Parameter JSON format - -```json -"tags": { - "value": { - "Environment": "Non-Prod", - "Contact": "test.user@testcompany.com", - "PurchaseOrder": "1234", - "CostCenter": "7890", - "ServiceName": "DeploymentValidation", - "Role": "DeploymentValidation" - } -} -``` - -
- -
- -Bicep format - -```bicep -tags: { - Environment: 'Non-Prod' - Contact: 'test.user@testcompany.com' - PurchaseOrder: '1234' - CostCenter: '7890' - ServiceName: 'DeploymentValidation' - Role: 'DeploymentValidation' -} -``` - -
-

- -## Outputs - -| Output Name | Type | Description | -| :-- | :-- | :-- | -| `name` | string | The name of the privateCloud. | -| `resourceGroupName` | string | The name of the resource group the privateCloud was created in. | -| `resourceId` | string | The resource ID of the privateCloud. | - -## Cross-referenced modules - -_None_ - -## Deployment examples - -The following module usage examples are retrieved from the content of the files hosted in the module's `.test` folder. - >**Note**: The name of each example is based on the name of the file from which it is taken. - - >**Note**: Each example lists all the required parameters first, followed by the rest - each in alphabetical order. diff --git a/utilities/tools/REST2CARML/private/extension/Set-DiagnosticModuleData.ps1 b/utilities/tools/REST2CARML/private/extension/Set-DiagnosticModuleData.ps1 index d783e0530e..1f49f77633 100644 --- a/utilities/tools/REST2CARML/private/extension/Set-DiagnosticModuleData.ps1 +++ b/utilities/tools/REST2CARML/private/extension/Set-DiagnosticModuleData.ps1 @@ -85,15 +85,18 @@ function Set-DiagnosticModuleData { } ) - $diagnosticResource = @( - "resource $($resourceTypeSingular)_diagnosticSettings 'Microsoft.Insights/diagnosticsettings@2021-05-01-preview' = if ((!empty(diagnosticStorageAccountId)) || (!empty(diagnosticWorkspaceId)) || (!empty(diagnosticEventHubAuthorizationRuleId)) || (!empty(diagnosticEventHubName))) {" - ' name: diagnosticSettingsName' - ' properties: {' - ' storageAccountId: !empty(diagnosticStorageAccountId) ? diagnosticStorageAccountId : null' - ' workspaceId: !empty(diagnosticWorkspaceId) ? diagnosticWorkspaceId : null' - ' eventHubAuthorizationRuleId: !empty(diagnosticEventHubAuthorizationRuleId) ? diagnosticEventHubAuthorizationRuleId : null' - ' eventHubName: !empty(diagnosticEventHubName) ? diagnosticEventHubName : null' - ) + $diagnosticResource = @{ + name = "$($resourceTypeSingular)_diagnosticSettings" + content = @( + "resource $($resourceTypeSingular)_diagnosticSettings 'Microsoft.Insights/diagnosticsettings@2021-05-01-preview' = if ((!empty(diagnosticStorageAccountId)) || (!empty(diagnosticWorkspaceId)) || (!empty(diagnosticEventHubAuthorizationRuleId)) || (!empty(diagnosticEventHubName))) {" + ' name: diagnosticSettingsName' + ' properties: {' + ' storageAccountId: !empty(diagnosticStorageAccountId) ? diagnosticStorageAccountId : null' + ' workspaceId: !empty(diagnosticWorkspaceId) ? diagnosticWorkspaceId : null' + ' eventHubAuthorizationRuleId: !empty(diagnosticEventHubAuthorizationRuleId) ? diagnosticEventHubAuthorizationRuleId : null' + ' eventHubName: !empty(diagnosticEventHubName) ? diagnosticEventHubName : null' + ) + } # Metric-specific if ($diagnosticOptions.Metrics) { @@ -112,20 +115,22 @@ function Set-DiagnosticModuleData { ) } ) - $ModuleData.variables += @( - 'var diagnosticsMetrics = [for metric in diagnosticMetricsToEnable: {' - ' category: metric' - ' timeGrain: null' - ' enabled: true' - ' retentionPolicy: {' - ' enabled: true' - ' days: diagnosticLogsRetentionInDays' - ' }' - '}]' - '' - ) + $ModuleData.variables += @{ + name = 'diagnosticsMetrics' + content = @( + 'var diagnosticsMetrics = [for metric in diagnosticMetricsToEnable: {' + ' category: metric' + ' timeGrain: null' + ' enabled: true' + ' retentionPolicy: {' + ' enabled: true' + ' days: diagnosticLogsRetentionInDays' + ' }' + '}]' + ) + } - $diagnosticResource += ' metrics: diagnosticsMetrics' + $diagnosticResource.content += ' metrics: diagnosticsMetrics' } # Log-specific @@ -140,22 +145,24 @@ function Set-DiagnosticModuleData { default = $diagnosticOptions.Logs } ) - $ModuleData.variables += @( - 'var diagnosticsLogs = [for category in diagnosticLogCategoriesToEnable: {' - ' category: category' - ' enabled: true' - ' retentionPolicy: {' - ' enabled: true' - ' days: diagnosticLogsRetentionInDays' - ' }' - '}]' - '' - ) + $ModuleData.variables += @{ + name = 'diagnosticsLogs' + content = @( + 'var diagnosticsLogs = [for category in diagnosticLogCategoriesToEnable: {' + ' category: category' + ' enabled: true' + ' retentionPolicy: {' + ' enabled: true' + ' days: diagnosticLogsRetentionInDays' + ' }' + '}]' + ) + } - $diagnosticResource += ' logs: diagnosticsLogs' + $diagnosticResource.content += ' logs: diagnosticsLogs' } - $diagnosticResource += @( + $diagnosticResource.content += @( ' }' " scope: $resourceTypeSingular" '}' diff --git a/utilities/tools/REST2CARML/private/extension/Set-LockModuleData.ps1 b/utilities/tools/REST2CARML/private/extension/Set-LockModuleData.ps1 index e15e939f06..bf7f7033d1 100644 --- a/utilities/tools/REST2CARML/private/extension/Set-LockModuleData.ps1 +++ b/utilities/tools/REST2CARML/private/extension/Set-LockModuleData.ps1 @@ -60,17 +60,20 @@ function Set-LockModuleData { } ) - $ModuleData.resources += @( - "resource keyVault_lock 'Microsoft.Authorization/locks@2017-04-01' = if (!empty(lock)) {" - " name: '`${$resourceTypeSingular.name}-`${lock}-lock'" - ' properties: {' - ' level: any(lock)' - " notes: lock == 'CanNotDelete' ? 'Cannot delete resource or child resources.' : 'Cannot modify the resource or child resources.'" - ' }' - ' scope: {0}' -f $resourceTypeSingular - '}' - '' - ) + $ModuleData.resources += @{ + name = "$($resourceTypeSingular)_lock" + content = @( + "resource $($resourceTypeSingular)_lock 'Microsoft.Authorization/locks@2017-04-01' = if (!empty(lock)) {" + " name: '`${$resourceTypeSingular.name}-`${lock}-lock'" + ' properties: {' + ' level: any(lock)' + " notes: lock == 'CanNotDelete' ? 'Cannot delete resource or child resources.' : 'Cannot modify the resource or child resources.'" + ' }' + ' scope: {0}' -f $resourceTypeSingular + '}' + '' + ) + } } end { diff --git a/utilities/tools/REST2CARML/private/extension/Set-PrivateEndpointModuleData.ps1 b/utilities/tools/REST2CARML/private/extension/Set-PrivateEndpointModuleData.ps1 index 7a5ebd8d10..26769e9011 100644 --- a/utilities/tools/REST2CARML/private/extension/Set-PrivateEndpointModuleData.ps1 +++ b/utilities/tools/REST2CARML/private/extension/Set-PrivateEndpointModuleData.ps1 @@ -61,28 +61,31 @@ function Set-PrivateEndpointModuleData { } ) - $ModuleData.resources += @( - "module $($resourceTypeSingular)_privateEndpoints '../../Microsoft.Network/privateEndpoints/deploy.bicep' = [for (privateEndpoint,index) in privateEndpoints: {" - " name: '`${uniqueString(deployment().name, location)}-$resourceTypeSingular-PrivateEndpoint-`${index}'" - ' params: {' - ' groupIds: [' - ' privateEndpoint.service' - ' ]' - " name: contains(privateEndpoint,'name') ? privateEndpoint.name : 'pe-`${last(split($resourceTypeSingular.id, '/'))}-`${privateEndpoint.service}-`${index}'" - ' serviceResourceId: {0}.id' -f $resourceTypeSingular - ' subnetResourceId: privateEndpoint.subnetResourceId' - ' enableDefaultTelemetry: enableReferencedModulesTelemetry' - " location: reference(split(privateEndpoint.subnetResourceId,'/subnets/')[0], '2020-06-01', 'Full').location" - " lock: contains(privateEndpoint,'lock') ? privateEndpoint.lock : lock" - " privateDnsZoneGroup: contains(privateEndpoint,'privateDnsZoneGroup') ? privateEndpoint.privateDnsZoneGroup : {}" - " roleAssignments: contains(privateEndpoint,'roleAssignments') ? privateEndpoint.roleAssignments : []" - " tags: contains(privateEndpoint,'tags') ? privateEndpoint.tags : {}" - " manualPrivateLinkServiceConnections: contains(privateEndpoint,'manualPrivateLinkServiceConnections') ? privateEndpoint.manualPrivateLinkServiceConnections : []" - " customDnsConfigs: contains(privateEndpoint,'customDnsConfigs') ? privateEndpoint.customDnsConfigs : []" - ' }' - '}]' - '' - ) + $ModuleData.modules += @{ + name = "$($resourceTypeSingular)_privateEndpoints" + content = @( + "module $($resourceTypeSingular)_privateEndpoints '../../Microsoft.Network/privateEndpoints/deploy.bicep' = [for (privateEndpoint,index) in privateEndpoints: {" + " name: '`${uniqueString(deployment().name, location)}-$resourceTypeSingular-PrivateEndpoint-`${index}'" + ' params: {' + ' groupIds: [' + ' privateEndpoint.service' + ' ]' + " name: contains(privateEndpoint,'name') ? privateEndpoint.name : 'pe-`${last(split($resourceTypeSingular.id, '/'))}-`${privateEndpoint.service}-`${index}'" + ' serviceResourceId: {0}.id' -f $resourceTypeSingular + ' subnetResourceId: privateEndpoint.subnetResourceId' + ' enableDefaultTelemetry: enableReferencedModulesTelemetry' + " location: reference(split(privateEndpoint.subnetResourceId,'/subnets/')[0], '2020-06-01', 'Full').location" + " lock: contains(privateEndpoint,'lock') ? privateEndpoint.lock : lock" + " privateDnsZoneGroup: contains(privateEndpoint,'privateDnsZoneGroup') ? privateEndpoint.privateDnsZoneGroup : {}" + " roleAssignments: contains(privateEndpoint,'roleAssignments') ? privateEndpoint.roleAssignments : []" + " tags: contains(privateEndpoint,'tags') ? privateEndpoint.tags : {}" + " manualPrivateLinkServiceConnections: contains(privateEndpoint,'manualPrivateLinkServiceConnections') ? privateEndpoint.manualPrivateLinkServiceConnections : []" + " customDnsConfigs: contains(privateEndpoint,'customDnsConfigs') ? privateEndpoint.customDnsConfigs : []" + ' }' + '}]' + '' + ) + } } end { diff --git a/utilities/tools/REST2CARML/private/extension/Set-RoleAssignmentsModuleData.ps1 b/utilities/tools/REST2CARML/private/extension/Set-RoleAssignmentsModuleData.ps1 index 84d3776761..106f0fd537 100644 --- a/utilities/tools/REST2CARML/private/extension/Set-RoleAssignmentsModuleData.ps1 +++ b/utilities/tools/REST2CARML/private/extension/Set-RoleAssignmentsModuleData.ps1 @@ -71,21 +71,24 @@ function Set-RoleAssignmentsModuleData { } ) - $ModuleData.resources += @( - "module $($resourceTypeSingular)_roleAssignments '.bicep/nested_roleAssignments.bicep' = [for (roleAssignment,index) in roleAssignments: {" - " name: '`${uniqueString(deployment().name, location)}-$resourceTypeSingular-Rbac-`${index}'" - ' params: {' - " description: contains(roleAssignment,'description') ? roleAssignment.description : ''" - ' principalIds: roleAssignment.principalIds' - " principalType: contains(roleAssignment,'principalType') ? roleAssignment.principalType : ''" - ' roleDefinitionIdOrName: roleAssignment.roleDefinitionIdOrName' - " condition: contains(roleAssignment,'condition') ? roleAssignment.condition : ''" - " delegatedManagedIdentityResourceId: contains(roleAssignment,'delegatedManagedIdentityResourceId') ? roleAssignment.delegatedManagedIdentityResourceId : ''" - " resourceId: $resourceTypeSingular.id" - ' }' - '}]' - '' - ) + $ModuleData.modules += @{ + name = "$($resourceTypeSingular)_roleAssignments" + content = @( + "module $($resourceTypeSingular)_roleAssignments '.bicep/nested_roleAssignments.bicep' = [for (roleAssignment,index) in roleAssignments: {" + " name: '`${uniqueString(deployment().name, location)}-$resourceTypeSingular-Rbac-`${index}'" + ' params: {' + " description: contains(roleAssignment,'description') ? roleAssignment.description : ''" + ' principalIds: roleAssignment.principalIds' + " principalType: contains(roleAssignment,'principalType') ? roleAssignment.principalType : ''" + ' roleDefinitionIdOrName: roleAssignment.roleDefinitionIdOrName' + " condition: contains(roleAssignment,'condition') ? roleAssignment.condition : ''" + " delegatedManagedIdentityResourceId: contains(roleAssignment,'delegatedManagedIdentityResourceId') ? roleAssignment.delegatedManagedIdentityResourceId : ''" + " resourceId: $resourceTypeSingular.id" + ' }' + '}]' + '' + ) + } $fileContent = @() $rawContent = Get-Content -Path (Join-Path $script:src 'nested_roleAssignments.bicep') -Raw diff --git a/utilities/tools/REST2CARML/private/module/Expand-DeploymentBlock.ps1 b/utilities/tools/REST2CARML/private/module/Expand-DeploymentBlock.ps1 index 6ddf91616b..9733b356ee 100644 --- a/utilities/tools/REST2CARML/private/module/Expand-DeploymentBlock.ps1 +++ b/utilities/tools/REST2CARML/private/module/Expand-DeploymentBlock.ps1 @@ -30,6 +30,25 @@ function Expand-DeploymentBlock { $relevantProperties = $DeclarationBlock.content | Where-Object { (Get-LineIndentation $_) -eq $topLevelIndent -and $_ -notlike "*$($NestedType): {*" -and $_ -like '*:*' } $topLevelElementNames = $relevantProperties | ForEach-Object { ($_ -split ':')[0].Trim() } + ########################################### + ## Collect specification information ## + ########################################### + switch ($NestedType) { + 'properties' { + $declarationElem = $declarationBlock.content[0] -split ' ' + $DeclarationBlock['name'] = $declarationElem[1] + $DeclarationBlock['type'] = ($declarationElem[2] -split '@')[0].Trim("'") + $DeclarationBlock['version'] = (($declarationElem[2] -split '@')[1])[0..9] -join '' # The date always has 10 characters + break + } + 'params' { + $declarationElem = $declarationBlock.content[0] -split ' ' + $DeclarationBlock['name'] = $declarationElem[1] + $DeclarationBlock['path'] = $declarationElem[2].Trim("'") + break + } + } + #################################### ## Collect top level elements ## #################################### diff --git a/utilities/tools/REST2CARML/private/module/Get-FormattedModuleParameter.ps1 b/utilities/tools/REST2CARML/private/module/Get-FormattedModuleParameter.ps1 index 90dc3a0522..5e37188b7d 100644 --- a/utilities/tools/REST2CARML/private/module/Get-FormattedModuleParameter.ps1 +++ b/utilities/tools/REST2CARML/private/module/Get-FormattedModuleParameter.ps1 @@ -24,6 +24,7 @@ function Get-FormattedModuleParameter { # description (optional) # ---------------------- + # TODO: Add logic to always add a finishing '.' if missing if ($ParameterData.description) { # For the description we have to escape any single quote that is not already escaped (i.e., negative lookbehind) if ($ParameterData.description -match '^\w+\. .+' ) { diff --git a/utilities/tools/REST2CARML/private/module/Get-LinkedChildModuleList.ps1 b/utilities/tools/REST2CARML/private/module/Get-LinkedChildModuleList.ps1 new file mode 100644 index 0000000000..c53a51af28 --- /dev/null +++ b/utilities/tools/REST2CARML/private/module/Get-LinkedChildModuleList.ps1 @@ -0,0 +1,47 @@ +function Get-LinkedChildModuleList { + + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string] $FullResourceType, + + [Parameter(Mandatory = $true)] + [array] $FullModuleData + ) + + begin { + Write-Debug ('{0} entered' -f $MyInvocation.MyCommand) + } + + process { + # Collect child-resource information + $linkedChildren = $fullmoduleData | Where-Object { + # Is nested + $_.identifier -like "$FullResourceType/*" -and + # Is direct child + (($_.identifier -split '/').Count -eq (($FullResourceType -split '/').Count + 1) + ) + } + ## Add indirect child (via proxy resource) (i.e. it's a nested-nested resources who's parent has no individual specification/JSONFilePath). + # TODO: Is that always true? What if the data is specified in one file? + $indirectChildren = $FullModuleData | Where-Object { + # Is nested + $_.identifier -like "$FullResourceType/*" -and + # Is indirect child + (($_.identifier -split '/').Count -eq (($FullResourceType -split '/').Count + 2)) + } | Where-Object { + # If the child's parent's parentUrlPath is empty, this parent has no PUT rest command which indicates it cannot be created independently + [String]::IsNullOrEmpty($_.metadata.parentUrlPath) + } + + if ($indirectChildren) { + $linkedChildren += $indirectChildren + } + + return $linkedChildren + } + + end { + Write-Debug ('{0} exited' -f $MyInvocation.MyCommand) + } +} diff --git a/utilities/tools/REST2CARML/private/module/Get-TemplateChildModuleContent.ps1 b/utilities/tools/REST2CARML/private/module/Get-TemplateChildModuleContent.ps1 new file mode 100644 index 0000000000..23d5c82c06 --- /dev/null +++ b/utilities/tools/REST2CARML/private/module/Get-TemplateChildModuleContent.ps1 @@ -0,0 +1,323 @@ +<# +.SYNOPSIS +Generate the child-module's template content based on the given module data. + +.DESCRIPTION +Generate the child-module's template content based on the given module data. + +.PARAMETER FullResourceType +Mandatory. The complete ResourceType identifier to update the template for (e.g., 'Microsoft.Storage/storageAccounts'). + +.PARAMETER ResourceType +Mandatory. The resource type without the provider namespace (e.g., 'storageAccounts') + +.PARAMETER ResourceTypeSingular +Optional. The 'singular' version of the resource type. For example 'container' instead of 'containers'. + +.PARAMETER ModuleData +Mandatory. The module data to fetch the data for this section from & then format it propertly for the template. + +Expects an array with objects like: +Name Value +---- ----- +parameters {name, identity, type, properties…} +outputs {} +additionalFiles {} +modules {} +variables {diagnosticsMetrics, diagnosticsLogs} +resources {privateCloud_diagnosticSettings, privateCloud_lock} +isSingleton False +additionalParameters {diagnosticLogsRetentionInDays, diagnosticStorageAccountId, diagnosticWorkspaceId, diagnosticEventHubAuthorizationRuleId…} + +.PARAMETER LinkedChildren +Optional. Information about any child-module of the current resource type. Used to generate proper module references. + +Expects an array with objects like: + +Name Value +---- ----- +identifier Microsoft.AVS/privateClouds/cloudLinks +data {parameters, outputs, additionalFiles, modules…} +metadata {urlPath, jsonFilePath, parentUrlPath} +identifier Microsoft.AVS/privateClouds/hcxEnterpriseSites +data {parameters, outputs, additionalFiles, modules…} +metadata {urlPath, jsonFilePath, parentUrlPath} +identifier Microsoft.AVS/privateClouds/authorizations +data {parameters, outputs, additionalFiles, modules…} +metadata {urlPath, jsonFilePath, parentUrlPath} + +.PARAMETER LocationParameterExists +Mandatory. An indicator whether the template will contain a 'location' parameter. Only then we can reference it in e.g., deployment names. + +.PARAMETER ExistingTemplateContent +Optional. The prepared content of an existing template, if any. + +Expects an array with objects like: + +Name Value +---- ----- +modules {privateCloud_cloudLinks, privateCloud_hcxEnterpriseSites, privateCloud_authorizations, privateCloud_scriptExecutions…} +variables {diagnosticsMetrics, diagnosticsLogs, enableReferencedModulesTelemetry} +parameters {name, sku, addons, authorizations…} +outputs {name, resourceId, resourceGroupName} +resources {defaultTelemetry, privateCloud, privateCloud_diagnosticSettings, privateCloud_lock} + +.PARAMETER ParentResourceTypes +Optional. The name of any parent resource type. (e.g., @('privateClouds', 'clusters') + +.EXAMPLE +$contentInputObject = @{ + FullResourceType = 'Microsoft.AVS/privateClouds/clusters/datastores' + ResourceType = 'privateClouds/clusters/datastores' + ResourceTypeSingular = 'datastore' + LinkedChildren = @(@{...}, (...)) + ModuleData = @(@{...}, (...)) + LocationParameterExists = $true + ExistingTemplateContent = @(@{...}, (...)) + ParentResourceTypes = @('privateClouds', 'clusters') +} +Get-TemplateChildModuleContent @contentInputObject + +Get the formatted template content for resource type 'Microsoft.AVS/privateClouds/clusters/datastores' based on the given data - including an existing template's data. The output looks something like: + +```bicep + +(...) +``` +#> +function Get-TemplateChildModuleContent { + + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string] $FullResourceType, + + [Parameter(Mandatory = $true)] + [string] $ResourceType, + + [Parameter(Mandatory = $false)] + [string] $ResourceTypeSingular = ((Get-ResourceTypeSingularName -ResourceType $ResourceType) -split '/')[-1], + + [Parameter(Mandatory = $false)] + [array] $LinkedChildren = @(), + + [Parameter(Mandatory = $true)] + [array] $ModuleData, + + [Parameter(Mandatory = $true)] + [bool] $LocationParameterExists, + + [Parameter(Mandatory = $false)] + [array] $ExistingTemplateContent = @(), + + [Parameter(Mandatory = $false)] + [array] $ParentResourceTypes = @() + ) + + begin { + Write-Debug ('{0} entered' -f $MyInvocation.MyCommand) + } + + process { + + ##################################### + ## Add child-module references ## + ##################################### + $templateContent = @() + + foreach ($dataBlock in ($linkedChildren | Sort-Object -Property 'identifier')) { + $childResourceType = ($dataBlock.identifier -split '/')[-1] + + $hasProxyParent = [String]::IsNullOrEmpty($dataBlock.metadata.parentUrlPath) + if ($hasProxyParent) { + $proxyParentName = Split-Path (Split-Path $dataBlock.identifier -Parent) -Leaf + } + + $moduleName = '{0}{1}_{2}' -f ($hasProxyParent ? "$($proxyParentName)_" : ''), $resourceTypeSingular, $childResourceType + $modulePath = '{0}{1}/deploy.bicep' -f ($hasProxyParent ? "$proxyParentName/" : ''), $childResourceType + + $existingModuleData = $ExistingTemplateContent.modules | Where-Object { $_.name -eq $moduleName -and $_.path -eq $modulePath } + + # Differentiate 'singular' children (like 'blobservices') vs. 'multiple' chilren (like 'containers') + if ($ModuleData.isSingleton) { + $templateContent += @( + "module $moduleName '$modulePath' = {" + ) + + if ($existingModuleData.topLevelElements.name -notcontains 'name') { + $templateContent += " name: '`${uniqueString(deployment().name$($LocationParameterExists ? ', location' : ''))}-$($resourceTypeSingular)-$($childResourceType)'" + } else { + $existingParam = $existingModuleData.topLevelElements | Where-Object { $_.name -eq 'name' } + $templateContent += $existingParam.content + } + + $templateContent += ' params: {' + $templateContent += @() + + $alreadyAddedParams = @() + + # All param names of parents + foreach ($parentResourceType in $parentResourceTypes) { + $parentParamName = ((Get-ResourceTypeSingularName -ResourceType $parentResourceType) -split '/')[-1] + $templateContent += ' {0}Name: {0}Name' -f $parentParamName + $alreadyAddedParams += $parentParamName + } + # Itself + $selfParamName = ((Get-ResourceTypeSingularName -ResourceType ($FullResourceType -split '/')[-1]) -split '/')[-1] + $templateContent += ' {0}Name: name' -f $selfParamName + $alreadyAddedParams += $selfParamName + + # Any proxy default if any + if ($hasProxyParent) { + $proxyDefaultValue = ($dataBlock.metadata.urlPath -split '\/')[-3] + $proxyParamName = Get-ResourceTypeSingularName -ResourceType ($proxyParentName -split '/')[-1] + $templateContent += " {0}Name: '{1}'" -f $proxyParamName, $proxyDefaultValue + $alreadyAddedParams += $proxyParamName + } + + # Add primary child parameters + $allParam = $dataBlock.data.parameters + $dataBlock.data.additionalParameters + foreach ($parameter in (($allParam | Where-Object { $_.Level -in @(0, 1) -and $_.name -ne 'properties' -and ([String]::IsNullOrEmpty($_.Parent) -or $_.Parent -eq 'properties') }) | Sort-Object -Property 'Name')) { + $wouldBeParameter = Get-FormattedModuleParameter -ParameterData $parameter | Where-Object { $_ -like 'param *' } | ForEach-Object { $_ -replace 'param ', '' } + $wouldBeParamElem = $wouldBeParameter -split ' = ' + $parameter.name = ($wouldBeParamElem -split ' ')[0] + + if ($existingModuleData.nestedElements.name -notcontains $parameter.name) { + $existingParam = $existingModuleData.nestedElements | Where-Object { $_.name -eq $parameter.name } + if ($alreadyAddedParams -notcontains $existingParam.name) { + $templateContent += $existingParam.content + } + continue + } + + if ($wouldBeParamElem.count -gt 1) { + # With default + + if ($parameter.name -eq 'lock') { + # Special handling as we pass the parameter down to the child + $templateContent += " $($parameter.name): contains($($childResourceType), 'lock') ? $($childResourceType).lock : lock" + $alreadyAddedParams += $parameter.name + continue + } + + $wouldBeParamValue = $wouldBeParamElem[1] + + # Special case, location function - should reference a location parameter instead + if ($wouldBeParamValue -like '*().location') { + $wouldBeParamValue = 'location' + } + + $templateContent += " $($parameter.name): contains($($childResourceType), '$($parameter.name)') ? $($childResourceType).$($parameter.name) : $($wouldBeParamValue)" + $alreadyAddedParams += $parameter.name + } else { + # No default + $templateContent += " $($parameter.name): $($childResourceType).$($parameter.name)" + $alreadyAddedParams += $parameter.name + } + } + + $templateContent += @( + # Special handling as we pass the variable down to the child + ' enableDefaultTelemetry: enableReferencedModulesTelemetry' + ' }' + '}' + '' + ) + } else { + + $childResourceTypeSingular = Get-ResourceTypeSingularName -ResourceType $childResourceType + + $templateContent += @( + "module $moduleName '$modulePath' = [for ($($childResourceTypeSingular), index) in $($childResourceType): {" + ) + + if ($existingModuleData.topLevelElements.name -notcontains 'name') { + $templateContent += " name: '`${uniqueString(deployment().name$($LocationParameterExists ? ', location' : ''))}-$($resourceTypeSingular)-$($childResourceTypeSingular)-`${index}'" + } else { + $existingParam = $existingModuleData.topLevelElements | Where-Object { $_.name -eq 'name' } + $templateContent += $existingParam.content + } + + $templateContent += ' params: {' + $templateContent += @() + + $alreadyAddedParams = @() + + # All param names of parents + foreach ($parentResourceType in $parentResourceTypes) { + $parentParamName = ((Get-ResourceTypeSingularName -ResourceType $parentResourceType) -split '/')[-1] + $templateContent += ' {0}Name: {0}Name' -f $parentParamName + $alreadyAddedParams += $parentParamName + } + # Itself + $selfParamName = ((Get-ResourceTypeSingularName -ResourceType ($FullResourceType -split '/')[-1]) -split '/')[-1] + $templateContent += ' {0}Name: name' -f $selfParamName + $alreadyAddedParams += $selfParamName + + # Any proxy default if any + if ($hasProxyParent) { + $proxyDefaultValue = ($dataBlock.metadata.urlPath -split '\/')[-3] + $proxyParamName = Get-ResourceTypeSingularName -ResourceType ($proxyParentName -split '/')[-1] + $templateContent += " {0}Name: '{1}'" -f $proxyParamName, $proxyDefaultValue + $alreadyAddedParams += $proxyParamName + } + + # Add primary child parameters + $allParam = $dataBlock.data.parameters + $dataBlock.data.additionalParameters + foreach ($parameter in (($allParam | Where-Object { $_.Level -in @(0, 1) -and $_.name -ne 'properties' -and ([String]::IsNullOrEmpty($_.Parent) -or $_.Parent -eq 'properties') }) | Sort-Object -Property 'Name')) { + $wouldBeParameter = Get-FormattedModuleParameter -ParameterData $parameter | Where-Object { $_ -like 'param *' } | ForEach-Object { $_ -replace 'param ', '' } + $wouldBeParamElem = $wouldBeParameter -split ' = ' + $parameterName = ($wouldBeParamElem -split ' ')[0] + + # If the existing content already specifies the parameter, let's use that one instead of generating a new + if ($existingModuleData.nestedElements.name -contains $parameterName) { + $existingParam = $existingModuleData.nestedElements | Where-Object { $_.name -eq $parameterName } + if ($alreadyAddedParams -notcontains $existingParam.name) { + $templateContent += $existingParam.content + } + continue + } + + if ($wouldBeParamElem.count -gt 1) { + # With default + + if ($parameterName -eq 'lock') { + # Special handling as we pass the parameter down to the child + $templateContent += " $($parameterName): contains($($childResourceTypeSingular), 'lock') ? $($childResourceTypeSingular).lock : lock" + $alreadyAddedParams += $parameterName + continue + } + + $wouldBeParamValue = $wouldBeParamElem[1] + + # Special case, location function - should reference a location parameter instead + if ($wouldBeParamValue -like '*().location') { + $wouldBeParamValue = 'location' + } + + $templateContent += " $($parameterName): contains($($childResourceTypeSingular), '$($parameterName)') ? $($childResourceTypeSingular).$($parameterName) : $($wouldBeParamValue)" + $alreadyAddedParams += $parameterName + } else { + # No default + $templateContent += " $($parameterName): $($childResourceTypeSingular).$($parameterName)" + $alreadyAddedParams += $parameterName + } + } + + $templateContent += @( + # Special handling as we pass the variable down to the child + ' enableDefaultTelemetry: enableReferencedModulesTelemetry' + ' }' + '}]' + '' + ) + } + } + + return $templateContent + } + + end { + Write-Debug ('{0} exited' -f $MyInvocation.MyCommand) + } +} diff --git a/utilities/tools/REST2CARML/private/module/Get-TemplateDeploymentsContent.ps1 b/utilities/tools/REST2CARML/private/module/Get-TemplateDeploymentsContent.ps1 new file mode 100644 index 0000000000..12a843ee56 --- /dev/null +++ b/utilities/tools/REST2CARML/private/module/Get-TemplateDeploymentsContent.ps1 @@ -0,0 +1,326 @@ +<# +.SYNOPSIS +Get the formatted content for the template's 'deployments' section + +.DESCRIPTION +Get the formatted content for the template's 'deployments' section. For the primary resource, template content of any pre-existing template takes precedence over new content. + +.PARAMETER FullResourceType +Mandatory. The complete ResourceType identifier to update the template for (e.g., 'Microsoft.Storage/storageAccounts'). + +.PARAMETER ResourceType +Mandatory. The resource type without the provider namespace (e.g., 'storageAccounts') + +.PARAMETER ResourceTypeSingular +Optional. The 'singular' version of the resource type. For example 'container' instead of 'containers'. + +.PARAMETER ModuleData +Mandatory. The module data to fetch the data for this section from & then format it propertly for the template. + +Expects an array with objects like: +Name Value +---- ----- +parameters {name, identity, type, properties…} +outputs {} +additionalFiles {} +modules {} +variables {diagnosticsMetrics, diagnosticsLogs} +resources {privateCloud_diagnosticSettings, privateCloud_lock} +isSingleton False +additionalParameters {diagnosticLogsRetentionInDays, diagnosticStorageAccountId, diagnosticWorkspaceId, diagnosticEventHubAuthorizationRuleId…} + +.PARAMETER FullModuleData +Mandatory. The full stack of module data of all modules included in the original invocation. May be used for parent-child references. + +Expects an array with objects like: + +Name Value +---- ----- +identifier Microsoft.AVS/privateClouds/workloadNetworks/dhcpConfigurations +data {parameters, outputs, additionalFiles, modules…} +metadata {urlPath, jsonFilePath, parentUrlPath} +identifier Microsoft.AVS/privateClouds/cloudLinks +data {parameters, outputs, additionalFiles, modules…} +metadata {urlPath, jsonFilePath, parentUrlPath} +identifier Microsoft.AVS/privateClouds/workloadNetworks/portMirroringProfiles +data {parameters, outputs, additionalFiles, modules…} +metadata {urlPath, jsonFilePath, parentUrlPath} + +.PARAMETER ParentResourceTypes +Optional. The name of any parent resource type. (e.g., @('privateClouds', 'clusters') + +.PARAMETER ExistingTemplateContent +Optional. The prepared content of an existing template, if any. + +Expects an array with objects like: + +Name Value +---- ----- +modules {privateCloud_cloudLinks, privateCloud_hcxEnterpriseSites, privateCloud_authorizations, privateCloud_scriptExecutions…} +variables {diagnosticsMetrics, diagnosticsLogs, enableReferencedModulesTelemetry} +parameters {name, sku, addons, authorizations…} +outputs {name, resourceId, resourceGroupName} +resources {defaultTelemetry, privateCloud, privateCloud_diagnosticSettings, privateCloud_lock} + +.PARAMETER LinkedChildren +Optional. Information about any child-module of the current resource type. Used to generate proper module references. + +Expects an array with objects like: + +Name Value +---- ----- +identifier Microsoft.AVS/privateClouds/cloudLinks +data {parameters, outputs, additionalFiles, modules…} +metadata {urlPath, jsonFilePath, parentUrlPath} +identifier Microsoft.AVS/privateClouds/hcxEnterpriseSites +data {parameters, outputs, additionalFiles, modules…} +metadata {urlPath, jsonFilePath, parentUrlPath} +identifier Microsoft.AVS/privateClouds/authorizations +data {parameters, outputs, additionalFiles, modules…} +metadata {urlPath, jsonFilePath, parentUrlPath} + +.EXAMPLE +$contentInputObject = @{ + FullResourceType = 'Microsoft.AVS/privateClouds/clusters/datastores' + ResourceType = 'privateClouds/clusters/datastores' + ResourceTypeSingular = 'datastore' + ModuleData = @(@{...}, (...)) + FullModuleData = @(@{...}, (...)) + ParentResourceTypes = @('privateClouds', 'clusters') + ExistingTemplateContent = @(@{...}, (...)) + LinkedChildren = @(@{...}, (...)) +} +Get-TemplateDeploymentsContent @contentInputObject + +Get the formatted template content for resource type 'Microsoft.AVS/privateClouds/clusters/datastores' based on the given data - including an existing template's data. The output looks something like: + +```bicep +// =============== // +// Deployments // +// =============== // + +resource defaultTelemetry 'Microsoft.Resources/deployments@2021-04-01' = if (enableDefaultTelemetry) { + name: 'pid-11111111-1111-1111-1111-111111111111-${uniqueString(deployment().name, location)}' + properties: { + mode: 'Incremental' + template: { + '$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#' + contentVersion: '1.0.0.0' + resources: [] + } + } +} +(...) +``` +#> +function Get-TemplateDeploymentsContent { + + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string] $FullResourceType, + + [Parameter(Mandatory = $true)] + [string] $ResourceType, + + [Parameter(Mandatory = $false)] + [string] $ResourceTypeSingular = ((Get-ResourceTypeSingularName -ResourceType $ResourceType) -split '/')[-1], + + [Parameter(Mandatory = $true)] + [array] $ModuleData, + + [Parameter(Mandatory = $true)] + [array] $FullModuleData, + + [Parameter(Mandatory = $false)] + [array] $ParentResourceTypes = @(), + + [Parameter(Mandatory = $false)] + [array] $ExistingTemplateContent = @(), + + [Parameter(Mandatory = $false)] + [array] $LinkedChildren = @() + ) + + begin { + Write-Debug ('{0} entered' -f $MyInvocation.MyCommand) + } + + process { + ##################### + ## Collect Data # + ##################### + + # Collect all parent references for 'exiting' resource references + $fullParentResourceStack = Get-ParentResourceTypeList -ResourceType $FullResourceType + + $locationParameterExists = ($templateContent | Where-Object { $_ -like 'param location *' }).Count -gt 0 + + $matchingExistingResource = $existingTemplateContent.resources | Where-Object { + $_.type -eq $FullResourceType -and $_.name -eq $resourceTypeSingular + } + + ######################## + ## Create Content ## + ######################## + + $templateContent = @( + '// =============== //' + '// Deployments //' + '// =============== //' + '' + ) + + # Add telemetry resource + # ---------------------- + $telemetryTemplate = Get-Content -Path (Join-Path $Script:src 'telemetry.bicep') + if (-not $locationParameterExists) { + # Remove the location from the deployment name if the template has no such parameter + $telemetryTemplate = $telemetryTemplate -replace ', location', '' + } + $templateContent += $telemetryTemplate + $templateContent += '' + + # Add 'existing' parents (if any) + # ------------------------------- + $existingResourceIndent = 0 + $orderedParentResourceTypes = $fullParentResourceStack | Where-Object { $_ -notlike $FullResourceType } | Sort-Object + foreach ($parentResourceType in $orderedParentResourceTypes) { + $singularParent = ((Get-ResourceTypeSingularName -ResourceType $parentResourceType) -split '/')[-1] + $levedParentResourceType = ($parentResourceType -ne (@() + $orderedParentResourceTypes)[0]) ? (Split-Path $parentResourceType -Leaf) : $parentResourceType + $parentJSONPath = ($FullModuleData | Where-Object { $_.identifier -eq $parentResourceType }).Metadata.JSONFilePath + + if ([String]::IsNullOrEmpty($parentJSONPath)) { + # Case: A child who's parent resource does not exist (i.e., is a proxy). In this case we use the current API paths as a fallback + # Example: 'Microsoft.AVS/privateClouds/workloadNetworks' is not actually existing as a parent for 'Microsoft.AVS/privateClouds/workloadNetworks/dhcpConfigurations' + $parentJSONPath = $JSONFilePath + } + + $parentResourceAPI = Split-Path (Split-Path $parentJSONPath -Parent) -Leaf + $templateContent += @( + "$(' ' * $existingResourceIndent)resource $($singularParent) '$($levedParentResourceType)@$($parentResourceAPI)' existing = {", + "$(' ' * $existingResourceIndent) name: $($singularParent)Name" + ) + if ($parentResourceType -ne (@() + $orderedParentResourceTypes)[-1]) { + # Only add an empty line if there is more content to add + $templateContent += '' + } + $existingResourceIndent += 4 + } + # Add closing brakets + foreach ($parentResourceType in ($fullParentResourceStack | Where-Object { $_ -notlike $FullResourceType } | Sort-Object)) { + $existingResourceIndent -= 4 + $templateContent += "$(' ' * $existingResourceIndent)}" + } + $templateContent += '' + + # Add primary resource + # -------------------- + # Deployment resource declaration line + $serviceAPIVersion = Split-Path (Split-Path $JSONFilePath -Parent) -Leaf + $templateContent += "resource $resourceTypeSingular '$FullResourceType@$serviceAPIVersion' = {" + + if (($FullResourceType -split '/').Count -ne 2) { + # In case of children, we set the 'parent' to the next parent + $templateContent += (' parent: {0}' -f (($parentResourceTypes | ForEach-Object { Get-ResourceTypeSingularName -ResourceType $_ }) -join '::')) + } + + foreach ($parameter in ($ModuleData.parameters | Where-Object { $_.level -eq 0 -and $_.name -ne 'properties' } | Sort-Object -Property 'name')) { + if ($matchingExistingResource.topLevelElements.name -notcontains $parameter.name) { + $templateContent += ' {0}: {0}' -f $parameter.name + } else { + $existingProperty = $matchingExistingResource.topLevelElements | Where-Object { $_.name -eq $parameter.name } + $templateContent += $existingProperty.content + } + } + + $templateContent += ' properties: {' + foreach ($parameter in ($ModuleData.parameters | Where-Object { $_.level -eq 1 -and $_.Parent -eq 'properties' } | Sort-Object -Property 'name')) { + if ($matchingExistingResource.nestedElements.name -notcontains $parameter.name) { + $templateContent += ' {0}: {0}' -f $parameter.name + } else { + $existingProperty = $matchingExistingResource.nestedElements | Where-Object { $_.name -eq $parameter.name } + $templateContent += $existingProperty.content + } + } + + $templateContent += @( + ' }' + '}' + '' + ) + + # If a template already exists, add 'extra' resources that are not yet part of the template content + # ------------------------------------------------------------------------------------------------- + # Excluded are + # - Anything we generate anew as a resource + # - Telemetry (as it's regenerated above anyways) + # - Existing parent resources (as they are regenerated above anyways) + if ($existingTemplateContent.resources.count -gt 0) { + $preExistingExtraResources = $existingTemplateContent.resources | Where-Object { + $_.name -notIn $ModuleData.resources.name + @('defaultTelemetry') + @($resourceTypeSingular) -and $_.content[0] -notlike '* existing = {' + } + foreach ($resource in $preExistingExtraResources) { + $templateContent += $resource.content + $templateContent += '' + } + } + + # Add additional resources such as extensions (like DiagnosticSettigs) + # -------------------------------------------------------------------- + # Other collected resources + foreach ($additionalResource in ($ModuleData.resources | Sort-Object 'name')) { + if ($existingTemplateContent.resources.name -notcontains $additionalResource.name) { + $templateContent += $additionalResource.content + } else { + $existingResource = $existingTemplateContent.resources | Where-Object { $_.name -eq $additionalResource.name } + $templateContent += $existingResource.content + $templateContent += '' + } + } + + # Add child-module references + # --------------------------- + $childrenInputObject = @{ + FullResourceType = $FullResourceType + ResourceType = $ResourceType + ResourceTypeSingular = $ResourceTypeSingular + ModuleData = $ModuleData + LocationParameterExists = $LocationParameterExists + } + if ($LinkedChildren.Count -gt 0) { + $childrenInputObject['LinkedChildren'] = $LinkedChildren + } + if ($ExistingTemplateContent.Count -gt 0) { + $childrenInputObject['ExistingTemplateContent'] = $ExistingTemplateContent + } + if ($ParentResourceTypes.Count -gt 0) { + $childrenInputObject['ParentResourceTypes'] = $ParentResourceTypes + } + $templateContent += Get-TemplateChildModuleContent @childrenInputObject + + # TODO : Add other module references + # ---------------------------------- + foreach ($additionalResource in $ModuleData.modules) { + if ($existingTemplateContent.modules.name -notcontains $additionalResource.name) { + $templateContent += $additionalResource.content + } else { + $existingResource = $existingTemplateContent.modules | Where-Object { $_.name -eq $additionalResource.name } + $templateContent += $existingResource.content + $templateContent += '' + } + } + + # TODO: Extra extra modules + # $preExistingExtraModules = $existingTemplateContent.modules | Where-Object { $_.name -notIn $ModuleData.modules.name } + # foreach ($preExistingMdoule in $preExistingExtraModules) { + # # Beware: The pre-existing content also contains e.g. 'linkedChildren' we add as part of the template generation + # } + + return $templateContent + } + + end { + Write-Debug ('{0} exited' -f $MyInvocation.MyCommand) + } +} diff --git a/utilities/tools/REST2CARML/private/module/Get-TemplateOutputContent.ps1 b/utilities/tools/REST2CARML/private/module/Get-TemplateOutputContent.ps1 new file mode 100644 index 0000000000..0957fe2774 --- /dev/null +++ b/utilities/tools/REST2CARML/private/module/Get-TemplateOutputContent.ps1 @@ -0,0 +1,173 @@ +<# +.SYNOPSIS +Get the formatted content for the template's 'outputs' section + +.DESCRIPTION +Get the formatted content for the template's 'outputs' section. For the primary resource, template content of any pre-existing template takes precedence over new content. + +.PARAMETER ResourceType +Mandatory. The resource type without the provider namespace (e.g., 'storageAccounts') + +.PARAMETER ResourceTypeSingular +Optional. The 'singular' version of the resource type. For example 'container' instead of 'containers'. + +.PARAMETER TargetScope +Mandatory. The scope of the target template (e.g., 'resourceGroup', 'subscription', etc.) + +.PARAMETER ModuleData +Mandatory. The module data to fetch the data for this section from & then format it propertly for the template. + +Expects an array with objects like: +Name Value +---- ----- +parameters {name, identity, type, properties…} +outputs {} +additionalFiles {} +modules {} +variables {diagnosticsMetrics, diagnosticsLogs} +resources {privateCloud_diagnosticSettings, privateCloud_lock} +isSingleton False +additionalParameters {diagnosticLogsRetentionInDays, diagnosticStorageAccountId, diagnosticWorkspaceId, diagnosticEventHubAuthorizationRuleId…} + +.PARAMETER ExistingTemplateContent +Optional. The prepared content of an existing template, if any. + +Expects an array with objects like: + +Name Value +---- ----- +modules {privateCloud_cloudLinks, privateCloud_hcxEnterpriseSites, privateCloud_authorizations, privateCloud_scriptExecutions…} +variables {diagnosticsMetrics, diagnosticsLogs, enableReferencedModulesTelemetry} +parameters {name, sku, addons, authorizations…} +outputs {name, resourceId, resourceGroupName} +resources {defaultTelemetry, privateCloud, privateCloud_diagnosticSettings, privateCloud_lock} + +.EXAMPLE +$contentInputObject = @{ + ResourceType = 'privateClouds/clusters/datastores' + ResourceTypeSingular = 'datastore' + TargetScope = 'resourceGroup' + ModuleData = @(@{...}, (...)) + ExistingTemplateContent = @(@{...}, (...)) +} +Get-TemplateOutputContent @contentInputObject + +Get the formatted template content for resource type 'Microsoft.AVS/privateClouds/clusters/datastores' based on the given data - including an existing template's data. The output looks something like: + +```bicep +// =========== // +// Outputs // +// =========== // + +@description('The name of the datastore.') +output name string = datastore.name + +@description('The resource ID of the datastore.') +output resourceId string = datastore.id + +@description('The name of the resource group the datastore was created in.') +output resourceGroupName string = resourceGroup().name +(...) +``` +#> +function Get-TemplateOutputContent { + + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string] $ResourceType, + + [Parameter(Mandatory = $false)] + [string] $ResourceTypeSingular = ((Get-ResourceTypeSingularName -ResourceType $ResourceType) -split '/')[-1], + + [Parameter(Mandatory = $true)] + [string] $TargetScope, + + [Parameter(Mandatory = $true)] + [array] $ModuleData, + + [Parameter(Mandatory = $false)] + [array] $ExistingTemplateContent = @() + ) + + begin { + Write-Debug ('{0} entered' -f $MyInvocation.MyCommand) + } + + process { + ##################### + ## Collect Data # + ##################### + $defaultOutputs = @( + @{ + name = 'name' + type = 'string' + content = @( + "@description('The name of the $resourceTypeSingular.')" + "output name string = $resourceTypeSingular.name" + ) + }, + @{ + name = 'resourceId' + type = 'string' + content = @( + "@description('The resource ID of the $resourceTypeSingular.')" + "output resourceId string = $resourceTypeSingular.id" + ) + } + ) + + if ($targetScope -eq 'resourceGroup') { + $defaultOutputs += @{ + name = 'resourceGroupName' + type = 'string' + content = @( + "@description('The name of the resource group the $resourceTypeSingular was created in.')" + 'output resourceGroupName string = resourceGroup().name' + ) + } + } + + # If the main resource has a location property, an output should be returned too + if ($ModuleData.parametersToAdd.name -contains 'location' -and $ModuleData.parametersToAdd['location'].defaultValue -ne 'global') { + $defaultOutputs += @{ + name = 'location' + type = 'string' + content = @( + "@description('The location the resource was deployed into.')" + '{0}.location' -f $resourceTypeSingular + ) + } + } + + # Extra outputs + $outputsToAdd = -not $ExistingTemplateContent ? @() : $ExistingTemplateContent.outputs + foreach ($default in $defaultOutputs) { + if ($outputsToAdd.name -notcontains $default.name) { + $outputsToAdd += $default + } + } + + ######################## + ## Create Content ## + ######################## + + $templateContent = @( + '// =========== //' + '// Outputs //' + '// =========== //' + '' + ) + + foreach ($output in $outputsToAdd) { + $templateContent += $output.content + $templateContent += '' + } + + return $templateContent + } + + end { + Write-Debug ('{0} exited' -f $MyInvocation.MyCommand) + } +} diff --git a/utilities/tools/REST2CARML/private/module/Get-TemplateParametersContent.ps1 b/utilities/tools/REST2CARML/private/module/Get-TemplateParametersContent.ps1 new file mode 100644 index 0000000000..df795e1038 --- /dev/null +++ b/utilities/tools/REST2CARML/private/module/Get-TemplateParametersContent.ps1 @@ -0,0 +1,250 @@ +<# +.SYNOPSIS +Get the formatted content for the template's 'parameters' section + +.DESCRIPTION +Get the formatted content for the template's 'parameters' section. Template content of any pre-existing template takes precedence over new content. + +.PARAMETER FullResourceType +Mandatory. The complete ResourceType identifier to update the template for (e.g., 'Microsoft.Storage/storageAccounts'). + +.PARAMETER ModuleData +Mandatory. The module data to fetch the data for this section from & then format it propertly for the template. + +Expects an array with objects like: +Name Value +---- ----- +parameters {name, identity, type, properties…} +outputs {} +additionalFiles {} +modules {} +variables {diagnosticsMetrics, diagnosticsLogs} +resources {privateCloud_diagnosticSettings, privateCloud_lock} +isSingleton False +additionalParameters {diagnosticLogsRetentionInDays, diagnosticStorageAccountId, diagnosticWorkspaceId, diagnosticEventHubAuthorizationRuleId…} + +.PARAMETER FullModuleData +Mandatory. The full stack of module data of all modules included in the original invocation. May be used for parent-child references. + +Expects an array with objects like: + +Name Value +---- ----- +identifier Microsoft.AVS/privateClouds/workloadNetworks/dhcpConfigurations +data {parameters, outputs, additionalFiles, modules…} +metadata {urlPath, jsonFilePath, parentUrlPath} +identifier Microsoft.AVS/privateClouds/cloudLinks +data {parameters, outputs, additionalFiles, modules…} +metadata {urlPath, jsonFilePath, parentUrlPath} +identifier Microsoft.AVS/privateClouds/workloadNetworks/portMirroringProfiles +data {parameters, outputs, additionalFiles, modules…} +metadata {urlPath, jsonFilePath, parentUrlPath} + +.PARAMETER ParentResourceTypes +Optional. The name of any parent resource type. (e.g., @('privateClouds', 'clusters') + +.PARAMETER ExistingTemplateContent +Optional. The prepared content of an existing template, if any. + +Expects an array with objects like: + +Name Value +---- ----- +modules {privateCloud_cloudLinks, privateCloud_hcxEnterpriseSites, privateCloud_authorizations, privateCloud_scriptExecutions…} +variables {diagnosticsMetrics, diagnosticsLogs, enableReferencedModulesTelemetry} +parameters {name, sku, addons, authorizations…} +outputs {name, resourceId, resourceGroupName} +resources {defaultTelemetry, privateCloud, privateCloud_diagnosticSettings, privateCloud_lock} + +.PARAMETER LinkedChildren +Optional. Information about any child-module of the current resource type. Used to generate proper module references. + +Expects an array with objects like: + +Name Value +---- ----- +identifier Microsoft.AVS/privateClouds/cloudLinks +data {parameters, outputs, additionalFiles, modules…} +metadata {urlPath, jsonFilePath, parentUrlPath} +identifier Microsoft.AVS/privateClouds/hcxEnterpriseSites +data {parameters, outputs, additionalFiles, modules…} +metadata {urlPath, jsonFilePath, parentUrlPath} +identifier Microsoft.AVS/privateClouds/authorizations +data {parameters, outputs, additionalFiles, modules…} +metadata {urlPath, jsonFilePath, parentUrlPath} + +.EXAMPLE +$contentInputObject = @{ + FullResourceType = 'Microsoft.AVS/privateClouds/clusters/datastores' + ModuleData = @(@{...}, (...)) + FullModuleData = @(@{...}, (...)) + ParentResourceTypes = @('privateClouds', 'clusters') + ExistingTemplateContent = @(@{...}, (...)) + LinkedChildren = @(@{...}, (...)) +} +Get-TemplateParametersContent @contentInputObject + +Get the formatted template content for resource type 'Microsoft.AVS/privateClouds/clusters/datastores' based on the given data - including an existing template's data. The output looks something like: + +```bicep +// ============== // +// Parameters // +// ============== // + +@description('Required. Name of the private cloud') +param name string + +@description('Required. The resource model definition representing SKU') +param sku object + +@description('Optional. The addons to create as part of the privateCloud.') +param addons array = [] +(...) +``` +#> +function Get-TemplateParametersContent { + + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string] $FullResourceType, + + [Parameter(Mandatory = $true)] + [array] $ModuleData, + + [Parameter(Mandatory = $true)] + [array] $FullModuleData, + + [Parameter(Mandatory = $false)] + [array] $ParentResourceTypes = @(), + + [Parameter(Mandatory = $false)] + [array] $ExistingTemplateContent = @(), + + [Parameter(Mandatory = $false)] + [array] $LinkedChildren = @() + ) + + begin { + Write-Debug ('{0} entered' -f $MyInvocation.MyCommand) + } + + process { + ##################### + ## Collect Data # + ##################### + + # Handle parent proxy, if any + $hasAProxyParent = $FullModuleData.identifier -notContains ((Split-Path $FullResourceType -Parent) -replace '\\', '/') + $parentProxyName = $hasAProxyParent ? ($UrlPath -split '\/')[-3] : '' + $proxyParentType = Split-Path (Split-Path $FullResourceType -Parent) -Leaf + + # Collect parameters to create + # ---------------------------- + $parametersToAdd = @() + + # Add parent parameters + foreach ($parentResourceType in ($parentResourceTypes | Sort-Object)) { + $thisParentIsProxy = $hasAProxyParent -and $parentResourceType -eq $proxyParentType + + $parentParamData = @{ + level = 0 + name = '{0}Name' -f (Get-ResourceTypeSingularName -ResourceType $parentResourceType) + type = 'string' + description = '{0}. The name of the parent {1}. Required if the template is used in a standalone deployment.' -f ($thisParentIsProxy ? 'Optional' : 'Conditional'), $parentResourceType + required = $false + } + + if ($thisParentIsProxy) { + # Handle proxy parents (i.e., empty containers with only a default value name) + $parentParamData['default'] = $parentProxyName + } + + $parametersToAdd += $parentParamData + } + + # Add primary (service) parameters (i.e. top-level and those in the properties) + $parametersToAdd += @() + ($ModuleData.parameters | Where-Object { $_.Level -in @(0, 1) -and $_.name -ne 'properties' -and ([String]::IsNullOrEmpty($_.Parent) -or $_.Parent -eq 'properties') }) + + # Add additional (extension) parameters + $parametersToAdd += $ModuleData.additionalParameters + + # Add child module references + foreach ($dataBlock in ($linkedChildren | Sort-Object -Property 'identifier')) { + $childResourceType = ($dataBlock.identifier -split '/')[-1] + $parametersToAdd += @{ + level = 0 + name = $childResourceType + type = 'array' + default = @() + description = "The $childResourceType to create as part of the $resourceTypeSingular." + required = $false + } + } + + # Add telemetry parameter + $parametersToAdd += @{ + level = 0 + name = 'enableDefaultTelemetry' + type = 'boolean' + default = $true + description = 'Enable telemetry via the Customer Usage Attribution ID (GUID).' + required = $false + } + + + ######################## + ## Create Content ## + ######################## + + $templateContent = @( + '// ============== //' + '// Parameters //' + '// ============== //' + '' + ) + + # Note: If there already is a template and a given parameter was already specified, we use the existing declaration instead of generating a new one + # as it may have custom logic / default values, etc. + + # First the required + foreach ($parameter in ($parametersToAdd | Where-Object { $_.required } | Sort-Object -Property 'Name')) { + if ($existingTemplateContent.parameters.name -notcontains $parameter.name) { + $templateContent += Get-FormattedModuleParameter -ParameterData $parameter + } else { + $templateContent += ($existingTemplateContent.parameters | Where-Object { $_.name -eq $parameter.name }).content + $templateContent += '' + } + } + # Then the conditional + foreach ($parameter in ($parametersToAdd | Where-Object { -not $_.required -and $_.description -like 'Conditional. *' } | Sort-Object -Property 'Name')) { + if ($existingTemplateContent.parameters.name -notcontains $parameter.name) { + $templateContent += Get-FormattedModuleParameter -ParameterData $parameter + } else { + $templateContent += ($existingTemplateContent.parameters | Where-Object { $_.name -eq $parameter.name }).content + $templateContent += '' + } + } + # Then the rest + foreach ($parameter in ($parametersToAdd | Where-Object { -not $_.required -and $_.description -notlike 'Conditional. *' } | Sort-Object -Property 'Name')) { + if ($existingTemplateContent.parameters.name -notcontains $parameter.name) { + $templateContent += Get-FormattedModuleParameter -ParameterData $parameter + } else { + $templateContent += ($existingTemplateContent.parameters | Where-Object { $_.name -eq $parameter.name }).content + $templateContent += '' + } + } + + # Add additional parameters to only exist in a pre-existing template at the end + foreach ($extraParameter in ($existingTemplateContent.parameters | Where-Object { $parametersToAdd.name -notcontains $_.name })) { + $templateContent += $extraParameter.content + $templateContent += '' + } + + return $templateContent + } + + end { + Write-Debug ('{0} exited' -f $MyInvocation.MyCommand) + } +} diff --git a/utilities/tools/REST2CARML/private/module/Get-TemplateVariablesContent.ps1 b/utilities/tools/REST2CARML/private/module/Get-TemplateVariablesContent.ps1 new file mode 100644 index 0000000000..6df2aec519 --- /dev/null +++ b/utilities/tools/REST2CARML/private/module/Get-TemplateVariablesContent.ps1 @@ -0,0 +1,118 @@ +<# +.SYNOPSIS +Get the formatted content for the template's 'variables' section + +.DESCRIPTION +Get the formatted content for the template's 'variables' section. Template content of any pre-existing template takes precedence over new content. + +.PARAMETER ModuleData +Mandatory. The module data to fetch the data for this section from & then format it propertly for the template. + +Expects an array with objects like: +Name Value +---- ----- +parameters {name, identity, type, properties…} +outputs {} +additionalFiles {} +modules {} +variables {diagnosticsMetrics, diagnosticsLogs} +resources {privateCloud_diagnosticSettings, privateCloud_lock} +isSingleton False +additionalParameters {diagnosticLogsRetentionInDays, diagnosticStorageAccountId, diagnosticWorkspaceId, diagnosticEventHubAuthorizationRuleId…} + +.PARAMETER ExistingTemplateContent +Optional. The prepared content of an existing template, if any. + +Expects an array with objects like: + +Name Value +---- ----- +modules {privateCloud_cloudLinks, privateCloud_hcxEnterpriseSites, privateCloud_authorizations, privateCloud_scriptExecutions…} +variables {diagnosticsMetrics, diagnosticsLogs, enableReferencedModulesTelemetry} +parameters {name, sku, addons, authorizations…} +outputs {name, resourceId, resourceGroupName} +resources {defaultTelemetry, privateCloud, privateCloud_diagnosticSettings, privateCloud_lock} + +.EXAMPLE +Get-TemplateVariablesContent -ModuleData @(@{ variables = @(@{ name = 'abc'; content = @( var abc = (...)) }; (...))}, (...)) -ExistingTemplateContent @(@{ variables = @(@{ name = 'abc'; content = @( var abc = (...))}, (...))}, (...)) + +Generate the variables content for the above example containing at least the 'abc' variable. Would result in an output like + +```bicep +// ============= // +// Variables // +// ============= // + +var abc = (...) +(...) +``` +#> +function Get-TemplateVariablesContent { + + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [array] $ModuleData, + + [Parameter(Mandatory = $false)] + [array] $ExistingTemplateContent = @() + ) + + begin { + Write-Debug ('{0} entered' -f $MyInvocation.MyCommand) + } + + process { + + ######################## + ## Create Content ## + ######################## + + $templateContent = @( + '// ============= //' + '// Variables //' + '// ============= //' + '' + ) + + foreach ($variable in $ModuleData.variables) { + if ($existingTemplateContent.variables.name -notcontains $variable.name) { + $templateContent += $variable.content + } else { + $matchingExistingVar = $existingTemplateContent.variables | Where-Object { $_.name -eq $variable.name } + $templateContent += $matchingExistingVar.content + } + $templateContent += '' + } + + # Add telemetry variable + if ($linkedChildren.Count -gt 0) { + if ($existingTemplateContent.variables.name -notcontains 'enableReferencedModulesTelemetry') { + $templateContent += @( + 'var enableReferencedModulesTelemetry = false' + ) + } else { + $matchingExistingVar = $existingTemplateContent.variables | Where-Object { $_.name -eq 'enableReferencedModulesTelemetry' } + $templateContent += $matchingExistingVar.content + } + $templateContent += '' + } + + # Add additional parameters to only exist in a pre-existing template at the end + foreach ($extraVariable in ($existingTemplateContent.variables | Where-Object { $ModuleData.variables.name -notcontains $_.name -and $_.name -ne 'enableReferencedModulesTelemetry' })) { + $templateContent += $extraVariable.content + $templateContent += '' + } + + # Only add the section if any content was added + if ($templateContent.count -eq 4) { + return @() + } else { + return $templateContent + } + } + + end { + Write-Debug ('{0} exited' -f $MyInvocation.MyCommand) + } +} diff --git a/utilities/tools/REST2CARML/private/module/Set-Module.ps1 b/utilities/tools/REST2CARML/private/module/Set-Module.ps1 index b58bfe2e06..b3d4644210 100644 --- a/utilities/tools/REST2CARML/private/module/Set-Module.ps1 +++ b/utilities/tools/REST2CARML/private/module/Set-Module.ps1 @@ -112,7 +112,7 @@ function Set-Module { Set-ModuleReadMe -TemplateFilePath $templatePath -Verbose:$false } } catch { - Write-Warning "Invocation of 'Set-ModuleReadMe' fuction for template in path [$templatePath] failed. Please review the template and re-run the command `Set-ModuleReadMe -TemplateFilePath '$templatePath'``" + Write-Warning "Invocation of 'Set-ModuleReadMe' function for template in path [$templatePath] failed. Please review the template and re-run the command `Set-ModuleReadMe -TemplateFilePath '$templatePath'``" } } diff --git a/utilities/tools/REST2CARML/private/module/Set-ModuleTemplate.ps1 b/utilities/tools/REST2CARML/private/module/Set-ModuleTemplate.ps1 index e10da1ccf2..517e1f8d0e 100644 --- a/utilities/tools/REST2CARML/private/module/Set-ModuleTemplate.ps1 +++ b/utilities/tools/REST2CARML/private/module/Set-ModuleTemplate.ps1 @@ -47,40 +47,23 @@ function Set-ModuleTemplate { begin { Write-Debug ('{0} entered' -f $MyInvocation.MyCommand) - - $templateFilePath = Join-Path $script:repoRoot 'modules' $FullResourceType 'deploy.bicep' - $providerNamespace = ($FullResourceType -split '/')[0] - $resourceType = $FullResourceType -replace "$providerNamespace/", '' } process { - ##################### ## Collect Data # ##################### + #region data + + $templateFilePath = Join-Path $script:repoRoot 'modules' $FullResourceType 'deploy.bicep' + $providerNamespace = ($FullResourceType -split '/')[0] + $resourceType = $FullResourceType -replace "$providerNamespace/", '' # Existing template (if any) $existingTemplateContent = Resolve-ExistingTemplateContent -TemplateFilePath $templateFilePath # Collect child-resource information - $linkedChildren = $fullmoduleData | Where-Object { - # Is nested - $_.identifier -like "$FullResourceType/*" -and - # Is direct child - (($_.identifier -split '/').Count -eq (($FullResourceType -split '/').Count + 1) - ) - } - ## Add indirect child (via proxy resource) (i.e. it's a nested-nested resources who's parent has no individual specification/JSONFilePath). TODO: Is that always true? What if the data is specified in one file?x` - $indirectChildren = $FullModuleData | Where-Object { - # Is nested - $_.identifier -like "$FullResourceType/*" -and - # Is indirect child - (($_.identifier -split '/').Count -eq (($FullResourceType -split '/').Count + 2)) - } | Where-Object { - # If the child's parent's parentUrlPath is empty, this parent has no PUT rest command which indicates it cannot be created independently - [String]::IsNullOrEmpty($_.metadata.parentUrlPath) - } - $linkedChildren += $indirectChildren + $linkedChildren = Get-LinkedChildModuleList -FullModuleData $FullModuleData -FullResourceType $FullResourceType # Collect parent resources to use for parent type references $typeElem = $FullResourceType -split '/' @@ -90,21 +73,13 @@ function Set-ModuleTemplate { $parentResourceTypes = @() } - # Collect all parent references for 'exiting' resource references - $fullParentResourceStack = Get-ParentResourceTypeList -ResourceType $FullResourceType - # Get the singular version of the current resource type for proper naming $resourceTypeSingular = ((Get-ResourceTypeSingularName -ResourceType $resourceType) -split '/')[-1] + #endregion - # Handle parent proxy, if any - $hasAProxyParent = $FullModuleData.identifier -notContains ((Split-Path $FullResourceType -Parent) -replace '\\', '/') - $parentProxyName = $hasAProxyParent ? ($UrlPath -split '\/')[-3] : '' - $proxyParentType = Split-Path (Split-Path $FullResourceType -Parent) -Leaf - - ################## - ## PARAMETERS ## - ################## - + ############# + ## SCOPE ## + ############# $targetScope = Get-TargetScope -UrlPath $UrlPath $templateContent = ($targetScope -ne 'resourceGroup') ? @( @@ -112,315 +87,101 @@ function Set-ModuleTemplate { '' ) : @() - $templateContent += @( - '// ============== //' - '// Parameters //' - '// ============== //' - '' - ) - - # Collect parameters to create - # ---------------------------- - $parametersToAdd = @() - - # Add parent parameters - foreach ($parentResourceType in ($parentResourceTypes | Sort-Object)) { - $thisParentIsProxy = $hasAProxyParent -and $parentResourceType -eq $proxyParentType - - $parentParamData = @{ - level = 0 - name = '{0}Name' -f (Get-ResourceTypeSingularName -ResourceType $parentResourceType) - type = 'string' - description = '{0}. The name of the parent {1}. Required if the template is used in a standalone deployment.' -f ($thisParentIsProxy ? 'Optional' : 'Conditional'), $parentResourceType - required = $false - } - - if ($thisParentIsProxy) { - # Handle proxy parents (i.e., empty containers with only a default value name) - $parentParamData['default'] = $parentProxyName - } - - $parametersToAdd += $parentParamData - } - - # Add primary (service) parameters (i.e. top-level and those in the properties) - $parametersToAdd += @() + ($ModuleData.parameters | Where-Object { $_.Level -in @(0, 1) -and $_.name -ne 'properties' -and ([String]::IsNullOrEmpty($_.Parent) -or $_.Parent -eq 'properties') }) - - - # Add additional (extension) parameters - $parametersToAdd += $ModuleData.additionalParameters - - # Add child module references - foreach ($dataBlock in ($linkedChildren | Sort-Object -Property 'identifier')) { - $childResourceType = ($dataBlock.identifier -split '/')[-1] - $parametersToAdd += @{ - level = 0 - name = $childResourceType - type = 'array' - default = @() - description = "The $childResourceType to create as part of the $resourceTypeSingular." - required = $false - } - } - - # Add telemetry parameter - $parametersToAdd += @{ - level = 0 - name = 'enableDefaultTelemetry' - type = 'boolean' - default = $true - description = 'Enable telemetry via the Customer Usage Attribution ID (GUID).' - required = $false - } + ################## + ## PARAMETERS ## + ################## + #region parameters - # Create collected parameters - # --------------------------- - # First the required - foreach ($parameter in ($parametersToAdd | Where-Object { $_.required } | Sort-Object -Property 'Name')) { - if ($existingTemplateContent.parameters.name -notcontains $parameter.name) { - $templateContent += Get-FormattedModuleParameter -ParameterData $parameter - } else { - $templateContent += ($existingTemplateContent.parameters | Where-Object { $_.name -eq $parameter.name }).content - $templateContent += '' - } + $parametersInputObject = @{ + ModuleData = $ModuleData + FullModuleData = $FullModuleData + FullResourceType = $FullResourceType } - # Then the conditional - foreach ($parameter in ($parametersToAdd | Where-Object { -not $_.required -and $_.description -like 'Conditional. *' } | Sort-Object -Property 'Name')) { - if ($existingTemplateContent.parameters.name -notcontains $parameter.name) { - $templateContent += Get-FormattedModuleParameter -ParameterData $parameter - } else { - $templateContent += ($existingTemplateContent.parameters | Where-Object { $_.name -eq $parameter.name }).content - $templateContent += '' - } + if ($ExistingTemplateContent.Count -gt 0) { + $parametersInputObject['ExistingTemplateContent'] = $ExistingTemplateContent } - # Then the rest - foreach ($parameter in ($parametersToAdd | Where-Object { -not $_.required -and $_.description -notlike 'Conditional. *' } | Sort-Object -Property 'Name')) { - if ($existingTemplateContent.parameters.name -notcontains $parameter.name) { - $templateContent += Get-FormattedModuleParameter -ParameterData $parameter - } else { - $templateContent += ($existingTemplateContent.parameters | Where-Object { $_.name -eq $parameter.name }).content - $templateContent += '' - } + if ($ParentResourceTypes.Count -gt 0) { + $parametersInputObject['ParentResourceTypes'] = $ParentResourceTypes } - - # Add additional parameters at the end - foreach ($extraParameter in ($existingTemplateContent.parameters | Where-Object { $parametersToAdd.name -notcontains $_.name })) { - $templateContent += $extraParameter.content - $templateContent += '' + if ($LinkedChildren.Count -gt 0) { + $parametersInputObject['LinkedChildren'] = $LinkedChildren } + $templateContent += Get-TemplateParametersContent @parametersInputObject + #endregion ################# ## VARIABLES ## ################# - + #region variables # Add a space in between the new section and the previous one in case no space exists if (-not [String]::IsNullOrEmpty($templateContent[-1])) { $templateContent += '' } - foreach ($variable in $ModuleData.variables) { - $templateContent += $variable + $variablesInputObject = @{ + ModuleData = $ModuleData } - # Add telemetry variable - if ($linkedChildren.Count -gt 0) { - $templateContent += @( - 'var enableReferencedModulesTelemetry = false' - '' - ) + if ($ExistingTemplateContent.Count -gt 0) { + $variablesInputObject['ExistingTemplateContent'] = $ExistingTemplateContent } + $templateContent += Get-TemplateVariablesContent @variablesInputObject + #endregion ################### ## DEPLOYMENTS ## ################### - - $locationParameterExists = ($templateContent | Where-Object { $_ -like 'param location *' }).Count -gt 0 + #region resources & modules # Add a space in between the new section and the previous one in case no space exists if (-not [String]::IsNullOrEmpty($templateContent[-1])) { $templateContent += '' } - $templateContent += @( - '// =============== //' - '// Deployments //' - '// =============== //' - '' - ) - - # Add telemetry resource - # ---------------------- - $telemetryTemplate = Get-Content -Path (Join-Path $Script:src 'telemetry.bicep') - if (-not $locationParameterExists) { - # Remove the location from the deployment name if the template has no such parameter - $telemetryTemplate = $telemetryTemplate -replace ', location', '' - } - $templateContent += $telemetryTemplate - $templateContent += '' - - # Add 'existing' parents (if any) - # ------------------------------- - $existingResourceIndent = 0 - $orderedParentResourceTypes = $fullParentResourceStack | Where-Object { $_ -notlike $FullResourceType } | Sort-Object - foreach ($parentResourceType in $orderedParentResourceTypes) { - $singularParent = ((Get-ResourceTypeSingularName -ResourceType $parentResourceType) -split '/')[-1] - $levedParentResourceType = ($parentResourceType -ne (@() + $orderedParentResourceTypes)[0]) ? (Split-Path $parentResourceType -Leaf) : $parentResourceType - $parentJSONPath = ($FullModuleData | Where-Object { $_.identifier -eq $parentResourceType }).Metadata.JSONFilePath - - if ([String]::IsNullOrEmpty($parentJSONPath)) { - # Case: A child who's parent resource does not exist (i.e., is a proxy). In this case we use the current API paths as a fallback - # Example: 'Microsoft.AVS/privateClouds/workloadNetworks' is not actually existing as a parent for 'Microsoft.AVS/privateClouds/workloadNetworks/dhcpConfigurations' - $parentJSONPath = $JSONFilePath - } - - $parentResourceAPI = Split-Path (Split-Path $parentJSONPath -Parent) -Leaf - $templateContent += @( - "$(' ' * $existingResourceIndent)resource $($singularParent) '$($levedParentResourceType)@$($parentResourceAPI)' existing = {", - "$(' ' * $existingResourceIndent) name: $($singularParent)Name" - ) - if ($parentResourceType -ne (@() + $orderedParentResourceTypes)[-1]) { - # Only add an empty line if there is more content to add - $templateContent += '' - } - $existingResourceIndent += 4 - } - # Add closing brakets - foreach ($parentResourceType in ($fullParentResourceStack | Where-Object { $_ -notlike $FullResourceType } | Sort-Object)) { - $existingResourceIndent -= 4 - $templateContent += "$(' ' * $existingResourceIndent)}" - } - $templateContent += '' - - # Add primary resource - # -------------------- - # Deployment resource declaration line - $serviceAPIVersion = Split-Path (Split-Path $JSONFilePath -Parent) -Leaf - $templateContent += "resource $resourceTypeSingular '$FullResourceType@$serviceAPIVersion' = {" - - if (($FullResourceType -split '/').Count -ne 2) { - # In case of children, we set the 'parent' to the next parent - $templateContent += (' parent: {0}' -f (($parentResourceTypes | ForEach-Object { Get-ResourceTypeSingularName -ResourceType $_ }) -join '::')) + $resourcesInputObject = @{ + FullResourceType = $FullResourceType + ResourceType = $ResourceType + ResourceTypeSingular = $ResourceTypeSingular + ModuleData = $ModuleData + FullModuleData = $FullModuleData } - - foreach ($parameter in ($ModuleData.parameters | Where-Object { $_.level -eq 0 -and $_.name -ne 'properties' } | Sort-Object -Property 'name')) { - $templateContent += ' {0}: {0}' -f $parameter.name + if ($ExistingTemplateContent.Count -gt 0) { + $resourcesInputObject['ExistingTemplateContent'] = $ExistingTemplateContent } - - $templateContent += ' properties: {' - foreach ($parameter in ($ModuleData.parameters | Where-Object { $_.level -eq 1 -and $_.Parent -eq 'properties' } | Sort-Object -Property 'name')) { - $templateContent += ' {0}: {0}' -f $parameter.name + if ($ParentResourceTypes.Count -gt 0) { + $resourcesInputObject['ParentResourceTypes'] = $ParentResourceTypes } - - $templateContent += @( - ' }' - '}' - '' - ) - - - # Add additional resources such as extensions (like RBAC) - # ------------------------------------------------------- - # Other collected resources - $templateContent += $ModuleData.resources - - # Add child-module references - # --------------------------- - foreach ($dataBlock in $linkedChildren) { - $childResourceType = ($dataBlock.identifier -split '/')[-1] - $childResourceTypeSingular = Get-ResourceTypeSingularName -ResourceType $childResourceType - - $hasProxyParent = [String]::IsNullOrEmpty($dataBlock.metadata.parentUrlPath) - if ($hasProxyParent) { - $proxyParentName = Split-Path (Split-Path $dataBlock.identifier -Parent) -Leaf - } - - $templateContent += @( - "module $($hasProxyParent ? "$($proxyParentName)_" : '')$($resourceTypeSingular)_$($childResourceType) '$($hasProxyParent ? "$proxyParentName/" : '')$($childResourceType)/deploy.bicep' = [for ($($childResourceTypeSingular), index) in $($childResourceType): {", - " name: '`${uniqueString(deployment().name$($locationParameterExists ? ', location' : ''))}-$($resourceTypeSingular)-$($childResourceTypeSingular)-`${index}'", - ' params: {' - ) - - # All param names of parents - foreach ($parentResourceType in $parentResourceTypes) { - $templateContent += ' {0}Name: {0}Name' -f ((Get-ResourceTypeSingularName -ResourceType $parentResourceType) -split '/')[-1] - } - # Itself - $templateContent += ' {0}Name: name' -f ((Get-ResourceTypeSingularName -ResourceType ($FullResourceType -split '/')[-1]) -split '/')[-1] - - # Any proxy default if any - if ($hasProxyParent) { - $proxyDefaultValue = ($dataBlock.metadata.urlPath -split '\/')[-3] - $templateContent += " {0}Name: '{1}'" -f (Get-ResourceTypeSingularName -ResourceType ($proxyParentName -split '/')[-1]), $proxyDefaultValue - } - - # Add primary child parameters - $allParam = $dataBlock.data.parameters + $dataBlock.data.additionalParameters - foreach ($parameter in (($allParam | Where-Object { $_.Level -in @(0, 1) -and $_.name -ne 'properties' -and ([String]::IsNullOrEmpty($_.Parent) -or $_.Parent -eq 'properties') }) | Sort-Object -Property 'Name')) { - $wouldBeParameter = Get-FormattedModuleParameter -ParameterData $parameter | Where-Object { $_ -like 'param *' } | ForEach-Object { $_ -replace 'param ', '' } - $wouldBeParamElem = $wouldBeParameter -split ' = ' - $parameter.name = ($wouldBeParamElem -split ' ')[0] - if ($wouldBeParamElem.count -gt 1) { - # With default - - if ($parameter.name -eq 'lock') { - # Special handling as we pass the parameter down to the child - $templateContent += " lock: contains($($childResourceTypeSingular), 'lock') ? $($childResourceTypeSingular).lock : lock" - continue - } - - $wouldBeParamValue = $wouldBeParamElem[1] - - # Special case, location function - should reference a location parameter instead - if ($wouldBeParamValue -like '*().location') { - $wouldBeParamValue = 'location' - } - - $templateContent += " $($parameter.name): contains($($childResourceTypeSingular), '$($parameter.name)') ? $($childResourceTypeSingular).$($parameter.name) : $($wouldBeParamValue)" - } else { - # No default - $templateContent += " $($parameter.name): $($childResourceTypeSingular).$($parameter.name)" - } - } - - $templateContent += @( - # Special handling as we pass the variable down to the child - ' enableDefaultTelemetry: enableReferencedModulesTelemetry' - ' }' - '}]' - '' - ) + if ($LinkedChildren.Count -gt 0) { + $resourcesInputObject['LinkedChildren'] = $LinkedChildren } + $templateContent += Get-TemplateDeploymentsContent @resourcesInputObject + #endregion ####################################### ## Create template outputs section ## ####################################### + #region outputs # Add a space in between the new section and the previous one in case no space exists if (-not [String]::IsNullOrEmpty($templateContent[-1])) { $templateContent += '' } - # Output header comment - $templateContent += @( - '// =========== //' - '// Outputs //' - '// =========== //' - '' - "@description('The name of the $resourceTypeSingular.')" - "output name string = $resourceTypeSingular.name" - '' - "@description('The resource ID of the $resourceTypeSingular.')" - "output resourceId string = $resourceTypeSingular.id" - '' - ) - - if ($targetScope -eq 'resourceGroup') { - $templateContent += @( - "@description('The name of the resource group the $resourceTypeSingular was created in.')" - 'output resourceGroupName string = resourceGroup().name' - '' - ) + $outputsInputObject = @{ + ResourceType = $ResourceType + ResourceTypeSingular = $ResourceTypeSingular + TargetScope = $TargetScope + ModuleData = $ModuleData + } + if ($ExistingTemplateContent.Count -gt 0) { + $outputsInputObject['ExistingTemplateContent'] = $ExistingTemplateContent } + $templateContent += Get-TemplateOutputContent @outputsInputObject + #endregion + + ############################ + ## Update template file ## + ############################ # Update file # ----------- diff --git a/utilities/tools/REST2CARML/private/specs/Resolve-ModuleData.ps1 b/utilities/tools/REST2CARML/private/specs/Resolve-ModuleData.ps1 index 62ebfec41d..9c335c0351 100644 --- a/utilities/tools/REST2CARML/private/specs/Resolve-ModuleData.ps1 +++ b/utilities/tools/REST2CARML/private/specs/Resolve-ModuleData.ps1 @@ -36,8 +36,9 @@ function Resolve-ModuleData { # Output object $templateData = [System.Collections.ArrayList]@() - # Collect data - # ------------ + ##################################### + ## Collect primary module data ## + ##################################### $specificationData = Get-Content -Path $JSONFilePath -Raw | ConvertFrom-Json -AsHashtable # Get PUT parameters @@ -72,6 +73,7 @@ function Resolve-ModuleData { parameters = $filteredList additionalParameters = @() resources = @() + modules = @() variables = @() outputs = @() additionalFiles = @() @@ -115,5 +117,10 @@ function Resolve-ModuleData { } Set-LockModuleData @lockInputObject + # Check if there can be mutliple instances of the current Resource Type. + # For example, this is 'true' for Resource Type 'Microsoft.Storage/storageAccounts/blobServices/containers', and 'false' for Resource Type 'Microsoft.Storage/storageAccounts/blobServices' + $listUrlPath = (Split-Path $UrlPath -Parent) -replace '\\', '/' + $moduleData['isSingleton'] = $specificationData.paths[$listUrlPath].get.Keys -notcontains 'x-ms-pageable' + return $moduleData }