diff --git a/.azuredevops/modulePipelines/ms.avs.privateclouds.yml b/.azuredevops/modulePipelines/ms.avs.privateclouds.yml new file mode 100644 index 0000000000..9e3bec03f2 --- /dev/null +++ b/.azuredevops/modulePipelines/ms.avs.privateclouds.yml @@ -0,0 +1,40 @@ +name: 'AVS - PrivateClouds' + +parameters: + - name: removeDeployment + displayName: Remove deployed module + type: boolean + default: true + - name: prerelease + displayName: Publish prerelease module + type: boolean + default: false + +pr: none + +trigger: + batch: true + branches: + include: + - main + paths: + include: + - '/.azuredevops/modulePipelines/ms.avs.privateclouds.yml' + - '/.azuredevops/pipelineTemplates/*.yml' + - '/modules/Microsoft.AVS/privateClouds/*' + - '/utilities/pipelines/*' + exclude: + - '/utilities/pipelines/dependencies/*' + - '/**/*.md' + +variables: + - template: '../../settings.yml' + - group: 'PLATFORM_VARIABLES' + - name: modulePath + value: '/modules/Microsoft.AVS/privateClouds' + +stages: + - template: /.azuredevops/pipelineTemplates/stages.module.yml + parameters: + removeDeployment: '${{ parameters.removeDeployment }}' + prerelease: '${{ parameters.prerelease }}' diff --git a/.github/workflows/ms.avs.privateclouds.yml b/.github/workflows/ms.avs.privateclouds.yml new file mode 100644 index 0000000000..15e86296b0 --- /dev/null +++ b/.github/workflows/ms.avs.privateclouds.yml @@ -0,0 +1,145 @@ +name: 'AVS: PrivateClouds' + +on: + workflow_dispatch: + inputs: + removeDeployment: + type: boolean + description: 'Remove deployed module' + required: false + default: true + prerelease: + type: boolean + description: 'Publish prerelease module' + required: false + default: false + push: + branches: + - main + paths: + - '.github/actions/templates/**' + - '.github/workflows/ms.avs.privateclouds.yml' + - 'modules/Microsoft.AVS/privateClouds/**' + - 'utilities/pipelines/**' + - '!utilities/pipelines/dependencies/**' + - '!*/**/readme.md' + +env: + variablesPath: 'settings.yml' + modulePath: 'modules/Microsoft.AVS/privateClouds' + workflowPath: '.github/workflows/ms.avs.privateclouds.yml' + AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }} + ARM_SUBSCRIPTION_ID: '${{ secrets.ARM_SUBSCRIPTION_ID }}' + ARM_MGMTGROUP_ID: '${{ secrets.ARM_MGMTGROUP_ID }}' + ARM_TENANT_ID: '${{ secrets.ARM_TENANT_ID }}' + TOKEN_NAMEPREFIX: '${{ secrets.TOKEN_NAMEPREFIX }}' + +jobs: + ########################### + # Initialize pipeline # + ########################### + job_initialize_pipeline: + runs-on: ubuntu-20.04 + name: 'Initialize pipeline' + steps: + - name: 'Checkout' + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: 'Set input parameters to output variables' + id: get-workflow-param + uses: ./.github/actions/templates/getWorkflowInput + with: + workflowPath: '${{ env.workflowPath}}' + - name: 'Get parameter file paths' + id: get-module-test-file-paths + uses: ./.github/actions/templates/getModuleTestFiles + with: + modulePath: '${{ env.modulePath }}' + outputs: + removeDeployment: ${{ steps.get-workflow-param.outputs.removeDeployment }} + moduleTestFilePaths: ${{ steps.get-module-test-file-paths.outputs.moduleTestFilePaths }} + + ######################### + # Static validation # + ######################### + job_module_pester_validation: + runs-on: ubuntu-20.04 + name: 'Static validation' + steps: + - name: 'Checkout' + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Set environment variables + uses: ./.github/actions/templates/setEnvironmentVariables + with: + variablesPath: ${{ env.variablesPath }} + - name: 'Run tests' + uses: ./.github/actions/templates/validateModulePester + with: + modulePath: '${{ env.modulePath }}' + moduleTestFilePath: '${{ env.moduleTestFilePath }}' + + ############################# + # Deployment validation # + ############################# + job_module_deploy_validation: + runs-on: ubuntu-20.04 + name: 'Deployment validation' + needs: + - job_initialize_pipeline + - job_module_pester_validation + strategy: + fail-fast: false + matrix: + moduleTestFilePaths: ${{ fromJSON(needs.job_initialize_pipeline.outputs.moduleTestFilePaths) }} + steps: + - name: 'Checkout' + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Set environment variables + uses: ./.github/actions/templates/setEnvironmentVariables + with: + variablesPath: ${{ env.variablesPath }} + - name: 'Using test file [${{ matrix.moduleTestFilePaths }}]' + uses: ./.github/actions/templates/validateModuleDeployment + with: + templateFilePath: '${{ env.modulePath }}/${{ matrix.moduleTestFilePaths }}' + location: '${{ env.location }}' + resourceGroupName: '${{ env.resourceGroupName }}' + subscriptionId: '${{ secrets.ARM_SUBSCRIPTION_ID }}' + managementGroupId: '${{ secrets.ARM_MGMTGROUP_ID }}' + removeDeployment: '${{ needs.job_initialize_pipeline.outputs.removeDeployment }}' + + ################## + # Publishing # + ################## + job_publish_module: + name: 'Publishing' + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' || github.event.inputs.prerelease == 'true' + runs-on: ubuntu-20.04 + needs: + - job_module_deploy_validation + steps: + - name: 'Checkout' + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Set environment variables + uses: ./.github/actions/templates/setEnvironmentVariables + with: + variablesPath: ${{ env.variablesPath }} + - name: 'Publishing' + uses: ./.github/actions/templates/publishModule + with: + templateFilePath: '${{ env.modulePath }}/deploy.bicep' + templateSpecsRGName: '${{ env.templateSpecsRGName }}' + templateSpecsRGLocation: '${{ env.templateSpecsRGLocation }}' + templateSpecsDescription: '${{ env.templateSpecsDescription }}' + templateSpecsDoPublish: '${{ env.templateSpecsDoPublish }}' + bicepRegistryName: '${{ env.bicepRegistryName }}' + bicepRegistryRGName: '${{ env.bicepRegistryRGName }}' + bicepRegistryRgLocation: '${{ env.bicepRegistryRgLocation }}' + bicepRegistryDoPublish: '${{ env.bicepRegistryDoPublish }}' diff --git a/modules/Microsoft.AVS/privateClouds/.test/common/deploy.bicep b/modules/Microsoft.AVS/privateClouds/.test/common/deploy.bicep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/modules/Microsoft.AVS/privateClouds/.test/min/deploy.bicep b/modules/Microsoft.AVS/privateClouds/.test/min/deploy.bicep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/modules/Microsoft.AVS/privateClouds/addons/deploy.bicep b/modules/Microsoft.AVS/privateClouds/addons/deploy.bicep new file mode 100644 index 0000000000..597f3dca3b --- /dev/null +++ b/modules/Microsoft.AVS/privateClouds/addons/deploy.bicep @@ -0,0 +1,62 @@ +// ============== // +// Parameters // +// ============== // + +@description('Required. Name of the addon for the private cloud') +param name string + +@description('Conditional. The name of the parent privateClouds. Required if the template is used in a standalone deployment.') +param privateCloudName string + +@description('Optional. The type of private cloud addon') +@allowed([ + 'SRM' + 'VR' + 'HCX' + 'Arc' +]) +param addonType string = '' + +@description('Optional. Enable telemetry via the Customer Usage Attribution ID (GUID).') +param enableDefaultTelemetry bool = true + +// =============== // +// Deployments // +// =============== // + +resource defaultTelemetry 'Microsoft.Resources/deployments@2021-04-01' = if (enableDefaultTelemetry) { + name: 'pid-47ed15a6-730a-4827-bcb4-0fd963ffbd82-${uniqueString(deployment().name)}' + properties: { + mode: 'Incremental' + template: { + '$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#' + contentVersion: '1.0.0.0' + resources: [] + } + } +} + +resource privateCloud 'Microsoft.AVS/privateClouds@2022-05-01' existing = { + name: privateCloudName +} + +resource addon 'Microsoft.AVS/privateClouds/addons@2022-05-01' = { + parent: privateCloud + name: name + properties: { + addonType: addonType + } +} + +// =========== // +// Outputs // +// =========== // + +@description('The name of the addon.') +output name string = addon.name + +@description('The resource ID of the addon.') +output resourceId string = addon.id + +@description('The name of the resource group the addon was created in.') +output resourceGroupName string = resourceGroup().name diff --git a/modules/Microsoft.AVS/privateClouds/addons/version.json b/modules/Microsoft.AVS/privateClouds/addons/version.json new file mode 100644 index 0000000000..41f66cc990 --- /dev/null +++ b/modules/Microsoft.AVS/privateClouds/addons/version.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", + "version": "0.1" +} diff --git a/modules/Microsoft.AVS/privateClouds/authorizations/deploy.bicep b/modules/Microsoft.AVS/privateClouds/authorizations/deploy.bicep new file mode 100644 index 0000000000..508635ef94 --- /dev/null +++ b/modules/Microsoft.AVS/privateClouds/authorizations/deploy.bicep @@ -0,0 +1,52 @@ +// ============== // +// Parameters // +// ============== // + +@description('Required. Name of the ExpressRoute Circuit Authorization in the private cloud') +param name string + +@description('Conditional. The name of the parent privateClouds. Required if the template is used in a standalone deployment.') +param privateCloudName string + +@description('Optional. Enable telemetry via the Customer Usage Attribution ID (GUID).') +param enableDefaultTelemetry bool = true + +// =============== // +// Deployments // +// =============== // + +resource defaultTelemetry 'Microsoft.Resources/deployments@2021-04-01' = if (enableDefaultTelemetry) { + name: 'pid-47ed15a6-730a-4827-bcb4-0fd963ffbd82-${uniqueString(deployment().name)}' + properties: { + mode: 'Incremental' + template: { + '$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#' + contentVersion: '1.0.0.0' + resources: [] + } + } +} + +resource privateCloud 'Microsoft.AVS/privateClouds@2022-05-01' existing = { + name: privateCloudName +} + +resource authorization 'Microsoft.AVS/privateClouds/authorizations@2022-05-01' = { + parent: privateCloud + name: name + properties: { + } +} + +// =========== // +// Outputs // +// =========== // + +@description('The name of the authorization.') +output name string = authorization.name + +@description('The resource ID of the authorization.') +output resourceId string = authorization.id + +@description('The name of the resource group the authorization was created in.') +output resourceGroupName string = resourceGroup().name diff --git a/modules/Microsoft.AVS/privateClouds/authorizations/readme.md b/modules/Microsoft.AVS/privateClouds/authorizations/readme.md new file mode 100644 index 0000000000..89ab5af150 --- /dev/null +++ b/modules/Microsoft.AVS/privateClouds/authorizations/readme.md @@ -0,0 +1,54 @@ +# AVS PrivateClouds Authorizations `[Microsoft.AVS/privateClouds/authorizations]` + +This module deploys AVS PrivateClouds Authorizations. +// TODO: Replace Resource and fill in description + +## Navigation + +- [Resource Types](#Resource-Types) +- [Parameters](#Parameters) +- [Outputs](#Outputs) +- [Cross-referenced modules](#Cross-referenced-modules) + +## Resource Types + +| Resource Type | API Version | +| :-- | :-- | +| `Microsoft.AVS/privateClouds/authorizations` | [2022-05-01](https://docs.microsoft.com/en-us/azure/templates/Microsoft.AVS/privateClouds/authorizations) | + +## Parameters + +**Required parameters** + +| Parameter Name | Type | Description | +| :-- | :-- | :-- | +| `name` | string | Name of the ExpressRoute Circuit Authorization in the private cloud | + +**Conditional parameters** + +| Parameter Name | Type | Description | +| :-- | :-- | :-- | +| `privateCloudName` | string | The name of the parent privateClouds. Required if the template is used in a standalone deployment. | + +**Optional parameters** + +| Parameter Name | Type | Default Value | Description | +| :-- | :-- | :-- | :-- | +| `enableDefaultTelemetry` | bool | `True` | Enable telemetry via the Customer Usage Attribution ID (GUID). | + + +### Parameter Usage: `` + +// TODO: Fill in Parameter usage + +## Outputs + +| Output Name | Type | Description | +| :-- | :-- | :-- | +| `name` | string | The name of the authorization. | +| `resourceGroupName` | string | The name of the resource group the authorization was created in. | +| `resourceId` | string | The resource ID of the authorization. | + +## Cross-referenced modules + +_None_ diff --git a/modules/Microsoft.AVS/privateClouds/authorizations/version.json b/modules/Microsoft.AVS/privateClouds/authorizations/version.json new file mode 100644 index 0000000000..41f66cc990 --- /dev/null +++ b/modules/Microsoft.AVS/privateClouds/authorizations/version.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", + "version": "0.1" +} diff --git a/modules/Microsoft.AVS/privateClouds/cloudLinks/deploy.bicep b/modules/Microsoft.AVS/privateClouds/cloudLinks/deploy.bicep new file mode 100644 index 0000000000..8948ad6f16 --- /dev/null +++ b/modules/Microsoft.AVS/privateClouds/cloudLinks/deploy.bicep @@ -0,0 +1,56 @@ +// ============== // +// Parameters // +// ============== // + +@description('Required. Name of the cloud link resource') +param name string + +@description('Conditional. The name of the parent privateClouds. Required if the template is used in a standalone deployment.') +param privateCloudName string + +@description('Optional. Enable telemetry via the Customer Usage Attribution ID (GUID).') +param enableDefaultTelemetry bool = true + +@description('Optional. Identifier of the other private cloud participating in the link.') +param linkedCloud string = '' + +// =============== // +// Deployments // +// =============== // + +resource defaultTelemetry 'Microsoft.Resources/deployments@2021-04-01' = if (enableDefaultTelemetry) { + name: 'pid-47ed15a6-730a-4827-bcb4-0fd963ffbd82-${uniqueString(deployment().name)}' + properties: { + mode: 'Incremental' + template: { + '$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#' + contentVersion: '1.0.0.0' + resources: [] + } + } +} + +resource privateCloud 'Microsoft.AVS/privateClouds@2022-05-01' existing = { + name: privateCloudName +} + +resource cloudLink 'Microsoft.AVS/privateClouds/cloudLinks@2022-05-01' = { + parent: privateCloud + name: name + properties: { + linkedCloud: linkedCloud + } +} + +// =========== // +// Outputs // +// =========== // + +@description('The name of the cloudLink.') +output name string = cloudLink.name + +@description('The resource ID of the cloudLink.') +output resourceId string = cloudLink.id + +@description('The name of the resource group the cloudLink was created in.') +output resourceGroupName string = resourceGroup().name diff --git a/modules/Microsoft.AVS/privateClouds/cloudLinks/readme.md b/modules/Microsoft.AVS/privateClouds/cloudLinks/readme.md new file mode 100644 index 0000000000..163ef46d6e --- /dev/null +++ b/modules/Microsoft.AVS/privateClouds/cloudLinks/readme.md @@ -0,0 +1,55 @@ +# AVS PrivateClouds CloudLinks `[Microsoft.AVS/privateClouds/cloudLinks]` + +This module deploys AVS PrivateClouds CloudLinks. +// TODO: Replace Resource and fill in description + +## Navigation + +- [Resource Types](#Resource-Types) +- [Parameters](#Parameters) +- [Outputs](#Outputs) +- [Cross-referenced modules](#Cross-referenced-modules) + +## Resource Types + +| Resource Type | API Version | +| :-- | :-- | +| `Microsoft.AVS/privateClouds/cloudLinks` | [2022-05-01](https://docs.microsoft.com/en-us/azure/templates/Microsoft.AVS/privateClouds/cloudLinks) | + +## Parameters + +**Required parameters** + +| Parameter Name | Type | Description | +| :-- | :-- | :-- | +| `name` | string | Name of the cloud link resource | + +**Conditional parameters** + +| Parameter Name | Type | Description | +| :-- | :-- | :-- | +| `privateCloudName` | string | The name of the parent privateClouds. Required if the template is used in a standalone deployment. | + +**Optional parameters** + +| Parameter Name | Type | Default Value | Description | +| :-- | :-- | :-- | :-- | +| `enableDefaultTelemetry` | bool | `True` | Enable telemetry via the Customer Usage Attribution ID (GUID). | +| `linkedCloud` | string | `''` | Identifier of the other private cloud participating in the link. | + + +### Parameter Usage: `` + +// TODO: Fill in Parameter usage + +## Outputs + +| Output Name | Type | Description | +| :-- | :-- | :-- | +| `name` | string | The name of the cloudLink. | +| `resourceGroupName` | string | The name of the resource group the cloudLink was created in. | +| `resourceId` | string | The resource ID of the cloudLink. | + +## Cross-referenced modules + +_None_ diff --git a/modules/Microsoft.AVS/privateClouds/cloudLinks/version.json b/modules/Microsoft.AVS/privateClouds/cloudLinks/version.json new file mode 100644 index 0000000000..41f66cc990 --- /dev/null +++ b/modules/Microsoft.AVS/privateClouds/cloudLinks/version.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", + "version": "0.1" +} diff --git a/modules/Microsoft.AVS/privateClouds/clusters/datastores/deploy.bicep b/modules/Microsoft.AVS/privateClouds/clusters/datastores/deploy.bicep new file mode 100644 index 0000000000..85bcc45cd9 --- /dev/null +++ b/modules/Microsoft.AVS/privateClouds/clusters/datastores/deploy.bicep @@ -0,0 +1,67 @@ +// ============== // +// Parameters // +// ============== // + +@description('Required. Name of the datastore in the private cloud cluster') +param name string + +@description('Conditional. The name of the parent clusters. Required if the template is used in a standalone deployment.') +param clusterName string + +@description('Conditional. The name of the parent privateClouds. Required if the template is used in a standalone deployment.') +param privateCloudName string + +@description('Optional. An iSCSI volume from Microsoft.StoragePool provider') +param diskPoolVolume object = {} + +@description('Optional. Enable telemetry via the Customer Usage Attribution ID (GUID).') +param enableDefaultTelemetry bool = true + +@description('Optional. An Azure NetApp Files volume from Microsoft.NetApp provider') +param netAppVolume object = {} + +// =============== // +// Deployments // +// =============== // + +resource defaultTelemetry 'Microsoft.Resources/deployments@2021-04-01' = if (enableDefaultTelemetry) { + name: 'pid-47ed15a6-730a-4827-bcb4-0fd963ffbd82-${uniqueString(deployment().name)}' + properties: { + mode: 'Incremental' + template: { + '$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#' + contentVersion: '1.0.0.0' + resources: [] + } + } +} + +resource privateCloud 'Microsoft.AVS/privateClouds@2022-05-01' existing = { + name: privateCloudName + + resource cluster 'clusters@2022-05-01' existing = { + name: clusterName + } +} + +resource datastore 'Microsoft.AVS/privateClouds/clusters/datastores@2022-05-01' = { + parent: privateCloud::cluster + name: name + properties: { + diskPoolVolume: diskPoolVolume + netAppVolume: netAppVolume + } +} + +// =========== // +// 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 diff --git a/modules/Microsoft.AVS/privateClouds/clusters/datastores/readme.md b/modules/Microsoft.AVS/privateClouds/clusters/datastores/readme.md new file mode 100644 index 0000000000..dcecf01a6c --- /dev/null +++ b/modules/Microsoft.AVS/privateClouds/clusters/datastores/readme.md @@ -0,0 +1,57 @@ +# AVS PrivateClouds Clusters Datastores `[Microsoft.AVS/privateClouds/clusters/datastores]` + +This module deploys AVS PrivateClouds Clusters Datastores. +// TODO: Replace Resource and fill in description + +## Navigation + +- [Resource Types](#Resource-Types) +- [Parameters](#Parameters) +- [Outputs](#Outputs) +- [Cross-referenced modules](#Cross-referenced-modules) + +## Resource Types + +| Resource Type | API Version | +| :-- | :-- | +| `Microsoft.AVS/privateClouds/clusters/datastores` | [2022-05-01](https://docs.microsoft.com/en-us/azure/templates/Microsoft.AVS/privateClouds/clusters/datastores) | + +## Parameters + +**Required parameters** + +| Parameter Name | Type | Description | +| :-- | :-- | :-- | +| `name` | string | Name of the datastore in the private cloud cluster | + +**Conditional parameters** + +| Parameter Name | Type | Description | +| :-- | :-- | :-- | +| `clusterName` | string | The name of the parent clusters. Required if the template is used in a standalone deployment. | +| `privateCloudName` | string | The name of the parent privateClouds. Required if the template is used in a standalone deployment. | + +**Optional parameters** + +| Parameter Name | Type | Default Value | Description | +| :-- | :-- | :-- | :-- | +| `diskPoolVolume` | object | `{object}` | An iSCSI volume from Microsoft.StoragePool provider | +| `enableDefaultTelemetry` | bool | `True` | Enable telemetry via the Customer Usage Attribution ID (GUID). | +| `netAppVolume` | object | `{object}` | An Azure NetApp Files volume from Microsoft.NetApp provider | + + +### Parameter Usage: `` + +// TODO: Fill in Parameter usage + +## Outputs + +| Output Name | Type | Description | +| :-- | :-- | :-- | +| `name` | string | The name of the datastore. | +| `resourceGroupName` | string | The name of the resource group the datastore was created in. | +| `resourceId` | string | The resource ID of the datastore. | + +## Cross-referenced modules + +_None_ diff --git a/modules/Microsoft.AVS/privateClouds/clusters/datastores/version.json b/modules/Microsoft.AVS/privateClouds/clusters/datastores/version.json new file mode 100644 index 0000000000..41f66cc990 --- /dev/null +++ b/modules/Microsoft.AVS/privateClouds/clusters/datastores/version.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", + "version": "0.1" +} diff --git a/modules/Microsoft.AVS/privateClouds/clusters/deploy.bicep b/modules/Microsoft.AVS/privateClouds/clusters/deploy.bicep new file mode 100644 index 0000000000..f400449f8c --- /dev/null +++ b/modules/Microsoft.AVS/privateClouds/clusters/deploy.bicep @@ -0,0 +1,105 @@ +// ============== // +// Parameters // +// ============== // + +@description('Required. Name of the cluster in the private cloud') +param name string + +@description('Required. The resource model definition representing SKU') +param sku object + +@description('Conditional. The name of the parent privateClouds. Required if the template is used in a standalone deployment.') +param privateCloudName string + +@description('Optional. The cluster size') +param clusterSize int = + +@description('Optional. The datastores to create as part of the cluster.') +param datastores array = [] + +@description('Optional. Enable telemetry via the Customer Usage Attribution ID (GUID).') +param enableDefaultTelemetry bool = true + +@description('Optional. The hosts') +param hosts array = [] + +@description('Optional. The placementPolicies to create as part of the cluster.') +param placementPolicies array = [] + +// ============= // +// Variables // +// ============= // + +var enableReferencedModulesTelemetry = false + +// =============== // +// Deployments // +// =============== // + +resource defaultTelemetry 'Microsoft.Resources/deployments@2021-04-01' = if (enableDefaultTelemetry) { + name: 'pid-47ed15a6-730a-4827-bcb4-0fd963ffbd82-${uniqueString(deployment().name)}' + properties: { + mode: 'Incremental' + template: { + '$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#' + contentVersion: '1.0.0.0' + resources: [] + } + } +} + +resource privateCloud 'Microsoft.AVS/privateClouds@2022-05-01' existing = { + name: privateCloudName +} + +resource cluster 'Microsoft.AVS/privateClouds/clusters@2022-05-01' = { + parent: privateCloud + name: name + sku: sku + properties: { + clusterSize: clusterSize + hosts: hosts + } +} + +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: { + privateCloudName: privateCloudName + clusterName: name + affinityStrength: contains(placementPolicy, 'affinityStrength') ? placementPolicy.affinityStrength : '' + azureHybridBenefitType: contains(placementPolicy, 'azureHybridBenefitType') ? placementPolicy.azureHybridBenefitType : '' + displayName: contains(placementPolicy, 'displayName') ? placementPolicy.displayName : '' + hostMembers: contains(placementPolicy, 'hostMembers') ? placementPolicy.hostMembers : [] + name: placementPolicy.name + state: contains(placementPolicy, 'state') ? placementPolicy.state : '' + type: contains(placementPolicy, 'type') ? placementPolicy.type : '' + vmMembers: contains(placementPolicy, 'vmMembers') ? placementPolicy.vmMembers : [] + enableDefaultTelemetry: enableReferencedModulesTelemetry + } +}] + +// =========== // +// Outputs // +// =========== // + +@description('The name of the cluster.') +output name string = cluster.name + +@description('The resource ID of the cluster.') +output resourceId string = cluster.id + +@description('The name of the resource group the cluster was created in.') +output resourceGroupName string = resourceGroup().name diff --git a/modules/Microsoft.AVS/privateClouds/clusters/placementPolicies/deploy.bicep b/modules/Microsoft.AVS/privateClouds/clusters/placementPolicies/deploy.bicep new file mode 100644 index 0000000000..bb78b7de40 --- /dev/null +++ b/modules/Microsoft.AVS/privateClouds/clusters/placementPolicies/deploy.bicep @@ -0,0 +1,103 @@ +// ============== // +// Parameters // +// ============== // + +@description('Required. Name of the VMware vSphere Distributed Resource Scheduler (DRS) placement policy') +param name string + +@description('Conditional. The name of the parent clusters. Required if the template is used in a standalone deployment.') +param clusterName string + +@description('Conditional. The name of the parent privateClouds. Required if the template is used in a standalone deployment.') +param privateCloudName string + +@description('Optional. VM-Host placement policy affinity strength (should/must)') +@allowed([ + 'Should' + 'Must' +]) +param affinityStrength string = '' + +@description('Optional. Placement policy hosts opt-in Azure Hybrid Benefit type') +@allowed([ + 'SqlHost' + 'None' +]) +param azureHybridBenefitType string = '' + +@description('Optional. Display name of the placement policy') +param displayName string = '' + +@description('Optional. Enable telemetry via the Customer Usage Attribution ID (GUID).') +param enableDefaultTelemetry bool = true + +@description('Optional. Host members list') +param hostMembers array = [] + +@description('Optional. Whether the placement policy is enabled or disabled') +@allowed([ + 'Enabled' + 'Disabled' +]) +param state string = '' + +@description('Optional. placement policy type') +@allowed([ + 'VmVm' + 'VmHost' +]) +param type string = '' + +@description('Optional. Virtual machine members list') +param vmMembers array = [] + +// =============== // +// Deployments // +// =============== // + +resource defaultTelemetry 'Microsoft.Resources/deployments@2021-04-01' = if (enableDefaultTelemetry) { + name: 'pid-47ed15a6-730a-4827-bcb4-0fd963ffbd82-${uniqueString(deployment().name)}' + properties: { + mode: 'Incremental' + template: { + '$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#' + contentVersion: '1.0.0.0' + resources: [] + } + } +} + +resource privateCloud 'Microsoft.AVS/privateClouds@2022-05-01' existing = { + name: privateCloudName + + resource cluster 'clusters@2022-05-01' existing = { + name: clusterName + } +} + +resource placementPolicy 'Microsoft.AVS/privateClouds/clusters/placementPolicies@2022-05-01' = { + parent: privateCloud::cluster + name: name + properties: { + affinityStrength: affinityStrength + azureHybridBenefitType: azureHybridBenefitType + displayName: displayName + hostMembers: hostMembers + state: state + type: type + vmMembers: vmMembers + } +} + +// =========== // +// Outputs // +// =========== // + +@description('The name of the placementPolicy.') +output name string = placementPolicy.name + +@description('The resource ID of the placementPolicy.') +output resourceId string = placementPolicy.id + +@description('The name of the resource group the placementPolicy was created in.') +output resourceGroupName string = resourceGroup().name diff --git a/modules/Microsoft.AVS/privateClouds/clusters/placementPolicies/version.json b/modules/Microsoft.AVS/privateClouds/clusters/placementPolicies/version.json new file mode 100644 index 0000000000..41f66cc990 --- /dev/null +++ b/modules/Microsoft.AVS/privateClouds/clusters/placementPolicies/version.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", + "version": "0.1" +} diff --git a/modules/Microsoft.AVS/privateClouds/clusters/version.json b/modules/Microsoft.AVS/privateClouds/clusters/version.json new file mode 100644 index 0000000000..41f66cc990 --- /dev/null +++ b/modules/Microsoft.AVS/privateClouds/clusters/version.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", + "version": "0.1" +} diff --git a/modules/Microsoft.AVS/privateClouds/deploy.bicep b/modules/Microsoft.AVS/privateClouds/deploy.bicep new file mode 100644 index 0000000000..0a76029ca3 --- /dev/null +++ b/modules/Microsoft.AVS/privateClouds/deploy.bicep @@ -0,0 +1,411 @@ +// ============== // +// 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 = [] + +@description('Optional. The authorizations to create as part of the privateCloud.') +param authorizations array = [] + +@description('Optional. The properties describing private cloud availability zone distribution') +param availability object = {} + +@description('Optional. An ExpressRoute Circuit') +param circuit object = {} + +@description('Optional. The cloudLinks to create as part of the privateCloud.') +param cloudLinks array = [] + +@description('Optional. The clusters to create as part of the privateCloud.') +param clusters array = [] + +@description('Optional. The dhcpConfigurations to create as part of the privateCloud.') +param dhcpConfigurations array = [] + +@description('Optional. 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.') +param diagnosticEventHubAuthorizationRuleId string = '' + +@description('Optional. 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.') +param diagnosticEventHubName string = '' + +@description('Optional. The name of logs that will be streamed.') +@allowed([ + 'CapacityLatest' + 'DiskUsedPercentage' + 'EffectiveCpuAverage' + 'EffectiveMemAverage' + 'OverheadAverage' + 'TotalMbAverage' + 'UsageAverage' + 'UsedLatest' +]) +param diagnosticLogCategoriesToEnable array = [ + 'CapacityLatest' + 'DiskUsedPercentage' + 'EffectiveCpuAverage' + 'EffectiveMemAverage' + 'OverheadAverage' + 'TotalMbAverage' + 'UsageAverage' + 'UsedLatest' +] + +@description('Optional. Specifies the number of days that logs will be kept for; a value of 0 will retain data indefinitely.') +param diagnosticLogsRetentionInDays int = 365 + +@description('Optional. The name of the diagnostic setting, if deployed.') +param diagnosticSettingsName string = '${name}-diagnosticSettings' + +@description('Optional. 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.') +param diagnosticStorageAccountId string = '' + +@description('Optional. 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.') +param diagnosticWorkspaceId string = '' + +@description('Optional. The dnsServices to create as part of the privateCloud.') +param dnsServices array = [] + +@description('Optional. The dnsZones to create as part of the privateCloud.') +param dnsZones array = [] + +@description('Optional. Enable telemetry via the Customer Usage Attribution ID (GUID).') +param enableDefaultTelemetry bool = true + +@description('Optional. The properties of customer managed encryption key') +param encryption object = {} + +@description('Optional. The globalReachConnections to create as part of the privateCloud.') +param globalReachConnections array = [] + +@description('Optional. The hcxEnterpriseSites to create as part of the privateCloud.') +param hcxEnterpriseSites array = [] + +@description('Optional. Identity for the virtual machine.') +param identity object = {} + +@description('Optional. vCenter Single Sign On Identity Sources') +param identitySources array = [] + +@description('Optional. Connectivity to internet is enabled or disabled') +@allowed([ + 'Enabled' + 'Disabled' +]) +param internet string = 'Disabled' + +@description('Optional. Location for all Resources.') +param location string = resourceGroup().location + +@description('Optional. Specify the type of lock.') +@allowed([ + '' + 'CanNotDelete' + 'ReadOnly' +]) +param lock string = '' + +@description('Optional. The properties of a management cluster') +param managementCluster object = {} + +@description('Optional. 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') +param networkBlock string = '' + +@description('Optional. Optionally, set the NSX-T Manager password when the private cloud is created') +@secure() +param nsxtPassword string = '' + +@description('Optional. The portMirroringProfiles to create as part of the privateCloud.') +param portMirroringProfiles array = [] + +@description('Optional. The publicIPs to create as part of the privateCloud.') +param publicIPs array = [] + +@description('Optional. The scriptExecutions to create as part of the privateCloud.') +param scriptExecutions array = [] + +@description('Optional. An ExpressRoute Circuit') +param secondaryCircuit object = {} + +@description('Optional. The segments to create as part of the privateCloud.') +param segments array = [] + +@description('Optional. Resource tags') +param tags object = {} + +@description('Optional. Optionally, set the vCenter admin password when the private cloud is created') +@secure() +param vcenterPassword string = '' + +@description('Optional. The vmGroups to create as part of the privateCloud.') +param vmGroups array = [] + +// ============= // +// Variables // +// ============= // + +var diagnosticsLogs = [for category in diagnosticLogCategoriesToEnable: { + category: category + enabled: true + retentionPolicy: { + enabled: true + days: diagnosticLogsRetentionInDays + } +}] + +var enableReferencedModulesTelemetry = false + +// =============== // +// Deployments // +// =============== // + +resource defaultTelemetry 'Microsoft.Resources/deployments@2021-04-01' = if (enableDefaultTelemetry) { + name: 'pid-47ed15a6-730a-4827-bcb4-0fd963ffbd82-${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: [] + } + } +} + + +resource privateCloud 'Microsoft.AVS/privateClouds@2022-05-01' = { + identity: identity + location: location + name: name + sku: sku + tags: tags + properties: { + availability: availability + circuit: circuit + encryption: encryption + identitySources: identitySources + internet: internet + managementCluster: managementCluster + networkBlock: networkBlock + nsxtPassword: nsxtPassword + secondaryCircuit: secondaryCircuit + vcenterPassword: vcenterPassword + } +} + +resource privateCloud_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 + logs: diagnosticsLogs + } + scope: privateCloud +} + +resource privateCloud_lock 'Microsoft.Authorization/locks@2017-04-01' = if (!empty(lock)) { + name: '${privateCloud.name}-${lock}-lock' + properties: { + level: any(lock) + notes: lock == 'CanNotDelete' ? 'Cannot delete resource or child resources.' : 'Cannot modify the resource or child resources.' + } + 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: { + privateCloudName: name + name: authorization.name + enableDefaultTelemetry: enableReferencedModulesTelemetry + } +}] + +module privateCloud_cloudLinks 'cloudLinks/deploy.bicep' = [for (cloudLink, index) in cloudLinks: { + name: '${uniqueString(deployment().name, location)}-privateCloud-cloudLink-${index}' + params: { + privateCloudName: name + linkedCloud: contains(cloudLink, 'linkedCloud') ? cloudLink.linkedCloud : '' + name: cloudLink.name + enableDefaultTelemetry: enableReferencedModulesTelemetry + } +}] + +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_globalReachConnections 'globalReachConnections/deploy.bicep' = [for (globalReachConnection, index) in globalReachConnections: { + name: '${uniqueString(deployment().name, location)}-privateCloud-globalReachConnection-${index}' + params: { + privateCloudName: name + authorizationKey: contains(globalReachConnection, 'authorizationKey') ? globalReachConnection.authorizationKey : '' + expressRouteId: contains(globalReachConnection, 'expressRouteId') ? globalReachConnection.expressRouteId : '' + name: globalReachConnection.name + peerExpressRouteCircuit: contains(globalReachConnection, 'peerExpressRouteCircuit') ? globalReachConnection.peerExpressRouteCircuit : '' + enableDefaultTelemetry: enableReferencedModulesTelemetry + } +}] + +module privateCloud_hcxEnterpriseSites 'hcxEnterpriseSites/deploy.bicep' = [for (hcxEnterpriseSite, index) in hcxEnterpriseSites: { + name: '${uniqueString(deployment().name, location)}-privateCloud-hcxEnterpriseSite-${index}' + params: { + privateCloudName: name + name: hcxEnterpriseSite.name + enableDefaultTelemetry: enableReferencedModulesTelemetry + } +}] + +module privateCloud_scriptExecutions 'scriptExecutions/deploy.bicep' = [for (scriptExecution, index) in scriptExecutions: { + name: '${uniqueString(deployment().name, location)}-privateCloud-scriptExecution-${index}' + params: { + privateCloudName: name + failureReason: contains(scriptExecution, 'failureReason') ? scriptExecution.failureReason : '' + hiddenParameters: contains(scriptExecution, 'hiddenParameters') ? scriptExecution.hiddenParameters : [] + name: scriptExecution.name + namedOutputs: contains(scriptExecution, 'namedOutputs') ? scriptExecution.namedOutputs : {} + output: contains(scriptExecution, 'output') ? scriptExecution.output : [] + parameters: contains(scriptExecution, 'parameters') ? scriptExecution.parameters : [] + retention: contains(scriptExecution, 'retention') ? scriptExecution.retention : '' + scriptCmdletId: contains(scriptExecution, 'scriptCmdletId') ? scriptExecution.scriptCmdletId : '' + timeout: contains(scriptExecution, 'timeout') ? scriptExecution.timeout : '' + enableDefaultTelemetry: enableReferencedModulesTelemetry + } +}] + +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: { + privateCloudName: name + workloadNetworkName: 'default' + defaultDnsZone: contains(dnsService, 'defaultDnsZone') ? dnsService.defaultDnsZone : '' + displayName: contains(dnsService, 'displayName') ? dnsService.displayName : '' + dnsServiceIp: contains(dnsService, 'dnsServiceIp') ? dnsService.dnsServiceIp : '' + fqdnZones: contains(dnsService, 'fqdnZones') ? dnsService.fqdnZones : [] + logLevel: contains(dnsService, 'logLevel') ? dnsService.logLevel : '' + name: dnsService.name + revision: contains(dnsService, 'revision') ? dnsService.revision : + enableDefaultTelemetry: enableReferencedModulesTelemetry + } +}] + +module workloadNetworks_privateCloud_dnsZones 'workloadNetworks/dnsZones/deploy.bicep' = [for (dnsZone, index) in dnsZones: { + name: '${uniqueString(deployment().name, location)}-privateCloud-dnsZone-${index}' + params: { + privateCloudName: name + workloadNetworkName: 'default' + displayName: contains(dnsZone, 'displayName') ? dnsZone.displayName : '' + dnsServerIps: contains(dnsZone, 'dnsServerIps') ? dnsZone.dnsServerIps : [] + dnsServices: contains(dnsZone, 'dnsServices') ? dnsZone.dnsServices : + domain: contains(dnsZone, 'domain') ? dnsZone.domain : [] + name: dnsZone.name + revision: contains(dnsZone, 'revision') ? dnsZone.revision : + sourceIp: contains(dnsZone, 'sourceIp') ? dnsZone.sourceIp : '' + enableDefaultTelemetry: enableReferencedModulesTelemetry + } +}] + +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' + 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_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(publicIP, 'displayName') ? publicIP.displayName : '' + name: publicIP.name + numberOfPublicIPs: contains(publicIP, 'numberOfPublicIPs') ? publicIP.numberOfPublicIPs : + enableDefaultTelemetry: enableReferencedModulesTelemetry + } +}] + +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_vmGroups 'workloadNetworks/vmGroups/deploy.bicep' = [for (vmGroup, index) in vmGroups: { + name: '${uniqueString(deployment().name, location)}-privateCloud-vmGroup-${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 : + enableDefaultTelemetry: enableReferencedModulesTelemetry + } +}] + +// =========== // +// Outputs // +// =========== // + +@description('The name of the privateCloud.') +output name string = privateCloud.name + +@description('The resource ID of the privateCloud.') +output resourceId string = privateCloud.id + +@description('The name of the resource group the privateCloud was created in.') +output resourceGroupName string = resourceGroup().name diff --git a/modules/Microsoft.AVS/privateClouds/globalReachConnections/deploy.bicep b/modules/Microsoft.AVS/privateClouds/globalReachConnections/deploy.bicep new file mode 100644 index 0000000000..bd98d9ca91 --- /dev/null +++ b/modules/Microsoft.AVS/privateClouds/globalReachConnections/deploy.bicep @@ -0,0 +1,64 @@ +// ============== // +// Parameters // +// ============== // + +@description('Required. Name of the global reach connection in the private cloud') +param name string + +@description('Conditional. The name of the parent privateClouds. Required if the template is used in a standalone deployment.') +param privateCloudName string + +@description('Optional. Authorization key from the peer express route used for the global reach connection') +param authorizationKey string = '' + +@description('Optional. Enable telemetry via the Customer Usage Attribution ID (GUID).') +param enableDefaultTelemetry bool = true + +@description('Optional. The ID of the Private Cloud\'s ExpressRoute Circuit that is participating in the global reach connection') +param expressRouteId string = '' + +@description('Optional. Identifier of the ExpressRoute Circuit to peer with in the global reach connection') +param peerExpressRouteCircuit string = '' + +// =============== // +// Deployments // +// =============== // + +resource defaultTelemetry 'Microsoft.Resources/deployments@2021-04-01' = if (enableDefaultTelemetry) { + name: 'pid-47ed15a6-730a-4827-bcb4-0fd963ffbd82-${uniqueString(deployment().name)}' + properties: { + mode: 'Incremental' + template: { + '$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#' + contentVersion: '1.0.0.0' + resources: [] + } + } +} + +resource privateCloud 'Microsoft.AVS/privateClouds@2022-05-01' existing = { + name: privateCloudName +} + +resource globalReachConnection 'Microsoft.AVS/privateClouds/globalReachConnections@2022-05-01' = { + parent: privateCloud + name: name + properties: { + authorizationKey: authorizationKey + expressRouteId: expressRouteId + peerExpressRouteCircuit: peerExpressRouteCircuit + } +} + +// =========== // +// Outputs // +// =========== // + +@description('The name of the globalReachConnection.') +output name string = globalReachConnection.name + +@description('The resource ID of the globalReachConnection.') +output resourceId string = globalReachConnection.id + +@description('The name of the resource group the globalReachConnection was created in.') +output resourceGroupName string = resourceGroup().name diff --git a/modules/Microsoft.AVS/privateClouds/globalReachConnections/readme.md b/modules/Microsoft.AVS/privateClouds/globalReachConnections/readme.md new file mode 100644 index 0000000000..3da1c7294f --- /dev/null +++ b/modules/Microsoft.AVS/privateClouds/globalReachConnections/readme.md @@ -0,0 +1,57 @@ +# AVS PrivateClouds GlobalReachConnections `[Microsoft.AVS/privateClouds/globalReachConnections]` + +This module deploys AVS PrivateClouds GlobalReachConnections. +// TODO: Replace Resource and fill in description + +## Navigation + +- [Resource Types](#Resource-Types) +- [Parameters](#Parameters) +- [Outputs](#Outputs) +- [Cross-referenced modules](#Cross-referenced-modules) + +## Resource Types + +| Resource Type | API Version | +| :-- | :-- | +| `Microsoft.AVS/privateClouds/globalReachConnections` | [2022-05-01](https://docs.microsoft.com/en-us/azure/templates/Microsoft.AVS/privateClouds/globalReachConnections) | + +## Parameters + +**Required parameters** + +| Parameter Name | Type | Description | +| :-- | :-- | :-- | +| `name` | string | Name of the global reach connection in the private cloud | + +**Conditional parameters** + +| Parameter Name | Type | Description | +| :-- | :-- | :-- | +| `privateCloudName` | string | The name of the parent privateClouds. Required if the template is used in a standalone deployment. | + +**Optional parameters** + +| Parameter Name | Type | Default Value | Description | +| :-- | :-- | :-- | :-- | +| `authorizationKey` | string | `''` | Authorization key from the peer express route used for the global reach connection | +| `enableDefaultTelemetry` | bool | `True` | Enable telemetry via the Customer Usage Attribution ID (GUID). | +| `expressRouteId` | string | `''` | The ID of the Private Cloud's ExpressRoute Circuit that is participating in the global reach connection | +| `peerExpressRouteCircuit` | string | `''` | Identifier of the ExpressRoute Circuit to peer with in the global reach connection | + + +### Parameter Usage: `` + +// TODO: Fill in Parameter usage + +## Outputs + +| Output Name | Type | Description | +| :-- | :-- | :-- | +| `name` | string | The name of the globalReachConnection. | +| `resourceGroupName` | string | The name of the resource group the globalReachConnection was created in. | +| `resourceId` | string | The resource ID of the globalReachConnection. | + +## Cross-referenced modules + +_None_ diff --git a/modules/Microsoft.AVS/privateClouds/globalReachConnections/version.json b/modules/Microsoft.AVS/privateClouds/globalReachConnections/version.json new file mode 100644 index 0000000000..41f66cc990 --- /dev/null +++ b/modules/Microsoft.AVS/privateClouds/globalReachConnections/version.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", + "version": "0.1" +} diff --git a/modules/Microsoft.AVS/privateClouds/hcxEnterpriseSites/deploy.bicep b/modules/Microsoft.AVS/privateClouds/hcxEnterpriseSites/deploy.bicep new file mode 100644 index 0000000000..f7206b8c05 --- /dev/null +++ b/modules/Microsoft.AVS/privateClouds/hcxEnterpriseSites/deploy.bicep @@ -0,0 +1,52 @@ +// ============== // +// Parameters // +// ============== // + +@description('Required. Name of the HCX Enterprise Site in the private cloud') +param name string + +@description('Conditional. The name of the parent privateClouds. Required if the template is used in a standalone deployment.') +param privateCloudName string + +@description('Optional. Enable telemetry via the Customer Usage Attribution ID (GUID).') +param enableDefaultTelemetry bool = true + +// =============== // +// Deployments // +// =============== // + +resource defaultTelemetry 'Microsoft.Resources/deployments@2021-04-01' = if (enableDefaultTelemetry) { + name: 'pid-47ed15a6-730a-4827-bcb4-0fd963ffbd82-${uniqueString(deployment().name)}' + properties: { + mode: 'Incremental' + template: { + '$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#' + contentVersion: '1.0.0.0' + resources: [] + } + } +} + +resource privateCloud 'Microsoft.AVS/privateClouds@2022-05-01' existing = { + name: privateCloudName +} + +resource hcxEnterpriseSite 'Microsoft.AVS/privateClouds/hcxEnterpriseSites@2022-05-01' = { + parent: privateCloud + name: name + properties: { + } +} + +// =========== // +// Outputs // +// =========== // + +@description('The name of the hcxEnterpriseSite.') +output name string = hcxEnterpriseSite.name + +@description('The resource ID of the hcxEnterpriseSite.') +output resourceId string = hcxEnterpriseSite.id + +@description('The name of the resource group the hcxEnterpriseSite was created in.') +output resourceGroupName string = resourceGroup().name diff --git a/modules/Microsoft.AVS/privateClouds/hcxEnterpriseSites/readme.md b/modules/Microsoft.AVS/privateClouds/hcxEnterpriseSites/readme.md new file mode 100644 index 0000000000..7a0a111fdf --- /dev/null +++ b/modules/Microsoft.AVS/privateClouds/hcxEnterpriseSites/readme.md @@ -0,0 +1,54 @@ +# AVS PrivateClouds HcxEnterpriseSites `[Microsoft.AVS/privateClouds/hcxEnterpriseSites]` + +This module deploys AVS PrivateClouds HcxEnterpriseSites. +// TODO: Replace Resource and fill in description + +## Navigation + +- [Resource Types](#Resource-Types) +- [Parameters](#Parameters) +- [Outputs](#Outputs) +- [Cross-referenced modules](#Cross-referenced-modules) + +## Resource Types + +| Resource Type | API Version | +| :-- | :-- | +| `Microsoft.AVS/privateClouds/hcxEnterpriseSites` | [2022-05-01](https://docs.microsoft.com/en-us/azure/templates/Microsoft.AVS/privateClouds/hcxEnterpriseSites) | + +## Parameters + +**Required parameters** + +| Parameter Name | Type | Description | +| :-- | :-- | :-- | +| `name` | string | Name of the HCX Enterprise Site in the private cloud | + +**Conditional parameters** + +| Parameter Name | Type | Description | +| :-- | :-- | :-- | +| `privateCloudName` | string | The name of the parent privateClouds. Required if the template is used in a standalone deployment. | + +**Optional parameters** + +| Parameter Name | Type | Default Value | Description | +| :-- | :-- | :-- | :-- | +| `enableDefaultTelemetry` | bool | `True` | Enable telemetry via the Customer Usage Attribution ID (GUID). | + + +### Parameter Usage: `` + +// TODO: Fill in Parameter usage + +## Outputs + +| Output Name | Type | Description | +| :-- | :-- | :-- | +| `name` | string | The name of the hcxEnterpriseSite. | +| `resourceGroupName` | string | The name of the resource group the hcxEnterpriseSite was created in. | +| `resourceId` | string | The resource ID of the hcxEnterpriseSite. | + +## Cross-referenced modules + +_None_ diff --git a/modules/Microsoft.AVS/privateClouds/hcxEnterpriseSites/version.json b/modules/Microsoft.AVS/privateClouds/hcxEnterpriseSites/version.json new file mode 100644 index 0000000000..41f66cc990 --- /dev/null +++ b/modules/Microsoft.AVS/privateClouds/hcxEnterpriseSites/version.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", + "version": "0.1" +} diff --git a/modules/Microsoft.AVS/privateClouds/scriptExecutions/deploy.bicep b/modules/Microsoft.AVS/privateClouds/scriptExecutions/deploy.bicep new file mode 100644 index 0000000000..5422b3033b --- /dev/null +++ b/modules/Microsoft.AVS/privateClouds/scriptExecutions/deploy.bicep @@ -0,0 +1,84 @@ +// ============== // +// Parameters // +// ============== // + +@description('Required. Name of the user-invoked script execution resource') +param name string + +@description('Conditional. The name of the parent privateClouds. Required if the template is used in a standalone deployment.') +param privateCloudName string + +@description('Optional. Enable telemetry via the Customer Usage Attribution ID (GUID).') +param enableDefaultTelemetry bool = true + +@description('Optional. Error message if the script was able to run, but if the script itself had errors or powershell threw an exception') +param failureReason string = '' + +@description('Optional. Parameters that will be hidden/not visible to ARM, such as passwords and credentials') +param hiddenParameters array = [] + +@description('Optional. User-defined dictionary.') +param namedOutputs object = {} + +@description('Optional. Standard output stream from the powershell execution') +param output array = [] + +@description('Optional. Parameters the script will accept') +param parameters array = [] + +@description('Optional. Time to live for the resource. If not provided, will be available for 60 days') +param retention string = '' + +@description('Optional. A reference to the script cmdlet resource if user is running a AVS script') +param scriptCmdletId string = '' + +@description('Optional. Time limit for execution') +param timeout string = '' + +// =============== // +// Deployments // +// =============== // + +resource defaultTelemetry 'Microsoft.Resources/deployments@2021-04-01' = if (enableDefaultTelemetry) { + name: 'pid-47ed15a6-730a-4827-bcb4-0fd963ffbd82-${uniqueString(deployment().name)}' + properties: { + mode: 'Incremental' + template: { + '$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#' + contentVersion: '1.0.0.0' + resources: [] + } + } +} + +resource privateCloud 'Microsoft.AVS/privateClouds@2022-05-01' existing = { + name: privateCloudName +} + +resource scriptExecution 'Microsoft.AVS/privateClouds/scriptExecutions@2022-05-01' = { + parent: privateCloud + name: name + properties: { + failureReason: failureReason + hiddenParameters: hiddenParameters + namedOutputs: namedOutputs + output: output + parameters: parameters + retention: retention + scriptCmdletId: scriptCmdletId + timeout: timeout + } +} + +// =========== // +// Outputs // +// =========== // + +@description('The name of the scriptExecution.') +output name string = scriptExecution.name + +@description('The resource ID of the scriptExecution.') +output resourceId string = scriptExecution.id + +@description('The name of the resource group the scriptExecution was created in.') +output resourceGroupName string = resourceGroup().name diff --git a/modules/Microsoft.AVS/privateClouds/scriptExecutions/readme.md b/modules/Microsoft.AVS/privateClouds/scriptExecutions/readme.md new file mode 100644 index 0000000000..50ea7501f0 --- /dev/null +++ b/modules/Microsoft.AVS/privateClouds/scriptExecutions/readme.md @@ -0,0 +1,62 @@ +# AVS PrivateClouds ScriptExecutions `[Microsoft.AVS/privateClouds/scriptExecutions]` + +This module deploys AVS PrivateClouds ScriptExecutions. +// TODO: Replace Resource and fill in description + +## Navigation + +- [Resource Types](#Resource-Types) +- [Parameters](#Parameters) +- [Outputs](#Outputs) +- [Cross-referenced modules](#Cross-referenced-modules) + +## Resource Types + +| Resource Type | API Version | +| :-- | :-- | +| `Microsoft.AVS/privateClouds/scriptExecutions` | [2022-05-01](https://docs.microsoft.com/en-us/azure/templates/Microsoft.AVS/privateClouds/scriptExecutions) | + +## Parameters + +**Required parameters** + +| Parameter Name | Type | Description | +| :-- | :-- | :-- | +| `name` | string | Name of the user-invoked script execution resource | + +**Conditional parameters** + +| Parameter Name | Type | Description | +| :-- | :-- | :-- | +| `privateCloudName` | string | The name of the parent privateClouds. Required if the template is used in a standalone deployment. | + +**Optional parameters** + +| Parameter Name | Type | Default Value | Description | +| :-- | :-- | :-- | :-- | +| `enableDefaultTelemetry` | bool | `True` | Enable telemetry via the Customer Usage Attribution ID (GUID). | +| `failureReason` | string | `''` | Error message if the script was able to run, but if the script itself had errors or powershell threw an exception | +| `hiddenParameters` | array | `[]` | Parameters that will be hidden/not visible to ARM, such as passwords and credentials | +| `namedOutputs` | object | `{object}` | User-defined dictionary. | +| `output` | array | `[]` | Standard output stream from the powershell execution | +| `parameters` | array | `[]` | Parameters the script will accept | +| `retention` | string | `''` | Time to live for the resource. If not provided, will be available for 60 days | +| `scriptCmdletId` | string | `''` | A reference to the script cmdlet resource if user is running a AVS script | +| `timeout` | string | `''` | Time limit for execution | + + +### Parameter Usage: `` + +// TODO: Fill in Parameter usage + +## Outputs + +| Output Name | Type | Description | +| :-- | :-- | :-- | +| `name` | string | The name of the scriptExecution. | +| `resourceGroupName` | string | The name of the resource group the scriptExecution was created in. | +| `resourceId` | string | The resource ID of the scriptExecution. | + +## Cross-referenced modules + +_None_ diff --git a/modules/Microsoft.AVS/privateClouds/scriptExecutions/version.json b/modules/Microsoft.AVS/privateClouds/scriptExecutions/version.json new file mode 100644 index 0000000000..41f66cc990 --- /dev/null +++ b/modules/Microsoft.AVS/privateClouds/scriptExecutions/version.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", + "version": "0.1" +} diff --git a/modules/Microsoft.AVS/privateClouds/version.json b/modules/Microsoft.AVS/privateClouds/version.json new file mode 100644 index 0000000000..41f66cc990 --- /dev/null +++ b/modules/Microsoft.AVS/privateClouds/version.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", + "version": "0.1" +} diff --git a/modules/Microsoft.AVS/privateClouds/workloadNetworks/dhcpConfigurations/deploy.bicep b/modules/Microsoft.AVS/privateClouds/workloadNetworks/dhcpConfigurations/deploy.bicep new file mode 100644 index 0000000000..39ec347746 --- /dev/null +++ b/modules/Microsoft.AVS/privateClouds/workloadNetworks/dhcpConfigurations/deploy.bicep @@ -0,0 +1,75 @@ +// ============== // +// Parameters // +// ============== // + +@description('Required. NSX DHCP identifier. Generally the same as the DHCP display name') +param name string + +@description('Conditional. The name of the parent privateClouds. Required if the template is used in a standalone deployment.') +param privateCloudName string + +@description('Optional. Type of DHCP: SERVER or RELAY.') +@allowed([ + 'SERVER' + 'RELAY' +]) +param dhcpType string = '' + +@description('Optional. Display name of the DHCP entity.') +param displayName string = '' + +@description('Optional. Enable telemetry via the Customer Usage Attribution ID (GUID).') +param enableDefaultTelemetry bool = true + +@description('Optional. NSX revision number.') +param revision int = + +@description('Optional. The name of the parent workloadNetworks. Required if the template is used in a standalone deployment.') +param workloadNetworkName string = 'default' + +// =============== // +// Deployments // +// =============== // + +resource defaultTelemetry 'Microsoft.Resources/deployments@2021-04-01' = if (enableDefaultTelemetry) { + name: 'pid-47ed15a6-730a-4827-bcb4-0fd963ffbd82-${uniqueString(deployment().name)}' + properties: { + mode: 'Incremental' + template: { + '$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#' + contentVersion: '1.0.0.0' + resources: [] + } + } +} + +resource privateCloud 'Microsoft.AVS/privateClouds@2022-05-01' existing = { + name: privateCloudName + + resource workloadNetwork 'workloadNetworks@2022-05-01' existing = { + name: workloadNetworkName + } +} + +resource dhcpConfiguration 'Microsoft.AVS/privateClouds/workloadNetworks/dhcpConfigurations@2022-05-01' = { + parent: privateCloud::workloadNetwork + name: name + properties: { + dhcpType: dhcpType + displayName: displayName + revision: revision + } +} + +// =========== // +// Outputs // +// =========== // + +@description('The name of the dhcpConfiguration.') +output name string = dhcpConfiguration.name + +@description('The resource ID of the dhcpConfiguration.') +output resourceId string = dhcpConfiguration.id + +@description('The name of the resource group the dhcpConfiguration was created in.') +output resourceGroupName string = resourceGroup().name diff --git a/modules/Microsoft.AVS/privateClouds/workloadNetworks/dhcpConfigurations/version.json b/modules/Microsoft.AVS/privateClouds/workloadNetworks/dhcpConfigurations/version.json new file mode 100644 index 0000000000..41f66cc990 --- /dev/null +++ b/modules/Microsoft.AVS/privateClouds/workloadNetworks/dhcpConfigurations/version.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", + "version": "0.1" +} diff --git a/modules/Microsoft.AVS/privateClouds/workloadNetworks/dnsServices/deploy.bicep b/modules/Microsoft.AVS/privateClouds/workloadNetworks/dnsServices/deploy.bicep new file mode 100644 index 0000000000..1364785293 --- /dev/null +++ b/modules/Microsoft.AVS/privateClouds/workloadNetworks/dnsServices/deploy.bicep @@ -0,0 +1,90 @@ +// ============== // +// Parameters // +// ============== // + +@description('Required. NSX DNS Service identifier. Generally the same as the DNS Service\'s display name') +param name string + +@description('Conditional. The name of the parent privateClouds. Required if the template is used in a standalone deployment.') +param privateCloudName string + +@description('Optional. Default DNS zone of the DNS Service.') +param defaultDnsZone string = '' + +@description('Optional. Display name of the DNS Service.') +param displayName string = '' + +@description('Optional. DNS service IP of the DNS Service.') +param dnsServiceIp string = '' + +@description('Optional. Enable telemetry via the Customer Usage Attribution ID (GUID).') +param enableDefaultTelemetry bool = true + +@description('Optional. FQDN zones of the DNS Service.') +param fqdnZones array = [] + +@description('Optional. DNS Service log level.') +@allowed([ + 'DEBUG' + 'INFO' + 'WARNING' + 'ERROR' + 'FATAL' +]) +param logLevel string = '' + +@description('Optional. NSX revision number.') +param revision int = + +@description('Optional. The name of the parent workloadNetworks. Required if the template is used in a standalone deployment.') +param workloadNetworkName string = 'default' + +// =============== // +// Deployments // +// =============== // + +resource defaultTelemetry 'Microsoft.Resources/deployments@2021-04-01' = if (enableDefaultTelemetry) { + name: 'pid-47ed15a6-730a-4827-bcb4-0fd963ffbd82-${uniqueString(deployment().name)}' + properties: { + mode: 'Incremental' + template: { + '$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#' + contentVersion: '1.0.0.0' + resources: [] + } + } +} + +resource privateCloud 'Microsoft.AVS/privateClouds@2022-05-01' existing = { + name: privateCloudName + + resource workloadNetwork 'workloadNetworks@2022-05-01' existing = { + name: workloadNetworkName + } +} + +resource dnsService 'Microsoft.AVS/privateClouds/workloadNetworks/dnsServices@2022-05-01' = { + parent: privateCloud::workloadNetwork + name: name + properties: { + defaultDnsZone: defaultDnsZone + displayName: displayName + dnsServiceIp: dnsServiceIp + fqdnZones: fqdnZones + logLevel: logLevel + revision: revision + } +} + +// =========== // +// Outputs // +// =========== // + +@description('The name of the dnsService.') +output name string = dnsService.name + +@description('The resource ID of the dnsService.') +output resourceId string = dnsService.id + +@description('The name of the resource group the dnsService was created in.') +output resourceGroupName string = resourceGroup().name diff --git a/modules/Microsoft.AVS/privateClouds/workloadNetworks/dnsServices/version.json b/modules/Microsoft.AVS/privateClouds/workloadNetworks/dnsServices/version.json new file mode 100644 index 0000000000..41f66cc990 --- /dev/null +++ b/modules/Microsoft.AVS/privateClouds/workloadNetworks/dnsServices/version.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", + "version": "0.1" +} diff --git a/modules/Microsoft.AVS/privateClouds/workloadNetworks/dnsZones/deploy.bicep b/modules/Microsoft.AVS/privateClouds/workloadNetworks/dnsZones/deploy.bicep new file mode 100644 index 0000000000..6e610bb3e9 --- /dev/null +++ b/modules/Microsoft.AVS/privateClouds/workloadNetworks/dnsZones/deploy.bicep @@ -0,0 +1,83 @@ +// ============== // +// Parameters // +// ============== // + +@description('Required. NSX DNS Zone identifier. Generally the same as the DNS Zone\'s display name') +param name string + +@description('Conditional. The name of the parent privateClouds. Required if the template is used in a standalone deployment.') +param privateCloudName string + +@description('Optional. Display name of the DNS Zone.') +param displayName string = '' + +@description('Optional. DNS Server IP array of the DNS Zone.') +param dnsServerIps array = [] + +@description('Optional. Number of DNS Services using the DNS zone.') +param dnsServices int = + +@description('Optional. Domain names of the DNS Zone.') +param domain array = [] + +@description('Optional. Enable telemetry via the Customer Usage Attribution ID (GUID).') +param enableDefaultTelemetry bool = true + +@description('Optional. NSX revision number.') +param revision int = + +@description('Optional. Source IP of the DNS Zone.') +param sourceIp string = '' + +@description('Optional. The name of the parent workloadNetworks. Required if the template is used in a standalone deployment.') +param workloadNetworkName string = 'default' + +// =============== // +// Deployments // +// =============== // + +resource defaultTelemetry 'Microsoft.Resources/deployments@2021-04-01' = if (enableDefaultTelemetry) { + name: 'pid-47ed15a6-730a-4827-bcb4-0fd963ffbd82-${uniqueString(deployment().name)}' + properties: { + mode: 'Incremental' + template: { + '$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#' + contentVersion: '1.0.0.0' + resources: [] + } + } +} + +resource privateCloud 'Microsoft.AVS/privateClouds@2022-05-01' existing = { + name: privateCloudName + + resource workloadNetwork 'workloadNetworks@2022-05-01' existing = { + name: workloadNetworkName + } +} + +resource dnsZone 'Microsoft.AVS/privateClouds/workloadNetworks/dnsZones@2022-05-01' = { + parent: privateCloud::workloadNetwork + name: name + properties: { + displayName: displayName + dnsServerIps: dnsServerIps + dnsServices: dnsServices + domain: domain + revision: revision + sourceIp: sourceIp + } +} + +// =========== // +// Outputs // +// =========== // + +@description('The name of the dnsZone.') +output name string = dnsZone.name + +@description('The resource ID of the dnsZone.') +output resourceId string = dnsZone.id + +@description('The name of the resource group the dnsZone was created in.') +output resourceGroupName string = resourceGroup().name diff --git a/modules/Microsoft.AVS/privateClouds/workloadNetworks/dnsZones/version.json b/modules/Microsoft.AVS/privateClouds/workloadNetworks/dnsZones/version.json new file mode 100644 index 0000000000..41f66cc990 --- /dev/null +++ b/modules/Microsoft.AVS/privateClouds/workloadNetworks/dnsZones/version.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", + "version": "0.1" +} diff --git a/modules/Microsoft.AVS/privateClouds/workloadNetworks/portMirroringProfiles/deploy.bicep b/modules/Microsoft.AVS/privateClouds/workloadNetworks/portMirroringProfiles/deploy.bicep new file mode 100644 index 0000000000..5eb37963af --- /dev/null +++ b/modules/Microsoft.AVS/privateClouds/workloadNetworks/portMirroringProfiles/deploy.bicep @@ -0,0 +1,84 @@ +// ============== // +// Parameters // +// ============== // + +@description('Required. NSX Port Mirroring identifier. Generally the same as the Port Mirroring display name') +param name string + +@description('Conditional. The name of the parent privateClouds. Required if the template is used in a standalone deployment.') +param privateCloudName string + +@description('Optional. Destination VM Group.') +param destination string = '' + +@description('Optional. Direction of port mirroring profile.') +@allowed([ + 'INGRESS' + 'EGRESS' + 'BIDIRECTIONAL' +]) +param direction string = '' + +@description('Optional. Display name of the port mirroring profile.') +param displayName string = '' + +@description('Optional. Enable telemetry via the Customer Usage Attribution ID (GUID).') +param enableDefaultTelemetry bool = true + +@description('Optional. NSX revision number.') +param revision int = + +@description('Optional. Source VM Group.') +param source string = '' + +@description('Optional. The name of the parent workloadNetworks. Required if the template is used in a standalone deployment.') +param workloadNetworkName string = 'default' + +// =============== // +// Deployments // +// =============== // + +resource defaultTelemetry 'Microsoft.Resources/deployments@2021-04-01' = if (enableDefaultTelemetry) { + name: 'pid-47ed15a6-730a-4827-bcb4-0fd963ffbd82-${uniqueString(deployment().name)}' + properties: { + mode: 'Incremental' + template: { + '$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#' + contentVersion: '1.0.0.0' + resources: [] + } + } +} + +resource privateCloud 'Microsoft.AVS/privateClouds@2022-05-01' existing = { + name: privateCloudName + + resource workloadNetwork 'workloadNetworks@2022-05-01' existing = { + name: workloadNetworkName + } +} + +resource portMirroringProfile 'Microsoft.AVS/privateClouds/workloadNetworks/portMirroringProfiles@2022-05-01' = { + parent: privateCloud::workloadNetwork + name: name + properties: { + destination: destination + direction: direction + displayName: displayName + revision: revision + source: source + } +} + +// =========== // +// Outputs // +// =========== // + +@description('The name of the portMirroringProfile.') +output name string = portMirroringProfile.name + +@description('The resource ID of the portMirroringProfile.') +output resourceId string = portMirroringProfile.id + +@description('The name of the resource group the portMirroringProfile was created in.') +output resourceGroupName string = resourceGroup().name diff --git a/modules/Microsoft.AVS/privateClouds/workloadNetworks/portMirroringProfiles/version.json b/modules/Microsoft.AVS/privateClouds/workloadNetworks/portMirroringProfiles/version.json new file mode 100644 index 0000000000..41f66cc990 --- /dev/null +++ b/modules/Microsoft.AVS/privateClouds/workloadNetworks/portMirroringProfiles/version.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", + "version": "0.1" +} diff --git a/modules/Microsoft.AVS/privateClouds/workloadNetworks/publicIPs/deploy.bicep b/modules/Microsoft.AVS/privateClouds/workloadNetworks/publicIPs/deploy.bicep new file mode 100644 index 0000000000..f424a34ab5 --- /dev/null +++ b/modules/Microsoft.AVS/privateClouds/workloadNetworks/publicIPs/deploy.bicep @@ -0,0 +1,67 @@ +// ============== // +// Parameters // +// ============== // + +@description('Required. NSX Public IP Block identifier. Generally the same as the Public IP Block\'s display name') +param name string + +@description('Conditional. The name of the parent privateClouds. Required if the template is used in a standalone deployment.') +param privateCloudName string + +@description('Optional. Display name of the Public IP Block.') +param displayName string = '' + +@description('Optional. Enable telemetry via the Customer Usage Attribution ID (GUID).') +param enableDefaultTelemetry bool = true + +@description('Optional. Number of Public IPs requested.') +param numberOfPublicIPs int = + +@description('Optional. The name of the parent workloadNetworks. Required if the template is used in a standalone deployment.') +param workloadNetworkName string = 'default' + +// =============== // +// Deployments // +// =============== // + +resource defaultTelemetry 'Microsoft.Resources/deployments@2021-04-01' = if (enableDefaultTelemetry) { + name: 'pid-47ed15a6-730a-4827-bcb4-0fd963ffbd82-${uniqueString(deployment().name)}' + properties: { + mode: 'Incremental' + template: { + '$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#' + contentVersion: '1.0.0.0' + resources: [] + } + } +} + +resource privateCloud 'Microsoft.AVS/privateClouds@2022-05-01' existing = { + name: privateCloudName + + resource workloadNetwork 'workloadNetworks@2022-05-01' existing = { + name: workloadNetworkName + } +} + +resource publicIP 'Microsoft.AVS/privateClouds/workloadNetworks/publicIPs@2022-05-01' = { + parent: privateCloud::workloadNetwork + name: name + properties: { + displayName: displayName + numberOfPublicIPs: numberOfPublicIPs + } +} + +// =========== // +// Outputs // +// =========== // + +@description('The name of the publicIP.') +output name string = publicIP.name + +@description('The resource ID of the publicIP.') +output resourceId string = publicIP.id + +@description('The name of the resource group the publicIP was created in.') +output resourceGroupName string = resourceGroup().name diff --git a/modules/Microsoft.AVS/privateClouds/workloadNetworks/publicIPs/version.json b/modules/Microsoft.AVS/privateClouds/workloadNetworks/publicIPs/version.json new file mode 100644 index 0000000000..41f66cc990 --- /dev/null +++ b/modules/Microsoft.AVS/privateClouds/workloadNetworks/publicIPs/version.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", + "version": "0.1" +} diff --git a/modules/Microsoft.AVS/privateClouds/workloadNetworks/segments/deploy.bicep b/modules/Microsoft.AVS/privateClouds/workloadNetworks/segments/deploy.bicep new file mode 100644 index 0000000000..ed80042d91 --- /dev/null +++ b/modules/Microsoft.AVS/privateClouds/workloadNetworks/segments/deploy.bicep @@ -0,0 +1,75 @@ +// ============== // +// Parameters // +// ============== // + +@description('Required. NSX Segment identifier. Generally the same as the Segment\'s display name') +param name string + +@description('Conditional. The name of the parent privateClouds. Required if the template is used in a standalone deployment.') +param privateCloudName string + +@description('Optional. Gateway which to connect segment to.') +param connectedGateway string = '' + +@description('Optional. Display name of the segment.') +param displayName string = '' + +@description('Optional. Enable telemetry via the Customer Usage Attribution ID (GUID).') +param enableDefaultTelemetry bool = true + +@description('Optional. NSX revision number.') +param revision int = + +@description('Optional. Subnet configuration for segment') +param subnet object = {} + +@description('Optional. The name of the parent workloadNetworks. Required if the template is used in a standalone deployment.') +param workloadNetworkName string = 'default' + +// =============== // +// Deployments // +// =============== // + +resource defaultTelemetry 'Microsoft.Resources/deployments@2021-04-01' = if (enableDefaultTelemetry) { + name: 'pid-47ed15a6-730a-4827-bcb4-0fd963ffbd82-${uniqueString(deployment().name)}' + properties: { + mode: 'Incremental' + template: { + '$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#' + contentVersion: '1.0.0.0' + resources: [] + } + } +} + +resource privateCloud 'Microsoft.AVS/privateClouds@2022-05-01' existing = { + name: privateCloudName + + resource workloadNetwork 'workloadNetworks@2022-05-01' existing = { + name: workloadNetworkName + } +} + +resource segment 'Microsoft.AVS/privateClouds/workloadNetworks/segments@2022-05-01' = { + parent: privateCloud::workloadNetwork + name: name + properties: { + connectedGateway: connectedGateway + displayName: displayName + revision: revision + subnet: subnet + } +} + +// =========== // +// Outputs // +// =========== // + +@description('The name of the segment.') +output name string = segment.name + +@description('The resource ID of the segment.') +output resourceId string = segment.id + +@description('The name of the resource group the segment was created in.') +output resourceGroupName string = resourceGroup().name diff --git a/modules/Microsoft.AVS/privateClouds/workloadNetworks/segments/version.json b/modules/Microsoft.AVS/privateClouds/workloadNetworks/segments/version.json new file mode 100644 index 0000000000..41f66cc990 --- /dev/null +++ b/modules/Microsoft.AVS/privateClouds/workloadNetworks/segments/version.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", + "version": "0.1" +} diff --git a/modules/Microsoft.AVS/privateClouds/workloadNetworks/vmGroups/deploy.bicep b/modules/Microsoft.AVS/privateClouds/workloadNetworks/vmGroups/deploy.bicep new file mode 100644 index 0000000000..7efe5c6a16 --- /dev/null +++ b/modules/Microsoft.AVS/privateClouds/workloadNetworks/vmGroups/deploy.bicep @@ -0,0 +1,71 @@ +// ============== // +// Parameters // +// ============== // + +@description('Required. NSX VM Group identifier. Generally the same as the VM Group\'s display name') +param name string + +@description('Conditional. The name of the parent privateClouds. Required if the template is used in a standalone deployment.') +param privateCloudName string + +@description('Optional. Display name of the VM group.') +param displayName string = '' + +@description('Optional. Enable telemetry via the Customer Usage Attribution ID (GUID).') +param enableDefaultTelemetry bool = true + +@description('Optional. Virtual machine members of this group.') +param members array = [] + +@description('Optional. NSX revision number.') +param revision int = + +@description('Optional. The name of the parent workloadNetworks. Required if the template is used in a standalone deployment.') +param workloadNetworkName string = 'default' + +// =============== // +// Deployments // +// =============== // + +resource defaultTelemetry 'Microsoft.Resources/deployments@2021-04-01' = if (enableDefaultTelemetry) { + name: 'pid-47ed15a6-730a-4827-bcb4-0fd963ffbd82-${uniqueString(deployment().name)}' + properties: { + mode: 'Incremental' + template: { + '$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#' + contentVersion: '1.0.0.0' + resources: [] + } + } +} + +resource privateCloud 'Microsoft.AVS/privateClouds@2022-05-01' existing = { + name: privateCloudName + + resource workloadNetwork 'workloadNetworks@2022-05-01' existing = { + name: workloadNetworkName + } +} + +resource vmGroup 'Microsoft.AVS/privateClouds/workloadNetworks/vmGroups@2022-05-01' = { + parent: privateCloud::workloadNetwork + name: name + properties: { + displayName: displayName + members: members + revision: revision + } +} + +// =========== // +// Outputs // +// =========== // + +@description('The name of the vmGroup.') +output name string = vmGroup.name + +@description('The resource ID of the vmGroup.') +output resourceId string = vmGroup.id + +@description('The name of the resource group the vmGroup was created in.') +output resourceGroupName string = resourceGroup().name diff --git a/modules/Microsoft.AVS/privateClouds/workloadNetworks/vmGroups/version.json b/modules/Microsoft.AVS/privateClouds/workloadNetworks/vmGroups/version.json new file mode 100644 index 0000000000..41f66cc990 --- /dev/null +++ b/modules/Microsoft.AVS/privateClouds/workloadNetworks/vmGroups/version.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", + "version": "0.1" +} diff --git a/utilities/pipelines/sharedScripts/Get-ModuleTestFileList.ps1 b/utilities/pipelines/sharedScripts/Get-ModuleTestFileList.ps1 index e0cbac542b..55afc9f33e 100644 --- a/utilities/pipelines/sharedScripts/Get-ModuleTestFileList.ps1 +++ b/utilities/pipelines/sharedScripts/Get-ModuleTestFileList.ps1 @@ -47,7 +47,8 @@ function Get-ModuleTestFileList { } if (-not $deploymentTests) { - throw "No deployment test files found for module [$ModulePath]" + Write-Warning "No deployment test files found for module [$ModulePath]" + return @() } $deploymentTests = $deploymentTests | ForEach-Object { diff --git a/utilities/tools/REST2CARML/ModuleConfig.psd1 b/utilities/tools/REST2CARML/ModuleConfig.psd1 new file mode 100644 index 0000000000..5ce1d2178c --- /dev/null +++ b/utilities/tools/REST2CARML/ModuleConfig.psd1 @@ -0,0 +1,13 @@ +@{ + #region general + #endregion + + #region specs + url_CloneRESTAPISpecRepository = 'https://github.com/Azure/azure-rest-api-specs.git' + #endregion + + #region monitor docs + url_MonitoringDocsRepositoryMetricsRaw = 'https://raw.githubusercontent.com/MicrosoftDocs/azure-docs/main/articles/azure-monitor/essentials/metrics-supported.md' + url_MonitoringDocsRepositoryLogsRaw = 'https://raw.githubusercontent.com/MicrosoftDocs/azure-docs/main/articles/azure-monitor/essentials/resource-logs-categories.md' + #endregion +} diff --git a/utilities/tools/REST2CARML/REST2CARML.psd1 b/utilities/tools/REST2CARML/REST2CARML.psd1 new file mode 100644 index 0000000000..84e52635d7 --- /dev/null +++ b/utilities/tools/REST2CARML/REST2CARML.psd1 @@ -0,0 +1,133 @@ +# +# Module manifest for module 'AzHdo' +# +# Generated by: ServicesCode +# +# Generated on: 11/12/2020 +# + +@{ + + # Script module or binary module file associated with this manifest. + RootModule = 'REST2CARML.psm1' + + # Version number of this module. + ModuleVersion = '0.1.0' + + # Supported PSEditions + # CompatiblePSEditions = @() + + # ID used to uniquely identify this module + GUID = '807456f1-947d-43af-9b4d-49d13f593130' + + # Author of this module + Author = 'CARML' + + # Company or vendor of this module + CompanyName = 'Microsoft' + + # Copyright statement for this module + Copyright = '(c) ServicesCode. All rights reserved.' + + # Description of the functionality provided by this module + Description = 'Automatically generate new CARML modules using the Azure API & docs.' + + # Minimum version of the PowerShell engine required by this module + # PowerShellVersion = '' + + # Name of the PowerShell host required by this module + # PowerShellHostName = '' + + # Minimum version of the PowerShell host required by this module + # PowerShellHostVersion = '' + + # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. + # DotNetFrameworkVersion = '' + + # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. + # ClrVersion = '' + + # Processor architecture (None, X86, Amd64) required by this module + # ProcessorArchitecture = '' + + # Modules that must be imported into the global environment prior to importing this module + RequiredModules = @( + 'AzureAPICrawler' + ) + + # Assemblies that must be loaded prior to importing this module + # RequiredAssemblies = @() + + # Script files (.ps1) that are run in the caller's environment prior to importing this module. + # ScriptsToProcess = @() + + # Type files (.ps1xml) to be loaded when importing this module + # TypesToProcess = @() + + # Format files (.ps1xml) to be loaded when importing this module + # FormatsToProcess = @() + + # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess + # NestedModules = @() + + # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. + FunctionsToExport = @() + + # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. + CmdletsToExport = @() + + # Variables to export from this module + VariablesToExport = '*' + + # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. + AliasesToExport = @() + + # DSC resources to export from this module + # DscResourcesToExport = @() + + # List of all modules packaged with this module + # ModuleList = @() + + # List of all files packaged with this module + # FileList = @() + + # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. + PrivateData = @{ + + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @('CARML') + + # A URL to the license for this module. + # LicenseUri = '' + + # A URL to the main website for this project. + ProjectUri = 'https://aka.ms/CARML' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + # ReleaseNotes = '' + + # Prerelease string of this module + # Prerelease = '' + + # Flag to indicate whether the module requires explicit user acceptance for install/update/save + # RequireLicenseAcceptance = $false + + # External dependent modules of this module + # ExternalModuleDependencies = @() + + } # End of PSData hashtable + + } # End of PrivateData hashtable + + # HelpInfo URI of this module + # HelpInfoURI = '' + + # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. + DefaultCommandPrefix = 'Carml' + +} diff --git a/utilities/tools/REST2CARML/REST2CARML.psm1 b/utilities/tools/REST2CARML/REST2CARML.psm1 new file mode 100644 index 0000000000..ccdc3288bb --- /dev/null +++ b/utilities/tools/REST2CARML/REST2CARML.psm1 @@ -0,0 +1,28 @@ +[cmdletbinding()] +param() + +# Load central config file; Config File can be referenced within module scope by $script:CONFIG +Write-Verbose 'Load Config' +$moduleConfigPath = Join-Path $PSScriptRoot 'ModuleConfig.psd1' +$script:CONFIG = Import-PowerShellDataFile -Path (Resolve-Path ($moduleConfigPath)) + +$script:repoRoot = (Get-Item $PSScriptRoot).Parent.Parent.Parent +$script:moduleRoot = $PSScriptRoot +$script:src = Join-Path $PSScriptRoot 'src' +$script:temp = Join-Path $PSScriptRoot 'temp' + +Write-Verbose 'Import everything in sub folders public & private' +$functionFolders = @('public', 'private') +foreach ($folder in $functionFolders) { + $folderPath = Join-Path -Path $PSScriptRoot -ChildPath $folder + If (Test-Path -Path $folderPath) { + Write-Verbose "Importing from $folder" + $functions = Get-ChildItem -Path $folderPath -Filter '*.ps1' -Recurse + foreach ($function in $functions) { + Write-Verbose (' Importing [{0}]' -f $function.BaseName) + . $function.FullName + } + } +} +$publicFunctions = (Get-ChildItem -Path "$PSScriptRoot\public" -Filter '*.ps1').BaseName +Export-ModuleMember -Function $publicFunctions diff --git a/utilities/tools/REST2CARML/private/extension/Set-DiagnosticModuleData.ps1 b/utilities/tools/REST2CARML/private/extension/Set-DiagnosticModuleData.ps1 new file mode 100644 index 0000000000..d5fe88e841 --- /dev/null +++ b/utilities/tools/REST2CARML/private/extension/Set-DiagnosticModuleData.ps1 @@ -0,0 +1,196 @@ +<# +.SYNOPSIS +Populate the provided ModuleData with all parameters, variables & resources required for diagnostic settings. + +.DESCRIPTION +Populate the provided ModuleData with all parameters, variables & resources required for diagnostic settings. + +.PARAMETER ResourceType +Mandatory. The ResourceType to fetch the available diagnostic options for. + +.PARAMETER ModuleData +Mandatory. The ModuleData object to populate. + +.EXAMPLE +Set-DiagnosticModuleData -ResourceType 'vaults' -ModuleData @{ parameters = @(...); resources = @(...); (...) } + +Add the diagnostic module data of the resource type [Microsoft.KeyVault/vaults] to the provided module data object +#> +function Set-DiagnosticModuleData { + + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string] $ResourceType, + + [Parameter(Mandatory = $false)] + [string[]] $DiagnosticMetricsOptions = @(), + + [Parameter(Mandatory = $false)] + [string[]] $DiagnosticLogsOptions = @(), + + [Parameter(Mandatory = $true)] + [Hashtable] $ModuleData + ) + + begin { + Write-Debug ('{0} entered' -f $MyInvocation.MyCommand) + } + + process { + $resourceTypeSingular = ((Get-ResourceTypeSingularName -ResourceType $resourceType) -split '/')[-1] + + # Type check (in case PowerShell auto-converted the array to a hashtable) + if ($ModuleData.additionalParameters -is [hashtable]) { + $ModuleData.additionalParameters = @($ModuleData.additionalParameters) + } + if ($ModuleData.variables -is [hashtable]) { + $ModuleData.variables = @($ModuleData.variables) + } + if ($ModuleData.resources -is [hashtable]) { + $ModuleData.resources = @($ModuleData.resources) + } + + $ModuleData.additionalParameters += @( + @{ + name = 'diagnosticLogsRetentionInDays' + type = 'integer' + description = 'Specifies the number of days that logs will be kept for; a value of 0 will retain data indefinitely.' + required = $false + default = 365 + minimum = 0 + maximum = 365 + } + @{ + name = 'diagnosticStorageAccountId' + type = 'string' + description = '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.' + required = $false + default = '' + } + @{ + name = 'diagnosticWorkspaceId' + type = 'string' + description = '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.' + required = $false + default = '' + } + @{ + name = 'diagnosticEventHubAuthorizationRuleId' + type = 'string' + description = '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.' + required = $false + default = '' + } + @{ + name = 'diagnosticEventHubName' + type = 'string' + description = '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.' + required = $false + default = '' + } + ) + + $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) { + + # TODO: Clarify: Might need to be always 'All metrics' if any metric exists + $ModuleData.additionalParameters += @( + @{ + name = 'diagnosticMetricsToEnable' + type = 'array' + description = 'The name of metrics that will be streamed.' + required = $false + allowedValues = @( + 'AllMetrics' + ) + default = @( + 'AllMetrics' + ) + } + ) + $ModuleData.variables += @{ + name = 'diagnosticsMetrics' + content = @( + 'var diagnosticsMetrics = [for metric in diagnosticMetricsToEnable: {' + ' category: metric' + ' timeGrain: null' + ' enabled: true' + ' retentionPolicy: {' + ' enabled: true' + ' days: diagnosticLogsRetentionInDays' + ' }' + '}]' + ) + } + + $diagnosticResource.content += ' metrics: diagnosticsMetrics' + } + + # Log-specific + if ($DiagnosticLogsOptions) { + $ModuleData.additionalParameters += @( + @{ + name = 'diagnosticLogCategoriesToEnable' + type = 'array' + description = 'The name of logs that will be streamed.' + required = $false + allowedValues = $DiagnosticLogsOptions + default = $DiagnosticLogsOptions + } + ) + $ModuleData.variables += @{ + name = 'diagnosticsLogs' + content = @( + 'var diagnosticsLogs = [for category in diagnosticLogCategoriesToEnable: {' + ' category: category' + ' enabled: true' + ' retentionPolicy: {' + ' enabled: true' + ' days: diagnosticLogsRetentionInDays' + ' }' + '}]' + ) + } + + $diagnosticResource.content += ' logs: diagnosticsLogs' + } + + $diagnosticResource.content += @( + ' }' + " scope: $resourceTypeSingular" + '}' + '' + ) + + $ModuleData.resources += $diagnosticResource + + # Other variables + $ModuleData.additionalParameters += @( + @{ + name = 'diagnosticSettingsName' + type = 'string' + description = 'The name of the diagnostic setting, if deployed.' + required = $false + default = '${name}-diagnosticSettings' + } + ) + } + + end { + Write-Debug ('{0} exited' -f $MyInvocation.MyCommand) + } +} diff --git a/utilities/tools/REST2CARML/private/extension/Set-LockModuleData.ps1 b/utilities/tools/REST2CARML/private/extension/Set-LockModuleData.ps1 new file mode 100644 index 0000000000..58245ae48e --- /dev/null +++ b/utilities/tools/REST2CARML/private/extension/Set-LockModuleData.ps1 @@ -0,0 +1,81 @@ +<# +.SYNOPSIS +Populate the provided ModuleData with all parameters, variables & resources required for locks. + +.DESCRIPTION +Populate the provided ModuleData with all parameters, variables & resources required for locks. + +.PARAMETER ResourceType +Mandatory. The resource type to check if lock are supported. + +.PARAMETER ModuleData +Mandatory. The ModuleData object to populate. + +.EXAMPLE +Set-LockModuleData -ResourceType 'vaults' -ModuleData @{ parameters = @(...); resources = @(...); (...) } + +Add the lock module data of the resource type [vaults] to the provided module data object +#> +function Set-LockModuleData { + + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string] $ResourceType, + + [Parameter(Mandatory = $true)] + [Hashtable] $ModuleData + ) + + begin { + Write-Debug ('{0} entered' -f $MyInvocation.MyCommand) + } + + process { + + $resourceTypeSingular = ((Get-ResourceTypeSingularName -ResourceType $resourceType) -split '/')[-1] + + # Type check (in case PowerShell auto-converted the array to a hashtable) + if ($ModuleData.additionalFiles -is [hashtable]) { + $ModuleData.additionalFiles = @($ModuleData.additionalFiles) + } + if ($ModuleData.resources -is [hashtable]) { + $ModuleData.resources = @($ModuleData.resources) + } + + $ModuleData.additionalParameters += @( + @{ + name = 'lock' + type = 'string' + description = 'Specify the type of lock.' + required = $false + default = '' + allowedValues = @( + '' + 'CanNotDelete' + 'ReadOnly' + ) + } + ) + + $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 { + Write-Debug ('{0} exited' -f $MyInvocation.MyCommand) + } +} + diff --git a/utilities/tools/REST2CARML/private/extension/Set-PrivateEndpointModuleData.ps1 b/utilities/tools/REST2CARML/private/extension/Set-PrivateEndpointModuleData.ps1 new file mode 100644 index 0000000000..8f2f2bd61f --- /dev/null +++ b/utilities/tools/REST2CARML/private/extension/Set-PrivateEndpointModuleData.ps1 @@ -0,0 +1,88 @@ +<# +.SYNOPSIS +Populate the provided ModuleData with all parameters, variables & resources required for private endpoints. + +.DESCRIPTION +Populate the provided ModuleData with all parameters, variables & resources required for private endpoints. + +.PARAMETER ResourceType +Mandatory. The resource type to check if private endpoints are supported. + +.PARAMETER ModuleData +Mandatory. The ModuleData object to populate. + +.EXAMPLE +Set-PrivateEndpointModuleData -ResourceType 'vaults' -ModuleData @{ parameters = @(...); resources = @(...); (...) } + +Add the private endpoint module data of the resource type [vaults] to the provided module data object +#> +function Set-PrivateEndpointModuleData { + + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string] $ResourceType, + + [Parameter(Mandatory = $true)] + [Hashtable] $ModuleData + ) + + begin { + Write-Debug ('{0} entered' -f $MyInvocation.MyCommand) + } + + process { + + $resourceTypeSingular = ((Get-ResourceTypeSingularName -ResourceType $resourceType) -split '/')[-1] + + # Type check (in case PowerShell auto-converted the array to a hashtable) + if ($ModuleData.additionalParameters -is [hashtable]) { + $ModuleData.additionalParameters = @($ModuleData.additionalParameters) + } + if ($ModuleData.modules -is [hashtable]) { + $ModuleData.modules = @($ModuleData.modules) + } + + # Built result + $ModuleData.additionalParameters += @( + @{ + name = 'privateEndpoints' + type = 'array' + description = 'Configuration details for private endpoints. For security reasons, it is recommended to use private endpoints whenever possible.' + required = $false + default = @() + } + ) + + $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 { + Write-Debug ('{0} exited' -f $MyInvocation.MyCommand) + } +} + diff --git a/utilities/tools/REST2CARML/private/extension/Set-RoleAssignmentsModuleData.ps1 b/utilities/tools/REST2CARML/private/extension/Set-RoleAssignmentsModuleData.ps1 new file mode 100644 index 0000000000..bc2d650a47 --- /dev/null +++ b/utilities/tools/REST2CARML/private/extension/Set-RoleAssignmentsModuleData.ps1 @@ -0,0 +1,142 @@ +<# +.SYNOPSIS +Fetch all available roles for a given resource type, store the content of the the corresponding nested_roleAssignment.bicep file in the given 'additionalFiles' array of the provided module data object & add any additional required parameters, variables & resources to the provide module data object. + +.DESCRIPTION +Fetch all available roles for a given resource type, store the content of the the corresponding nested_roleAssignment.bicep file in the given 'additionalFiles' array of the provided module data object & add any additional required parameters, variables & resources to the provide module data object. + +.PARAMETER ProviderNamespace +Mandatory. The ProviderNamespace to fetch the available role options for. + +.PARAMETER ResourceType +Mandatory. The ResourceType to fetch the available role options for. + +.PARAMETER ServiceApiVersion +Mandatory. The API version of the module to generate the RBAC module file for + +.PARAMETER ModuleData +Mandatory. The ModuleData object to populate. + +.EXAMPLE +Set-RoleAssignmentsModuleData -ProviderNamespace 'Microsoft.KeyVault' -ResourceType 'vaults' -ServiceApiVersion '10-10-2022' -ModuleData @{ parameters = @(...); resources = @(...); (...) } + +Generate the nested_roleAssignment.bicep file in the [Microsoft.KeyVault/vaults]'s module path and add any additional required data to the provided module data object. +#> +function Set-RoleAssignmentsModuleData { + + [CmdletBinding(SupportsShouldProcess)] + param ( + [Parameter(Mandatory = $true)] + [string] $ProviderNamespace, + + [Parameter(Mandatory = $true)] + [object[]] $RelevantRoles = @(), + + [Parameter(Mandatory = $true)] + [string] $ResourceType, + + [Parameter(Mandatory = $true)] + [string] $ServiceApiVersion, + + [Parameter(Mandatory = $true)] + [Hashtable] $ModuleData + ) + + begin { + Write-Debug ('{0} entered' -f $MyInvocation.MyCommand) + } + + process { + + $resourceTypeSingular = ((Get-ResourceTypeSingularName -ResourceType $resourceType) -split '/')[-1] + + # Type check (in case PowerShell auto-converted the array to a hashtable) + if ($ModuleData.modules -is [hashtable]) { + $ModuleData.modules = @($ModuleData.modules) + } + if ($ModuleData.additionalFiles -is [hashtable]) { + $ModuleData.additionalFiles = @($ModuleData.additionalFiles) + } + if ($ModuleData.additionalParameters -is [hashtable]) { + $ModuleData.additionalParameters = @($ModuleData.additionalParameters) + } + + # Tokens to replace in files + $tokens = @{ + providerNamespace = $ProviderNamespace + resourceType = $ResourceType + resourceTypeSingular = $resourceTypeSingular + apiVersion = $ServiceApiVersion + } + + # Format roles + if ($RelevantRoles.count -eq 0) { + return + } else { + $roleAssignmentList = [System.Collections.ArrayList]@() + foreach ($role in $RelevantRoles | Sort-Object -Property 'Name' -Unique) { + $roleAssignmentList += "{0}: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '{1}')" -f ($role.Name -match '\s+' ? ("'{0}'" -f $role.Name) : $role.Name), $role.Id + } + } + + $ModuleData.additionalParameters += @( + @{ + name = 'roleAssignments' + type = 'array' + description = "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\'." + required = $false + default = @() + } + ) + + $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 + + # Replace general tokens + $fileContent = Set-TokenValuesInArray -Content $rawContent -Tokens $tokens + + # Add roles + ## Split content into pre-roles & post-roles content + $preRolesContent = ($fileContent -split '<>')[0].Trim() -split '\n' | ForEach-Object { $_.TrimEnd() } + $postRolesContent = ($fileContent -split '<>')[1].Trim() -split '\n' | ForEach-Object { $_.TrimEnd() } + ## Add roles + $fileContent = $preRolesContent.TrimEnd() + ($roleAssignmentList | ForEach-Object { " $_" }) + $postRolesContent + + # Set content + $roleTemplateFilePath = Join-Path '.bicep' 'nested_roleAssignments.bicep' + + if ($PSCmdlet.ShouldProcess("RBAC data for file in path [$roleTemplateFilePath]", 'Set')) { + $ModuleData.additionalFiles += @{ + type = 'roleAssignments' + relativeFilePath = $roleTemplateFilePath + fileContent = ($fileContent | Out-String).Trim() + onlyRoleDefinitionIds = $roleAssignmentList.onlyRoleDefinitionIds + onlyRoleDefinitionNames = $roleAssignmentList.onlyRoleDefinitionNames + } + } + } + + end { + Write-Debug ('{0} exited' -f $MyInvocation.MyCommand) + } +} + diff --git a/utilities/tools/REST2CARML/private/module/Expand-DeploymentBlock.ps1 b/utilities/tools/REST2CARML/private/module/Expand-DeploymentBlock.ps1 new file mode 100644 index 0000000000..9733b356ee --- /dev/null +++ b/utilities/tools/REST2CARML/private/module/Expand-DeploymentBlock.ps1 @@ -0,0 +1,158 @@ +<# +.SYNOPSIS +Expand the given module/resource block content with details about contained params/properties + +.DESCRIPTION +Expand the given module/resource block content with details about contained params/properties + +.PARAMETER DeclarationBlock +Mandatory. The declaration block to expand upon. If available, the object will get the 2 new properties 'topLevelElements' & 'nestedElements' + +.PARAMETER NestedType +Mandatory. The key word that indicates nested-elements. Can be 'params' or 'properties' + +.EXAMPLE +Expand-DeploymentBlock -DeclarationBlock @{ startIndex = 173; endIndex = 183; content = @( 'resource storageAccount 'Microsoft.Storage/storageAccounts@2021-09-01' = {', ' name: name', ' location: location', (..)) } -NestedType 'properties' +#> +function Expand-DeploymentBlock { + + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [hashtable] $DeclarationBlock, + + [Parameter(Mandatory = $true)] + [ValidateSet('properties', 'params')] + [string] $NestedType + ) + + $topLevelIndent = Get-LineIndentation -Line $DeclarationBlock.content[1] + $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 ## + #################################### + $topLevelElements = @() + foreach ($topLevelElementName in $topLevelElementNames) { + + # Find start index of element + $relativeElementStartIndex = 1 + for ($index = $relativeElementStartIndex; $index -lt $DeclarationBlock.content.Count; $index++) { + if ($DeclarationBlock.content[$index] -match ("^\s{$($topLevelIndent)}$($topLevelElementName):.+$" )) { + $relativeElementStartIndex = $index + break + } + } + + # Find end index of element + $isPropertyOrClosing = "^\s{$($topLevelIndent)}\w+:.+$|^}$" + if ($DeclarationBlock.content[$index + 1] -notmatch $isPropertyOrClosing) { + # If the next line is not another element/property/param, it's a multi-line declaration + $relativeElementEndIndex = $relativeElementStartIndex + while ($DeclarationBlock.content[($relativeElementEndIndex + 1)] -notmatch $isPropertyOrClosing) { + $relativeElementEndIndex++ + } + } else { + $relativeElementEndIndex = $relativeElementStartIndex + } + + # Build result + $topLevelElements += @{ + name = $topLevelElementName + content = $DeclarationBlock.content[$relativeElementStartIndex..$relativeElementEndIndex] + } + } + + $DeclarationBlock['topLevelElements'] = $topLevelElements + + ################################# + ## Collect nested elements ## + ################################# + if (($DeclarationBlock.content | Where-Object { $_ -match "^\s*$($NestedType): \{\s*$" }).count -gt 0) { + + # Find start index of nested block + # -------------------------------- + $propertiesStartIndex = 1 + for ($index = $propertiesStartIndex; $index -lt $DeclarationBlock.content.Count; $index++) { + if ($DeclarationBlock.Content[$index] -match "^\s*$($NestedType): \{\s*$") { + $propertiesStartIndex = $index + break + } + } + + # Find end index of nested block + # ------------------------------ + $propertiesEndIndex = $propertiesStartIndex + for ($index = $propertiesEndIndex; $index -lt $DeclarationBlock.content.Count; $index++) { + if ((Get-LineIndentation -Line $DeclarationBlock.Content[$index]) -eq $topLevelIndent -and $DeclarationBlock.Content[$index].Trim() -eq '}') { + $propertiesEndIndex = $index + break + } + } + + # Process nested block + # -------------------- + if ($DeclarationBlock.content[$propertiesStartIndex] -like '*{*}*' -or $DeclarationBlock.content[$propertiesStartIndex + 1].Trim() -eq '}') { + # Empty properties block. Can be skipped. + $DeclarationBlock['nestedElements'] = @() + } else { + $nestedIndent = Get-LineIndentation -Line $DeclarationBlock.content[($propertiesStartIndex + 1)] + $relevantNestedProperties = $DeclarationBlock.content[($propertiesStartIndex + 1) .. ($propertiesEndIndex - 1)] | Where-Object { (Get-LineIndentation $_) -eq $nestedIndent -and $_ -match '^\s*\w+:.*' } + $nestedPropertyNames = $relevantNestedProperties | ForEach-Object { ($_ -split ':')[0].Trim() } + + # Collect full data block + $nestedElements = @() + foreach ($nestedPropertyName in $nestedPropertyNames) { + + # Find start index of poperty + $relativeElementStartIndex = 1 + for ($index = $relativeElementStartIndex; $index -lt $DeclarationBlock.content.Count; $index++) { + if ($DeclarationBlock.content[$index] -match ("^\s{$($nestedIndent)}$($nestedPropertyName):.+$" )) { + $relativeElementStartIndex = $index + break + } + } + + # Find end index of poperty + $isPropertyOrClosing = "^\s{$($nestedIndent)}\w+:.+$|^\s{$($topLevelIndent)}}$" + if ($DeclarationBlock.content[$index + 1] -notmatch $isPropertyOrClosing) { + # If the next line is not another element/property/param, it's a multi-line declaration + $relativeElementEndIndex = $relativeElementStartIndex + while ($DeclarationBlock.content[($relativeElementEndIndex + 1)] -notmatch $isPropertyOrClosing) { + $relativeElementEndIndex++ + } + } else { + $relativeElementEndIndex = $relativeElementStartIndex + } + + # Build result + $nestedElements += @{ + name = $nestedPropertyName + content = $DeclarationBlock.content[$relativeElementStartIndex..$relativeElementEndIndex] + } + } + + $DeclarationBlock['nestedElements'] = $nestedElements + } + } +} diff --git a/utilities/tools/REST2CARML/private/module/Get-FormattedModuleParameter.ps1 b/utilities/tools/REST2CARML/private/module/Get-FormattedModuleParameter.ps1 new file mode 100644 index 0000000000..5e37188b7d --- /dev/null +++ b/utilities/tools/REST2CARML/private/module/Get-FormattedModuleParameter.ps1 @@ -0,0 +1,173 @@ +<# +.SYNOPSIS +Convert the given Parameter Data object into a formatted Bicep parameter block. + +.DESCRIPTION +Convert the given Parameter Data object into a formatted Bicep parameter block. The result is returned as an array of parameter block lines. + +.PARAMETER ParameterData +Mandatory. The Parameter Data to convert. + +.EXAMPLE +Get-FormattedModuleParameter -ParameterData @{ name = 'myParam'; type = 'string'; (...) } + +Convert the given 'myParam' parameter into the Bicep format. +#> +function Get-FormattedModuleParameter { + + param ( + [Parameter(Mandatory = $true)] + [object] $ParameterData + ) + + $result = @() + + # 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+\. .+' ) { + # A keyword like 'Conditional' was already specified + $result += "@description('{0}')" -f ($ParameterData.description -replace "(? +function Get-LinkedChildModuleList { + + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string] $FullResourceType, + + [Parameter(Mandatory = $true)] + [hashtable] $FullModuleData + ) + + begin { + Write-Debug ('{0} entered' -f $MyInvocation.MyCommand) + } + + process { + $linkedChildren = @{} + + # Collect child-resource information + $fullmoduleData.Keys | Where-Object { + # Is nested + $_ -like "$FullResourceType/*" -and + # Is direct child + (($_ -split '/').Count -eq (($FullResourceType -split '/').Count + 1) + ) + } | ForEach-Object { $linkedChildren[$_] = $fullmoduleData[$_] } + + ## 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? + $FullModuleData.Keys | Where-Object { + # Is nested + $_ -like "$FullResourceType/*" -and + # Is indirect child + (($_ -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($fullModuleData[$_].metadata.parentUrlPath) + } | ForEach-Object { $linkedChildren[$_] = $fullmoduleData[$_] } + + return $linkedChildren + } + + end { + Write-Debug ('{0} exited' -f $MyInvocation.MyCommand) + } +} diff --git a/utilities/tools/REST2CARML/private/module/Get-ParentResourceTypeList.ps1 b/utilities/tools/REST2CARML/private/module/Get-ParentResourceTypeList.ps1 new file mode 100644 index 0000000000..55b36c6d9e --- /dev/null +++ b/utilities/tools/REST2CARML/private/module/Get-ParentResourceTypeList.ps1 @@ -0,0 +1,34 @@ +<# +.SYNOPSIS +Get the resource type's parents + +.DESCRIPTION +Get the resource type's parents + +.PARAMETER ResourceType +Mandatory. The resource type to get all parent paths of + +.EXAMPLE +Get-ParentResourceTypeList -ResourceType 'Microsoft.Storage/storageAccounts/blobServices/containers' + +Get the parent resource paths for [Microsoft.Storage/storageAccounts/blobServices/containers]. Would return +- Microsoft.Storage/storageAccounts/blobServices/containers +- Microsoft.Storage/storageAccounts/blobServices +- Microsoft.Storage/storageAccounts +#> +function Get-ParentResourceTypeList { + + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string] $ResourceType + ) + + $res = @( + $ResourceType + ) + if (($ResourceType -split '/').Count -gt 2) { + $res += Get-ParentResourceTypeList -ResourceType ((Split-Path $ResourceType -Parent) -replace '\\', '/') + } + return $res +} 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..2a30e80091 --- /dev/null +++ b/utilities/tools/REST2CARML/private/module/Get-TemplateChildModuleContent.ps1 @@ -0,0 +1,324 @@ +<# +.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)] + [hashtable] $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 ($childIdentifier in ($linkedChildren.Keys | Sort-Object)) { + $dataBlock = $LinkedChildren[$childIdentifier] + $childResourceType = ($childIdentifier -split '/')[-1] + + $hasProxyParent = [String]::IsNullOrEmpty($dataBlock.metadata.parentUrlPath) + if ($hasProxyParent) { + $proxyParentName = Split-Path (Split-Path $childIdentifier -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..3e091e6f03 --- /dev/null +++ b/utilities/tools/REST2CARML/private/module/Get-TemplateDeploymentsContent.ps1 @@ -0,0 +1,336 @@ +<# +.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)] + [hashtable] $FullModuleData, + + [Parameter(Mandatory = $false)] + [array] $ParentResourceTypes = @(), + + [Parameter(Mandatory = $false)] + [array] $ExistingTemplateContent = @(), + + [Parameter(Mandatory = $false)] + [hashtable] $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 + + # Add a space in between the new section and the previous one in case no space exists + if (-not [String]::IsNullOrEmpty($templateContent[-1])) { + $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[$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)}" + } + + # Add a space in between the new section and the previous one in case no space exists + if (-not [String]::IsNullOrEmpty($templateContent[-1])) { + $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 + } + } + + if (($ModuleData.parameters | Where-Object { $_.level -eq 1 -and $_.Parent -eq 'properties' }).Count -gt 0) { + $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 += ' }' + } + + $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.Keys.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..40295f460b --- /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.parameters.name -contains 'location' -and $ModuleData.parameters['location'].defaultValue -ne 'global') { + $defaultOutputs += @{ + name = 'location' + type = 'string' + content = @( + "@description('The location the resource was deployed into.')" + 'output location string = {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..7487d6a80a --- /dev/null +++ b/utilities/tools/REST2CARML/private/module/Get-TemplateParametersContent.ps1 @@ -0,0 +1,252 @@ +<# +.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)] + [hashtable] $FullModuleData, + + [Parameter(Mandatory = $false)] + [array] $ParentResourceTypes = @(), + + [Parameter(Mandatory = $false)] + [array] $ExistingTemplateContent = @(), + + [Parameter(Mandatory = $false)] + [hashtable] $LinkedChildren = @{} + ) + + begin { + Write-Debug ('{0} entered' -f $MyInvocation.MyCommand) + } + + process { + ##################### + ## Collect Data # + ##################### + + # Handle parent proxy, if any + $hasAProxyParent = $FullModuleData.Keys -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 ($childIdentifier in ($linkedChildren.Keys | Sort-Object)) { + $childResourceType = ($childIdentifier -split '/')[-1] + # Add only if not already exists in the primary parameters + if (($parametersToAdd | Where-Object { $_.name -eq $childResourceType }).Count -eq 0) { + $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..9c7c130d08 --- /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.Keys.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/Read-DeclarationBlock.ps1 b/utilities/tools/REST2CARML/private/module/Read-DeclarationBlock.ps1 new file mode 100644 index 0000000000..69fa5f7704 --- /dev/null +++ b/utilities/tools/REST2CARML/private/module/Read-DeclarationBlock.ps1 @@ -0,0 +1,134 @@ +<# +.SYNOPSIS +Find all instances of a Bicep delcaration block (e.g. 'param') in the given template file + +.DESCRIPTION +Find all instances of a Bicep delcaration block (e.g. 'param') in the given template file. Returns an array of all ocurrences, including their content, start- & end-index + +.PARAMETER DeclarationContent +Mandatory. The Bicep template content to search in + +.PARAMETER DeclarationType +Mandatory. The declaration type to search for. Can be 'param', 'var', 'resource', 'module', or 'output' + +.EXAMPLE +Read-DeclarationBlock -DeclarationContent @('targetScope = 'subscription', '', '@description('Some Description'), param description string, '', (..)) -DeclarationType 'param + +Get all 'param' declaration blocks of the given declaration content. +#> +function Read-DeclarationBlock { + + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [object[]] $DeclarationContent, + + [Parameter(Mandatory = $true)] + [ValidateSet('param', 'var', 'resource', 'module', 'output')] + [string] $DeclarationType + ) + + ###################################################################################################################################### + ## Define which key-words indicate a new declaration (depending on whether you'd move up in the template, or down line by line) ## + ###################################################################################################################################### + + # Any keyword when moving from the current line up to indicate that it's a new declaration + $newLogicIdentifiersUp = @( + '^targetScope.+$', + '^param .+$', + '^var .+$', + '^resource .+$', + '^module .+$', + '^output .+$', + '^//.*$' + '^\s*$' + ) + # Any keyword when moving from the current line down to indicate that it's a new declaration + $newLogicIdentifiersDown = @( + '^param .+$', + '^var .+$', + '^resource .+$', + '^module .+$', + '^output .+$', + '^//.*$', + '^@.+$'#, + #'^\s*$' + ) + + ######################################### + ## Find all indexes to investigate ## + ######################################### + $existingBlocks = @() + + $declarationIndexes = @() + for ($index = 0; $index -lt $DeclarationContent.Count; $index++) { + if ($DeclarationContent[$index] -like "$DeclarationType *") { + $declarationIndexes += $index + } + } + + ######################################################################################################################## + ## Process each found index and find its element's start- & end-index (i.e., where its declaration starts & ends) ## + ######################################################################################################################## + foreach ($declarationIndex in $declarationIndexes) { + switch ($DeclarationType) { + { $PSItem -in @('param') } { + # Let's go 'up' until the declarations end + $declarationStartIndex = $declarationIndex + while ($DeclarationContent[$declarationStartIndex] -eq $DeclarationContent[$declarationIndex] -or (($newLogicIdentifiersUp | Where-Object { $DeclarationContent[$declarationStartIndex] -match $_ }).Count -eq 0 -and $declarationStartIndex -ne 0)) { + $declarationStartIndex-- + } + # Logic always counts one too far + $declarationStartIndex++ + + # The declaration line is always the last line of the block + $declarationEndIndex = $declarationIndex + while ($DeclarationContent[$declarationEndIndex] -eq $DeclarationContent[$declarationIndex] -or (($newLogicIdentifiersDown | Where-Object { $DeclarationContent[$declarationEndIndex] -match $_ }).Count -eq 0 -and $declarationEndIndex -ne $DeclarationContent.Count)) { + $declarationEndIndex++ + } + # Logic always counts one too far + $declarationEndIndex-- + break + } + { $PSItem -in @('output') } { + # Let's go 'up' until the declarations end + $declarationStartIndex = $declarationIndex + while ($DeclarationContent[$declarationStartIndex] -eq $DeclarationContent[$declarationIndex] -or (($newLogicIdentifiersUp | Where-Object { $DeclarationContent[$declarationStartIndex] -match $_ }).Count -eq 0 -and $declarationStartIndex -ne 0)) { + $declarationStartIndex-- + } + # Logic always counts one too far + $declarationStartIndex++ + + # The declaration line is always the last line of the block + $declarationEndIndex = $declarationIndex + break + } + { $PSItem -in @('var', 'resource', 'module') } { + # The declaration line is always the first line of the block + $declarationStartIndex = $declarationIndex + + # Let's go 'down' until the var declarations end + $declarationEndIndex = $declarationIndex + while ($DeclarationContent[$declarationEndIndex] -eq $DeclarationContent[$declarationIndex] -or (($newLogicIdentifiersDown | Where-Object { $DeclarationContent[$declarationEndIndex] -match $_ }).Count -eq 0 -and $declarationEndIndex -ne $DeclarationContent.Count)) { + $declarationEndIndex++ + } + # Logic always counts one too far + $declarationEndIndex-- + break + } + } + + # Trim empty lines from the end + while ([String]::IsNullOrEmpty($templateContent[$declarationEndIndex])) { + $declarationEndIndex-- + } + + $existingBlocks += @{ + content = $templateContent[$declarationStartIndex .. $declarationEndIndex] + startIndex = $declarationStartIndex + endIndex = $declarationEndIndex + } + } + + return $existingBlocks +} diff --git a/utilities/tools/REST2CARML/private/module/Set-Module.ps1 b/utilities/tools/REST2CARML/private/module/Set-Module.ps1 new file mode 100644 index 0000000000..31ba896d02 --- /dev/null +++ b/utilities/tools/REST2CARML/private/module/Set-Module.ps1 @@ -0,0 +1,170 @@ +<# +.SYNOPSIS +Update the module's files with the provided module data including added extension resources data. + +.DESCRIPTION +Update the module's files with the provided module data including added extension resources data (i.e., RBAC, Diagnostic Settings, Private Endpoints, etc.). + +.PARAMETER FullResourceType +Mandatory. The complete ResourceType identifier to update the template for (e.g. 'Microsoft.Storage/storageAccounts'). + +.PARAMETER ModuleData +Mandatory. The module data (e.g. parameters) to add to the template. + +.PARAMETER FullModuleData +Mandatory. The full stack of module data of all modules included in the original invocation. May be used for parent-child references. + +.PARAMETER JSONFilePath +Mandatory. The service specification file to process. + +.PARAMETER UrlPath +Mandatory. The API Path in the JSON specification file to process + +.EXAMPLE +Set-Module -FullResourceType 'Microsoft.KeyVault/vaults' -ModuleData @{ parameters = @(...); resource = @(...); (...) } -JSONFilePath '(...)/resource-manager/Microsoft.KeyVault/stable/2022-07-01/keyvault.json' -UrlPath '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.KeyVault/vaults/{vaultName}' -FullModuleData @(@{ parameters = @(...); resource = @(...); (...) }, @{...}) + +Update the module [Microsoft.KeyVault/vaults] with the provided module data. +#> +function Set-Module { + + [CmdletBinding(SupportsShouldProcess)] + param ( + [Parameter(Mandatory = $true)] + [string] $FullResourceType, + + [Parameter(Mandatory = $true)] + [Hashtable] $ModuleData, + + [Parameter(Mandatory = $true)] + [hashtable] $FullModuleData, + + [Parameter(Mandatory = $true)] + [string] $JSONFilePath, + + [Parameter(Mandatory = $true)] + [string] $UrlPath + ) + + begin { + Write-Debug ('{0} entered' -f $MyInvocation.MyCommand) + + $moduleRootPath = Join-Path $script:repoRoot 'modules' $FullResourceType + $providerNamespace = ($FullResourceType -split '/')[0] + $resourceType = $FullResourceType -replace "$providerNamespace/", '' + $templatePath = Join-Path $moduleRootPath 'deploy.bicep' + $isTopLevelModule = ($FullResourceType -split '\/').Count -eq 2 + + # Load external functions + . (Join-Path $script:repoRoot 'utilities' 'tools' 'Set-ModuleReadMe.ps1') + } + + process { + ########################################## + ## Collection addtional information ## + ########################################## + + # RBAC + if ($ModuleData.roleAssignmentOptions.Count -gt 0) { + $rbacInputObject = @{ + ProviderNamespace = $ProviderNamespace + RelevantRoles = $ModuleData.roleAssignmentOptions + ResourceType = $ResourceType + ModuleData = $ModuleData + ServiceApiVersion = Split-Path (Split-Path $JSONFilePath -Parent) -Leaf + } + Set-RoleAssignmentsModuleData @rbacInputObject + } + + # Private Endpoints + if ($ModuleData.supportsPrivateEndpoints) { + $endpInputObject = @{ + ResourceType = $ResourceType + ModuleData = $ModuleData + } + Set-PrivateEndpointModuleData @endpInputObject + } + + # Locks + if ($ModuleData.supportsLocks) { + $lockInputObject = @{ + ResourceType = $ResourceType + ModuleData = $ModuleData + } + Set-LockModuleData @lockInputObject + } + + # Diagnostic Settings + if ($ModuleData.diagnosticMetricsOptions.count -gt 0 -or $ModuleData.diagnosticLogsOptions.count -gt 0) { + $diagInputObject = @{ + ResourceType = $ResourceType + DiagnosticMetricsOptions = $ModuleData.diagnosticMetricsOptions + DiagnosticLogsOptions = $ModuleData.diagnosticLogsOptions + ModuleData = $ModuleData + } + Set-DiagnosticModuleData @diagInputObject + } + + ############################# + ## Update Support Files # + ############################# + + # Pipeline & test files (top-level module only) + if ($isTopLevelModule) { + Set-ModulePipelineFile -FullResourceType $FullResourceType + Set-ModuleTestFile -FullResourceType $FullResourceType + } + + # Module files + $versionFilePath = Join-Path $moduleRootPath 'version.json' + if (-not (Test-Path $versionFilePath)) { + if ($PSCmdlet.ShouldProcess(('Version file [{0}]' -f ($versionFilePath -replace ($script:repoRoot -replace '\\', '\\'), '')), 'Create')) { + $versionFileContent = Get-Content (Join-Path $script:src 'moduleVersion.json') -Raw + $null = New-Item -Path $versionFilePath -ItemType 'File' -Value $versionFileContent -Force + } + } else { + Write-Verbose ('Version file [{0}] already exists.' -f ("$providerNamespace{0}" -f ($versionFilePath -split $providerNamespace)[1])) + } + + # Additional files as per API-Specs + foreach ($fileDefinition in $ModuleData.additionalFiles) { + $supportFilePath = Join-Path $ModuleRootPath $fileDefinition.relativeFilePath + if (-not (Test-Path $supportFilePath)) { + if ($PSCmdlet.ShouldProcess(('File [{0}].' -f (Split-Path $supportFilePath -Leaf)), 'Create')) { + $null = New-Item -Path $supportFilePath -ItemType 'File' -Value $fileDefinition.fileContent -Force + } + } else { + if ($PSCmdlet.ShouldProcess(('File [{0}].' -f (Split-Path $supportFilePath -Leaf)), 'Update')) { + $null = Set-Content -Path $supportFilePath -Value $fileDefinition.fileContent -NoNewline # -NoNewLine added to achieve the same behavior on file creation and update + } + } + } + + ############################# + ## Update Template File # + ############################# + $moduleTemplateContentInputObject = @{ + FullResourceType = $FullResourceType + ModuleData = $ModuleData + FullModuleData = $FullModuleData + JSONFilePath = $JSONFilePath + urlPath = $UrlPath + } + Set-ModuleTemplate @moduleTemplateContentInputObject + + ############################# + ## Update Module ReadMe # + ############################# + try { + if ($PSCmdlet.ShouldProcess(('Module ReadMe [{0}]' -f (Join-Path (Split-Path $templatePath -Parent) 'readme.md')), 'Update')) { + Set-ModuleReadMe -TemplateFilePath $templatePath -Verbose:$false + } + } catch { + 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'``" + } + } + + end { + Write-Debug ('{0} exited' -f $MyInvocation.MyCommand) + } + +} diff --git a/utilities/tools/REST2CARML/private/module/Set-ModulePipelineFile.ps1 b/utilities/tools/REST2CARML/private/module/Set-ModulePipelineFile.ps1 new file mode 100644 index 0000000000..4603f3b80c --- /dev/null +++ b/utilities/tools/REST2CARML/private/module/Set-ModulePipelineFile.ps1 @@ -0,0 +1,67 @@ +<# +.SYNOPSIS +Set the required pipeline files for the given Resource Type identifier (i.e., the Azure DevOps pipeline & GitHub workflow files) + +.DESCRIPTION +Set the required pipeline files for the given Resource Type identifier (i.e., the Azure DevOps pipeline & GitHub workflow files) + +.PARAMETER FullResourceType +Mandatory. The complete ResourceType identifier to add the files for (e.g. 'Microsoft.Storage/storageAccounts'). + +.EXAMPLE +Set-ModulePipelineFile -FullResourceType 'Microsoft.KeyVault/vaults' + +Set the pipeline/workflow files for the resource type identifier 'Microsoft.KeyVault/vaults' +#> +function Set-ModulePipelineFile { + + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter(Mandatory = $true)] + [string] $FullResourceType + ) + + $ProviderNamespace = ($FullResourceType -split '/')[0] + $ResourceType = $FullResourceType -replace "$ProviderNamespace/", '' + + # Tokens to replace in files + $tokens = @{ + providerNamespace = $ProviderNamespace + shortProviderNamespacePascal = ($ProviderNamespace -split '\.')[-1].substring(0, 1).toupper() + ($ProviderNamespace -split '\.')[-1].substring(1) + shortProviderNamespaceLower = ($ProviderNamespace -split '\.')[-1].ToLower() + resourceType = $ResourceType + resourceTypePascal = $ResourceType.substring(0, 1).toupper() + $ResourceType.substring(1) + resourceTypeLower = $ResourceType.ToLower() + } + + # Create/Update DevOps files + # -------------------------- + ## GitHub + $automationFileName = ('ms.{0}.{1}.yml' -f ($ProviderNamespace -split '\.')[-1], $ResourceType).ToLower() + $gitHubWorkflowYAMLPath = Join-Path $script:repoRoot '.github' 'workflows' $automationFileName + $workflowFileContent = Get-Content (Join-Path $script:src 'gitHubWorkflowTemplateFile.yml') -Raw + $workflowFileContent = Set-TokenValuesInArray -Content $workflowFileContent -Tokens $tokens + if (-not (Test-Path $gitHubWorkflowYAMLPath)) { + if ($PSCmdlet.ShouldProcess("GitHub Workflow file [$automationFileName]", 'Create')) { + $null = New-Item $gitHubWorkflowYAMLPath -ItemType 'File' -Value $workflowFileContent.TrimEnd() + } + } else { + if ($PSCmdlet.ShouldProcess("GitHub Workflow file [$automationFileName]", 'Update')) { + $null = Set-Content -Path $gitHubWorkflowYAMLPath -Value $workflowFileContent.TrimEnd() + } + } + + ## Azure DevOps + $azureDevOpsPipelineYAMLPath = Join-Path $script:repoRoot '.azuredevops' 'modulePipelines' $automationFileName + $pipelineFileContent = Get-Content (Join-Path $script:src 'azureDevOpsPipelineTemplateFile.yml') -Raw + $pipelineFileContent = Set-TokenValuesInArray -Content $pipelineFileContent -Tokens $tokens + if (-not (Test-Path $azureDevOpsPipelineYAMLPath)) { + if ($PSCmdlet.ShouldProcess("GitHub Workflow file [$automationFileName]", 'Create')) { + $null = New-Item $azureDevOpsPipelineYAMLPath -ItemType 'File' -Value $pipelineFileContent.TrimEnd() + } + } else { + if ($PSCmdlet.ShouldProcess("GitHub Workflow file [$automationFileName]", 'Update')) { + $null = Set-Content -Path $azureDevOpsPipelineYAMLPath -Value $pipelineFileContent.TrimEnd() + } + } +} diff --git a/utilities/tools/REST2CARML/private/module/Set-ModuleTemplate.ps1 b/utilities/tools/REST2CARML/private/module/Set-ModuleTemplate.ps1 new file mode 100644 index 0000000000..337ccd212f --- /dev/null +++ b/utilities/tools/REST2CARML/private/module/Set-ModuleTemplate.ps1 @@ -0,0 +1,194 @@ +<# +.SYNOPSIS +Update the module's primary template (deploy.bicep) as per the provided module data. + +.DESCRIPTION +Update the module's primary template (deploy.bicep) as per the provided module data. + +.PARAMETER FullResourceType +Mandatory. The complete ResourceType identifier to update the template for (e.g. 'Microsoft.Storage/storageAccounts'). + +.PARAMETER ModuleData +Mandatory. The module data (e.g. parameters) to add to the template. + +.PARAMETER FullModuleData +Mandatory. The full stack of module data of all modules included in the original invocation. May be used for parent-child references. + +.PARAMETER JSONFilePath +Mandatory. The service specification file to process. + +.PARAMETER UrlPath +Mandatory. The API Path in the JSON specification file to process + +.EXAMPLE +Set-ModuleTemplate -FullResourceType 'Microsoft.KeyVault/vaults' -ModuleData @{ parameters = @(...); resource = @(...); (...) } -JSONFilePath '(...)/resource-manager/Microsoft.KeyVault/stable/2022-07-01/keyvault.json' -UrlPath '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.KeyVault/vaults/{vaultName}' -FullModuleData @(@{ parameters = @(...); resource = @(...); (...) }, @{...}) + +Update the module [Microsoft.KeyVault/vaults] with the provided module data. +#> +function Set-ModuleTemplate { + + [CmdletBinding(SupportsShouldProcess)] + param ( + [Parameter(Mandatory = $true)] + [string] $FullResourceType, + + [Parameter(Mandatory = $true)] + [array] $ModuleData, + + [Parameter(Mandatory = $true)] + [hashtable] $FullModuleData, + + [Parameter(Mandatory = $true)] + [string] $JSONFilePath, + + [Parameter(Mandatory = $true)] + [string] $UrlPath + ) + + begin { + Write-Debug ('{0} entered' -f $MyInvocation.MyCommand) + } + + 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 = Get-LinkedChildModuleList -FullModuleData $FullModuleData -FullResourceType $FullResourceType + + # Collect parent resources to use for parent type references + $typeElem = $FullResourceType -split '/' + if ($typeElem.Count -gt 2) { + $parentResourceTypes = $typeElem[1..($typeElem.Count - 2)] + } else { + $parentResourceTypes = @() + } + + # Get the singular version of the current resource type for proper naming + $resourceTypeSingular = ((Get-ResourceTypeSingularName -ResourceType $resourceType) -split '/')[-1] + #endregion + + ############# + ## SCOPE ## + ############# + $targetScope = Get-TargetScope -UrlPath $UrlPath + + $templateContent = ($targetScope -ne 'resourceGroup') ? @( + "targetScope = '{0}'" -f $targetScope, + '' + ) : @() + + ################## + ## PARAMETERS ## + ################## + #region parameters + + $parametersInputObject = @{ + ModuleData = $ModuleData + FullModuleData = $FullModuleData + FullResourceType = $FullResourceType + } + if ($ExistingTemplateContent.Count -gt 0) { + $parametersInputObject['ExistingTemplateContent'] = $ExistingTemplateContent + } + if ($ParentResourceTypes.Count -gt 0) { + $parametersInputObject['ParentResourceTypes'] = $ParentResourceTypes + } + if ($LinkedChildren.Keys.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 += '' + } + + $variablesInputObject = @{ + ModuleData = $ModuleData + } + if ($ExistingTemplateContent.Count -gt 0) { + $variablesInputObject['ExistingTemplateContent'] = $ExistingTemplateContent + } + $templateContent += Get-TemplateVariablesContent @variablesInputObject + #endregion + + ################### + ## DEPLOYMENTS ## + ################### + #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 += '' + } + + $resourcesInputObject = @{ + FullResourceType = $FullResourceType + ResourceType = $ResourceType + ResourceTypeSingular = $ResourceTypeSingular + ModuleData = $ModuleData + FullModuleData = $FullModuleData + } + if ($ExistingTemplateContent.Count -gt 0) { + $resourcesInputObject['ExistingTemplateContent'] = $ExistingTemplateContent + } + if ($ParentResourceTypes.Count -gt 0) { + $resourcesInputObject['ParentResourceTypes'] = $ParentResourceTypes + } + if ($LinkedChildren.Keys.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 += '' + } + + $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 + # ----------- + Set-Content -Path $templateFilePath -Value ($templateContent | Out-String).TrimEnd() -Force + } + + end { + Write-Debug ('{0} exited' -f $MyInvocation.MyCommand) + } +} diff --git a/utilities/tools/REST2CARML/private/module/Set-ModuleTestFile.ps1 b/utilities/tools/REST2CARML/private/module/Set-ModuleTestFile.ps1 new file mode 100644 index 0000000000..7bdaa392f4 --- /dev/null +++ b/utilities/tools/REST2CARML/private/module/Set-ModuleTestFile.ps1 @@ -0,0 +1,50 @@ +<# +.SYNOPSIS +Set the test files for the given Resource Type identifier (i.e., the Azure DevOps pipeline & GitHub workflow files) + +.DESCRIPTION +Set the test files for the given Resource Type identifier (i.e., the Azure DevOps pipeline & GitHub workflow files) + +.PARAMETER FullResourceType +Mandatory. The complete ResourceType identifier to add the test files for (e.g. 'Microsoft.Storage/storageAccounts'). + +.EXAMPLE +Set-ModuleTestFile -FullResourceType 'Microsoft.KeyVault/vaults' + +Set the test files for the resource type identifier 'Microsoft.KeyVault/vaults' +#> +function Set-ModuleTestFile { + + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter(Mandatory = $true)] + [string] $FullResourceType + ) + + begin { + Write-Debug ('{0} entered' -f $MyInvocation.MyCommand) + $moduleRootPath = Join-Path $script:repoRoot 'modules' $FullResourceType + } + + process { + # Create module files + # ------------------- + ## .test files + @( + (Join-Path $moduleRootPath '.test' 'common' 'deploy.test.bicep') + (Join-Path $moduleRootPath '.test' 'min' 'deploy.test.bicep') + ) | ForEach-Object { + if (-not (Test-Path $_)) { + if ($PSCmdlet.ShouldProcess(('File [{0}]' -f ($_ -replace ($script:repoRoot -replace '\\', '\\'), '')), 'Create')) { + $null = New-Item -Path $_ -ItemType 'File' -Force + } + } else { + Write-Verbose "File [$_] already exists." + } + } + } + + end { + Write-Debug ('{0} exited' -f $MyInvocation.MyCommand) + } +} diff --git a/utilities/tools/REST2CARML/private/shared/Get-LineIndentation.ps1 b/utilities/tools/REST2CARML/private/shared/Get-LineIndentation.ps1 new file mode 100644 index 0000000000..9168cc2042 --- /dev/null +++ b/utilities/tools/REST2CARML/private/shared/Get-LineIndentation.ps1 @@ -0,0 +1,50 @@ +<# +.SYNOPSIS +Retrieve indentation of a line. + +.DESCRIPTION +Retrieve indentation of a line. + +.PARAMETER Line +Mandatory. The line to analyse for indentation. + +.EXAMPLE +Get-LineIndentation -Line ' Test' + +Retrieve indentation of line ' Test'. Would return 4. +#> +function Get-LineIndentation { + + [CmdletBinding()] + param ( + [Parameter(Mandatory = $false)] + [string] $Line + ) + + begin { + Write-Debug ('{0} entered' -f $MyInvocation.MyCommand) + } + + process { + $indentation = 0 + for ($i = 0; $i -lt $Line.Length; $i++) { + $Char = $Line[$i] + switch -regex ($Char) { + '`t' { + $indentation += 2 + } + ' ' { + $indentation += 1 + } + default { + return $indentation + } + } + } + return $indentation + } + + end { + Write-Debug ('{0} exited' -f $MyInvocation.MyCommand) + } +} diff --git a/utilities/tools/REST2CARML/private/shared/Get-ResourceTypeSingularName.ps1 b/utilities/tools/REST2CARML/private/shared/Get-ResourceTypeSingularName.ps1 new file mode 100644 index 0000000000..1d40d35bc3 --- /dev/null +++ b/utilities/tools/REST2CARML/private/shared/Get-ResourceTypeSingularName.ps1 @@ -0,0 +1,29 @@ +<# +.SYNOPSIS +Get the singular version of the given resource type + +.DESCRIPTION +Get the singular version of the given resource type + +.PARAMETER ResourceType +The resource type to convert to singular (if applicable) + +.EXAMPLE +Get-ResourceTypeSingularName -ResourceType 'vaults' + +Returns 'vault' +#> +function Get-ResourceTypeSingularName { + + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string] $ResourceType + ) + + if ($ResourceType -like '*ii') { return $ResourceType -replace 'ii$', 'us' } + if ($ResourceType -like '*ies') { return $ResourceType -replace 'ies$', 'y' } + if ($ResourceType -like '*s') { return $ResourceType -replace 's$', '' } + + return $ResourceType +} diff --git a/utilities/tools/REST2CARML/private/shared/Get-TargetScope.ps1 b/utilities/tools/REST2CARML/private/shared/Get-TargetScope.ps1 new file mode 100644 index 0000000000..42d575a7ce --- /dev/null +++ b/utilities/tools/REST2CARML/private/shared/Get-TargetScope.ps1 @@ -0,0 +1,32 @@ +<# +.SYNOPSIS +Get the target scope (bicep) of a given key path. + +.DESCRIPTION +Get the target scope (bicep) of a given key path. For example 'resourceGroup'. + +.PARAMETER UrlPath +Mandatory. The key path to check for its scope. + +.EXAMPLE +Get-TargetScope -UrlPath 'subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.KeyVault/vaults/{vaultName}' + +Check the given KeyPath for its scope. Would return 'resourceGroup'. +#> +function Get-TargetScope { + + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string] $UrlPath + ) + + switch ($UrlPath) { + { $PSItem -like '/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/*' } { return 'resourceGroup' } + { $PSItem -like '/subscriptions/{subscriptionId}/*' } { return 'subscription' } + { $PSItem -like '/providers/Microsoft.Management/managementGroups/*' } { return 'managementGroup' } + Default { + throw 'Unable to detect target scope' + } + } +} diff --git a/utilities/tools/REST2CARML/private/shared/Set-TokenValuesInArray.ps1 b/utilities/tools/REST2CARML/private/shared/Set-TokenValuesInArray.ps1 new file mode 100644 index 0000000000..7fba4d2483 --- /dev/null +++ b/utilities/tools/REST2CARML/private/shared/Set-TokenValuesInArray.ps1 @@ -0,0 +1,44 @@ +<# +.SYNOPSIS +Replace tokens like '<>' in the given file with an actual value + +.DESCRIPTION +Replace tokens like '<>' in the given file with an actual value. + +.PARAMETER Content +Mandatory. The content to update + +.PARAMETER Tokens +A hashtable of tokens to replace + +.EXAMPLE +Set-TokenValuesInArray -Content "Hello <>-<>" -Tokens @{ shortProviderNamespaceLower = 'keyvault'; resourceTypePascal = 'Vaults' } + +Update the provided content with different Provider Namespace & Resource Type token variant. Would return 'Hello keyvault-Vaults' +#> +function Set-TokenValuesInArray { + + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string] $Content, + + [Parameter(Mandatory = $true)] + [hashtable] $Tokens + ) + + begin { + Write-Debug ('{0} entered' -f $MyInvocation.MyCommand) + } + + process { + foreach ($token in $tokens.Keys) { + $content = $content -replace "<<$token>>", $tokens[$token] + } + return $content + } + + end { + Write-Debug ('{0} exited' -f $MyInvocation.MyCommand) + } +} diff --git a/utilities/tools/REST2CARML/public/Invoke-REST2CARML.ps1 b/utilities/tools/REST2CARML/public/Invoke-REST2CARML.ps1 new file mode 100644 index 0000000000..a3d7eb7316 --- /dev/null +++ b/utilities/tools/REST2CARML/public/Invoke-REST2CARML.ps1 @@ -0,0 +1,98 @@ +<# +.SYNOPSIS +Create/Update a CARML module based on the latest API information available + +.DESCRIPTION +Create/Update a CARML module based on the latest API information available. +NOTE: As we query some data from Azure, you must be connected to an Azure Context to use this function + +.PARAMETER FullResourceType +Mandatory. The full resource type including the provider namespace to query the data for (e.g., Microsoft.Storage/storageAccounts) + +.PARAMETER ExcludeChildren +Optional. Don't include child resource types in the result + +.PARAMETER IncludePreview +Mandatory. Include preview API versions + +.PARAMETER KeepArtifacts +Optional. Skip the removal of downloaded/cloned artifacts (e.g. the API-Specs repository). Useful if you want to run the function multiple times in a row. + +.EXAMPLE +Invoke-REST2CARML -FullResourceType 'Microsoft.Keyvault/vaults' + +Generate/Update a CARML module for [Microsoft.Keyvault/vaults] + +.EXAMPLE +Invoke-REST2CARML -FullResourceType 'Microsoft.AVS/privateClouds' -Verbose -KeepArtifacts + +Generate/Update a CARML module for [Microsoft.AVS/privateClouds] and do not delete any downloaded/cloned artifact. + +.EXAMPLE +Invoke-REST2CARML -FullResourceType 'Microsoft.Keyvault/vaults' -KeepArtifacts + +Generate/Update a CARML module for [Microsoft.Keyvault/vaults] and do not delete any downloaded/cloned artifact. + +.EXAMPLE +Invoke-AzureApiCrawler -FullResourceType 'Microsoft.Storage/storageAccounts/blobServices/containers' -Verbose -KeepArtifacts + +Generate/Update a CARML module for [Microsoft.Storage/storageAccounts/blobServices/containers] and do not delete any downloaded/cloned artifact. +#> +function Invoke-REST2CARML { + + [CmdletBinding(SupportsShouldProcess)] + param ( + [Parameter(Mandatory = $true)] + [string] $FullResourceType, + + [Parameter(Mandatory = $false)] + [switch] $ExcludeChildren, + + [Parameter(Mandatory = $false)] + [switch] $IncludePreview, + + [Parameter(Mandatory = $false)] + [switch] $KeepArtifacts + ) + + begin { + Write-Debug ('{0} entered' -f $MyInvocation.MyCommand) + Write-Verbose ('Processing module [{0}]' -f $FullResourceType) -Verbose + } + + process { + + ############################################ + ## Extract module data from API specs ## + ############################################ + + $apiSpecsInputObject = @{ + FullResourceType = $FullResourceType + ExcludeChildren = $ExcludeChildren + IncludePreview = $IncludePreview + KeepArtifacts = $KeepArtifacts + } + $fullModuleData = Get-AzureAPISpecsData @apiSpecsInputObject + + ############################ + ## Set module content ## + ############################ + foreach ($identifier in ($fullModuleData.Keys | Sort-Object)) { + # Sort shortest to longest path + $moduleTemplateInputObject = @{ + FullResourceType = $identifier + JSONFilePath = $fullModuleData[$identifier].metadata.jsonFilePath + UrlPath = $fullModuleData[$identifier].metadata.urlPath + ModuleData = $fullModuleData[$identifier].data + FullModuleData = $fullModuleData + } + if ($PSCmdlet.ShouldProcess(('Module [{0}] files' -f $identifier), 'Create/Update')) { + Set-Module @moduleTemplateInputObject + } + } + } + + end { + Write-Debug ('{0} exited' -f $MyInvocation.MyCommand) + } +} diff --git a/utilities/tools/REST2CARML/public/Resolve-ExistingTemplateContent.ps1 b/utilities/tools/REST2CARML/public/Resolve-ExistingTemplateContent.ps1 new file mode 100644 index 0000000000..d9744076f9 --- /dev/null +++ b/utilities/tools/REST2CARML/public/Resolve-ExistingTemplateContent.ps1 @@ -0,0 +1,132 @@ +<# +.SYNOPSIS +Get details about all parameters, variables, resources, modules & outputs in the given template + +.DESCRIPTION +Get details about all parameters, variables, resources, modules & outputs in the given template. Depending on the type of declaration, you further get information like names, types, nested properties, etc. + +.PARAMETER TemplateFilePath +Mandatory. The path of the template to extract the data from. + +.EXAMPLE +Resolve-ExistingTemplateContent -TemplateFilePath 'C:/dev/Microsoft.Storage/storageAccounts/deploy.bicep' + +Get all the requested information from the template in path 'C:/dev/Microsoft.Storage/storageAccounts/deploy.bicep'. Returns an object like: + +Name Value +---- ----- +outputs {resourceId, name, resourceGroupName, primaryBlobEndpoint…} +modules {System.Collections.Hashtable, System.Collections.Hashtable, System.Collections.Hashtable, System.Collections.Hashtable…} +variables {diagnosticsMetrics, supportsBlobService, supportsFileService, identityType…} +resources {System.Collections.Hashtable, System.Collections.Hashtable, System.Collections.Hashtable, System.Collections.Hashtable…} +parameters {name, location, roleAssignments, systemAssignedIdentity…} + +And if you drill down, for resources an array like: + +Name Value +---- ----- +startIndex 173 +endIndex 183 +content {resource defaultTelemetry 'Microsoft.Resources/deployments@2021-04-01' = if (enableDefaultTelemetry) {, name: 'pid-47ed15a6-730a-4827-bcb4-0fd963ffbd82-${uniqueString(deployment… +nestedElements {mode, template} +topLevelElements name + +startIndex 185 +endIndex 188 +content {resource keyVault 'Microsoft.KeyVault/vaults@2021-06-01-preview' existing = if (!empty(cMKKeyVaultResourceId)) {, name: last(split(cMKKeyVaultResourceId, '/')), scope: resourc… +topLevelElements {name, scope} + +startIndex 190 +endIndex 240 +content {resource storageAccount 'Microsoft.Storage/storageAccounts@2021-09-01' = {, name: name, location: location, kind: storageAccountKind…} +nestedElements {encryption, accessTier, supportsHttpsTrafficOnly, isHnsEnabled…} +topLevelElements {name, location, kind, sku…} + +startIndex 242 +endIndex 252 +content {resource storageAccount_diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if ((!empty(diagnosticStorageAccountId)) || (!empty(diagnosticWorkspaceId))… +nestedElements {storageAccountId, workspaceId, eventHubAuthorizationRuleId, eventHubName…} +topLevelElements {name, scope} + +startIndex 254 +endIndex 261 +content {resource storageAccount_lock 'Microsoft.Authorization/locks@2017-04-01' = if (!empty(lock)) {, name: '${storageAccount.name}-${lock}-lock', Elements: {, level: any(lock)… +nestedElements {level, notes} +topLevelElements {name, scope} +#> +function Resolve-ExistingTemplateContent { + + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string] $TemplateFilePath + ) + + if (-not (Test-Path $TemplateFilePath)) { + return + } + + $templateContent = Get-Content -Path $TemplateFilePath + + ############################ + ## Extract Parameters ## + ############################ + $existingParameterBlocks = Read-DeclarationBlock -DeclarationContent $templateContent -DeclarationType 'param' + + # Analyze content + foreach ($block in $existingParameterBlocks) { + $block['name'] = (($block.content | Where-Object { $_ -like 'param *' }) -split ' ')[1] + $block['type'] = (($block.content | Where-Object { $_ -like 'param *' }) -split ' ')[2] + } + + ########################### + ## Extract Variables ## + ########################### + $existingVariableBlocks = Read-DeclarationBlock -DeclarationContent $templateContent -DeclarationType 'var' + + # Analyze content + foreach ($block in $existingVariableBlocks) { + $block['name'] = (($block.content | Where-Object { $_ -like 'var *' }) -split ' ')[1] + } + + ########################### + ## Extract Resources ## + ########################### + $existingResourceBlocks = Read-DeclarationBlock -DeclarationContent $templateContent -DeclarationType 'resource' + + # Analyze content + foreach ($block in $existingResourceBlocks) { + Expand-DeploymentBlock -DeclarationBlock $block -NestedType 'properties' + } + + ######################### + ## Extract Modules ## + ######################### + $existingModuleBlocks = Read-DeclarationBlock -DeclarationContent $templateContent -DeclarationType 'module' + + # Analyze content + foreach ($block in $existingModuleBlocks) { + Expand-DeploymentBlock -DeclarationBlock $block -NestedType 'params' + } + + ######################### + ## Extract Outputs ## + ######################### + $existingOutputBlocks = Read-DeclarationBlock -DeclarationContent $templateContent -DeclarationType 'output' + + foreach ($block in $existingOutputBlocks) { + $block['name'] = (($block.content | Where-Object { $_ -like 'output *' }) -split ' ')[1] + $block['type'] = (($block.content | Where-Object { $_ -like 'output *' }) -split ' ')[2] + } + + ####################### + ## Return result ## + ####################### + return @{ + parameters = $existingParameterBlocks + variables = $existingVariableBlocks + resources = $existingResourceBlocks + modules = $existingModuleBlocks + outputs = $existingOutputBlocks + } +} diff --git a/utilities/tools/REST2CARML/readme.md b/utilities/tools/REST2CARML/readme.md new file mode 100644 index 0000000000..0577295b61 --- /dev/null +++ b/utilities/tools/REST2CARML/readme.md @@ -0,0 +1,37 @@ +# REST to CARML + +This module provides you with the ability to generate most of a CARML module's code by providing it with the desired Provider-Namespace / Resource-Type combination. + +> **_NOTE:_** This module will not generate all the code required for a CARML module, but only a large portion of it. As the Azure API is not 100% consistent, generated modules may still require manual refactoring (e.g. by introducing variables) or may contain errors. Further, while the expected test files & folders are generated, they are not populated with content as the utility would otherwise need to know how to all required dependencies too. + +### _Navigation_ + +- [Usage](#usage) +- [In-scope](#in-scope) +- [Out-of-scope](#out-of-scope) + +--- + + +## Usage +- Import the module using the command `Import-Module './utilities/tools/REST2CARML/REST2CARML.psm1' -Force -Verbose` +- Invoke its primary function using the command `Invoke-REST2CARML -ProviderNamespace '' -ResourceType '' -Verbose -KeepArtifacts` +- For repeated runs it is recommended to append the `-KeepArtifacts` parameter as the function will otherwise repeatably download & eventually delete the required documentation + +# In scope + +- Module itself with parameters, resource & outputs +- Child-modules with references via their parent +- Azure DevOps pipeline & GitHub workflow +- Extension code such as + - Diagnostic Settings + - RBAC + - Locks + - Private Endpoints + +# Out of scope + +- Idempotency: Run the module on existing code without overwriting any un-related content (**_can be implemented later_**). + - Notes: + - Should not update `params` that are already set (as we might have defined a custom default value) + - Should not update `properties`/`params` of resources/modules as we might have implemented custom logic diff --git a/utilities/tools/REST2CARML/src/azureDevOpsPipelineTemplateFile.yml b/utilities/tools/REST2CARML/src/azureDevOpsPipelineTemplateFile.yml new file mode 100644 index 0000000000..be03db149e --- /dev/null +++ b/utilities/tools/REST2CARML/src/azureDevOpsPipelineTemplateFile.yml @@ -0,0 +1,40 @@ +name: '<> - <>' + +parameters: + - name: removeDeployment + displayName: Remove deployed module + type: boolean + default: true + - name: prerelease + displayName: Publish prerelease module + type: boolean + default: false + +pr: none + +trigger: + batch: true + branches: + include: + - main + paths: + include: + - '/.azuredevops/modulePipelines/ms.<>.<>.yml' + - '/.azuredevops/pipelineTemplates/*.yml' + - '/modules/<>/<>/*' + - '/utilities/pipelines/*' + exclude: + - '/utilities/pipelines/deploymentRemoval/*' + - '/**/*.md' + +variables: + - template: '../../settings.yml' + - group: 'PLATFORM_VARIABLES' + - name: modulePath + value: '/modules/<>/<>' + +stages: + - template: /.azuredevops/pipelineTemplates/stages.module.yml + parameters: + removeDeployment: '${{ parameters.removeDeployment }}' + prerelease: '${{ parameters.prerelease }}' diff --git a/utilities/tools/REST2CARML/src/gitHubWorkflowTemplateFile.yml b/utilities/tools/REST2CARML/src/gitHubWorkflowTemplateFile.yml new file mode 100644 index 0000000000..3358fa7147 --- /dev/null +++ b/utilities/tools/REST2CARML/src/gitHubWorkflowTemplateFile.yml @@ -0,0 +1,148 @@ +name: '<>: <>' + +on: + workflow_dispatch: + inputs: + removeDeployment: + type: boolean + description: 'Remove deployed module' + required: false + default: true + prerelease: + type: boolean + description: 'Publish prerelease module' + required: false + default: false + push: + branches: + - main + paths: + - '.github/actions/templates/**' + - '.github/workflows/ms.<>.<>.yml' + - 'modules/<>/<>/**' + - 'utilities/pipelines/**' + - '!utilities/pipelines/deploymentRemoval/**' + - '!*/**/readme.md' + +env: + variablesPath: 'settings.yml' + modulePath: 'modules/<>/<>' + workflowPath: '.github/workflows/ms.<>.<>.yml' + AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }} + ARM_SUBSCRIPTION_ID: '${{ secrets.ARM_SUBSCRIPTION_ID }}' + ARM_MGMTGROUP_ID: '${{ secrets.ARM_MGMTGROUP_ID }}' + ARM_TENANT_ID: '${{ secrets.ARM_TENANT_ID }}' + TOKEN_NAMEPREFIX: '${{ secrets.TOKEN_NAMEPREFIX }}' + +concurrency: + group: ${{ github.workflow }} + +jobs: + ########################### + # Initialize pipeline # + ########################### + job_initialize_pipeline: + runs-on: ubuntu-20.04 + name: 'Initialize pipeline' + steps: + - name: 'Checkout' + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: 'Set input parameters to output variables' + id: get-workflow-param + uses: ./.github/actions/templates/getWorkflowInput + with: + workflowPath: '${{ env.workflowPath}}' + - name: 'Get parameter file paths' + id: get-module-test-file-paths + uses: ./.github/actions/templates/getModuleTestFiles + with: + modulePath: '${{ env.modulePath }}' + outputs: + workflowInput: ${{ steps.get-workflow-param.outputs.workflowInput }} + moduleTestFilePaths: ${{ steps.get-module-test-file-paths.outputs.moduleTestFilePaths }} + + ######################### + # Static validation # + ######################### + job_module_pester_validation: + runs-on: ubuntu-20.04 + name: 'Static validation' + steps: + - name: 'Checkout' + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Set environment variables + uses: ./.github/actions/templates/setEnvironmentVariables + with: + variablesPath: ${{ env.variablesPath }} + - name: 'Run tests' + uses: ./.github/actions/templates/validateModulePester + with: + modulePath: '${{ env.modulePath }}' + moduleTestFilePath: '${{ env.moduleTestFilePath }}' + + ############################# + # Deployment validation # + ############################# + job_module_deploy_validation: + runs-on: ubuntu-20.04 + name: 'Deployment validation' + needs: + - job_initialize_pipeline + - job_module_pester_validation + strategy: + fail-fast: false + matrix: + moduleTestFilePaths: ${{ fromJson(needs.job_initialize_pipeline.outputs.moduleTestFilePaths) }} + steps: + - name: 'Checkout' + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Set environment variables + uses: ./.github/actions/templates/setEnvironmentVariables + with: + variablesPath: ${{ env.variablesPath }} + - name: 'Using test file [${{ matrix.moduleTestFilePaths }}]' + uses: ./.github/actions/templates/validateModuleDeployment + with: + templateFilePath: '${{ env.modulePath }}/${{ matrix.moduleTestFilePaths }}' + location: '${{ env.location }}' + subscriptionId: '${{ secrets.ARM_SUBSCRIPTION_ID }}' + managementGroupId: '${{ secrets.ARM_MGMTGROUP_ID }}' + removeDeployment: '${{ (fromJson(needs.job_initialize_pipeline.outputs.workflowInput)).removeDeployment }}' + + ################## + # Publishing # + ################## + job_publish_module: + name: 'Publishing' + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' || github.event.inputs.prerelease == 'true' + runs-on: ubuntu-20.04 + needs: + - job_module_deploy_validation + steps: + - name: 'Checkout' + uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Set environment variables + uses: ./.github/actions/templates/setEnvironmentVariables + with: + variablesPath: ${{ env.variablesPath }} + - name: 'Publishing' + uses: ./.github/actions/templates/publishModule + with: + templateFilePath: '${{ env.modulePath }}/deploy.bicep' + templateSpecsRGName: '${{ env.templateSpecsRGName }}' + templateSpecsRGLocation: '${{ env.templateSpecsRGLocation }}' + templateSpecsDescription: '${{ env.templateSpecsDescription }}' + templateSpecsDoPublish: '${{ env.templateSpecsDoPublish }}' + bicepRegistryName: '${{ env.bicepRegistryName }}' + bicepRegistryRGName: '${{ env.bicepRegistryRGName }}' + bicepRegistryRgLocation: '${{ env.bicepRegistryRgLocation }}' + bicepRegistryDoPublish: '${{ env.bicepRegistryDoPublish }}' + publishLatest: '${{ env.publishLatest }}' diff --git a/utilities/tools/REST2CARML/src/moduleVersion.json b/utilities/tools/REST2CARML/src/moduleVersion.json new file mode 100644 index 0000000000..41f66cc990 --- /dev/null +++ b/utilities/tools/REST2CARML/src/moduleVersion.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json", + "version": "0.1" +} diff --git a/utilities/tools/REST2CARML/src/nested_roleAssignments.bicep b/utilities/tools/REST2CARML/src/nested_roleAssignments.bicep new file mode 100644 index 0000000000..667c4b8dce --- /dev/null +++ b/utilities/tools/REST2CARML/src/nested_roleAssignments.bicep @@ -0,0 +1,56 @@ +@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 = { + <> +} + +resource <> '<>/<>@<>' existing = { + name: last(split(resourceId, '/')) +} + +resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for principalId in principalIds: { + name: guid(<>.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: <> +}] diff --git a/utilities/tools/REST2CARML/src/telemetry.bicep b/utilities/tools/REST2CARML/src/telemetry.bicep new file mode 100644 index 0000000000..effe1a3788 --- /dev/null +++ b/utilities/tools/REST2CARML/src/telemetry.bicep @@ -0,0 +1,11 @@ +resource defaultTelemetry 'Microsoft.Resources/deployments@2021-04-01' = if (enableDefaultTelemetry) { + name: 'pid-47ed15a6-730a-4827-bcb4-0fd963ffbd82-${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: [] + } + } +} diff --git a/utilities/tools/Set-ModuleReadMe.ps1 b/utilities/tools/Set-ModuleReadMe.ps1 index fd32b00ae5..93cdb4802f 100644 --- a/utilities/tools/Set-ModuleReadMe.ps1 +++ b/utilities/tools/Set-ModuleReadMe.ps1 @@ -979,331 +979,338 @@ function Set-DeploymentExamplesSection { '

Example {0}: {1}

' -f $pathIndex, $exampleTitle ) - ## ----------------------------------- ## - ## Handle by type (Bicep vs. JSON) ## - ## ----------------------------------- ## - if ((Split-Path $testFilePath -Extension) -eq '.bicep') { - - # ------------------------- # - # Prepare Bicep to JSON # - # ------------------------- # - - # [1/6] Search for the relevant parameter start & end index - $bicepTestStartIndex = ($rawContentArray | Select-String ("^module testDeployment '..\/.*main.bicep' = {$") | ForEach-Object { $_.LineNumber - 1 })[0] - - $bicepTestEndIndex = $bicepTestStartIndex - do { - $bicepTestEndIndex++ - } while ($rawContentArray[$bicepTestEndIndex] -ne '}') - - $rawBicepExample = $rawContentArray[$bicepTestStartIndex..$bicepTestEndIndex] - - # [2/6] Replace placeholders - $serviceShort = ([regex]::Match($rawContent, "(?m)^param serviceShort string = '(.+)'\s*$")).Captures.Groups[1].Value - - $rawBicepExampleString = ($rawBicepExample | Out-String) - $rawBicepExampleString = $rawBicepExampleString -replace '\$\{serviceShort\}', $serviceShort - $rawBicepExampleString = $rawBicepExampleString -replace '\$\{namePrefix\}[-|\.|_]?', '' # Replacing with empty to not expose prefix and avoid potential deployment conflicts - $rawBicepExampleString = $rawBicepExampleString -replace '(?m):\s*location\s*$', ': ''''' - - # [3/6] Format header, remove scope property & any empty line - $rawBicepExample = $rawBicepExampleString -split '\n' - $rawBicepExample[0] = "module $moduleNameCamelCase './$fullModuleIdentifier/main.bicep' = {" - $rawBicepExample = $rawBicepExample | Where-Object { $_ -notmatch 'scope: *' } | Where-Object { -not [String]::IsNullOrEmpty($_) } - - # [4/6] Extract param block - $rawBicepExampleArray = $rawBicepExample -split '\n' - $moduleDeploymentPropertyIndent = ([regex]::Match($rawBicepExampleArray[1], '^(\s+).*')).Captures.Groups[1].Value.Length - $paramsStartIndex = ($rawBicepExampleArray | Select-String ("^[\s]{$moduleDeploymentPropertyIndent}params:[\s]*\{") | ForEach-Object { $_.LineNumber - 1 })[0] + 1 - if ($rawBicepExampleArray[$paramsStartIndex].Trim() -ne '}') { - # Handle case where param block is empty - $paramsEndIndex = ($rawBicepExampleArray[($paramsStartIndex + 1)..($rawBicepExampleArray.Count)] | Select-String "^[\s]{$moduleDeploymentPropertyIndent}\}" | ForEach-Object { $_.LineNumber - 1 })[0] + $paramsStartIndex - $paramBlock = ($rawBicepExampleArray[$paramsStartIndex..$paramsEndIndex] | Out-String).TrimEnd() - } else { - $paramBlock = '' - $paramsEndIndex = $paramsStartIndex - } - - # [5/6] Convert Bicep parameter block to JSON parameter block to enable processing - $conversionInputObject = @{ - BicepParamBlock = $paramBlock - CurrentFilePath = $testFilePath - } - $paramsInJSONFormat = ConvertTo-FormattedJSONParameterObject @conversionInputObject - - # [6/6] Convert JSON parameters back to Bicep and order & format them - $conversionInputObject = @{ - JSONParameters = $paramsInJSONFormat - RequiredParametersList = $RequiredParametersList - } - $bicepExample = ConvertTo-FormattedBicep @conversionInputObject - - # --------------------- # - # Add Bicep example # - # --------------------- # - if ($addBicep) { - - if ([String]::IsNullOrEmpty($paramBlock)) { + # Checking if the test file is empty (possible for modules freshly generated by REST2CARML) + if ([String]::IsNullOrEmpty($rawContent)) { + Write-Warning ('The test file {0} is empty, skipping the file in the examples section' -f $testFilePath) + } else { + ## ----------------------------------- ## + ## Handle by type (Bicep vs. JSON) ## + ## ----------------------------------- ## + if ((Split-Path $testFilePath -Extension) -eq '.bicep') { + + # ------------------------- # + # Prepare Bicep to JSON # + # ------------------------- # + + # [1/6] Search for the relevant parameter start & end index + $bicepTestStartIndex = ($rawContentArray | Select-String ("^module testDeployment '..\/.*main.bicep' = {$") | ForEach-Object { $_.LineNumber - 1 })[0] + + $bicepTestEndIndex = $bicepTestStartIndex + do { + $bicepTestEndIndex++ + } while ($rawContentArray[$bicepTestEndIndex] -ne '}') + + $rawBicepExample = $rawContentArray[$bicepTestStartIndex..$bicepTestEndIndex] + + # [2/6] Replace placeholders + $serviceShort = ([regex]::Match($rawContent, "(?m)^param serviceShort string = '(.+)'\s*$")).Captures.Groups[1].Value + + $rawBicepExampleString = ($rawBicepExample | Out-String) + $rawBicepExampleString = $rawBicepExampleString -replace '\$\{serviceShort\}', $serviceShort + $rawBicepExampleString = $rawBicepExampleString -replace '\$\{namePrefix\}[-|\.|_]?', '' # Replacing with empty to not expose prefix and avoid potential deployment conflicts + $rawBicepExampleString = $rawBicepExampleString -replace '(?m):\s*location\s*$', ': ''''' + + # [3/6] Format header, remove scope property & any empty line + $rawBicepExample = $rawBicepExampleString -split '\n' + $rawBicepExample[0] = "module $moduleNameCamelCase './$fullModuleIdentifier/main.bicep' = {" + $rawBicepExample = $rawBicepExample | Where-Object { $_ -notmatch 'scope: *' } | Where-Object { -not [String]::IsNullOrEmpty($_) } + + # [4/6] Extract param block + $rawBicepExampleArray = $rawBicepExample -split '\n' + $moduleDeploymentPropertyIndent = ([regex]::Match($rawBicepExampleArray[1], '^(\s+).*')).Captures.Groups[1].Value.Length + $paramsStartIndex = ($rawBicepExampleArray | Select-String ("^[\s]{$moduleDeploymentPropertyIndent}params:[\s]*\{") | ForEach-Object { $_.LineNumber - 1 })[0] + 1 + if ($rawBicepExampleArray[$paramsStartIndex].Trim() -ne '}') { # Handle case where param block is empty - $formattedBicepExample = $rawBicepExample[0..($paramsStartIndex - 1)] + $rawBicepExample[($paramsEndIndex)..($rawBicepExample.Count)] + $paramsEndIndex = ($rawBicepExampleArray[($paramsStartIndex + 1)..($rawBicepExampleArray.Count)] | Select-String "^[\s]{$moduleDeploymentPropertyIndent}\}" | ForEach-Object { $_.LineNumber - 1 })[0] + $paramsStartIndex + $paramBlock = ($rawBicepExampleArray[$paramsStartIndex..$paramsEndIndex] | Out-String).TrimEnd() } else { - $formattedBicepExample = $rawBicepExample[0..($paramsStartIndex - 1)] + ($bicepExample -split '\n') + $rawBicepExample[($paramsEndIndex + 1)..($rawBicepExample.Count)] + $paramBlock = '' + $paramsEndIndex = $paramsStartIndex } - # Remove any dependsOn as it it test specific - if ($detected = ($formattedBicepExample | Select-String "^\s{$moduleDeploymentPropertyIndent}dependsOn:\s*\[\s*$" | ForEach-Object { $_.LineNumber - 1 })) { - $dependsOnStartIndex = $detected[0] - - # Find out where the 'dependsOn' ends - $dependsOnEndIndex = $dependsOnStartIndex - do { - $dependsOnEndIndex++ - } while ($formattedBicepExample[$dependsOnEndIndex] -notmatch '^\s*\]\s*$') - - # Cut the 'dependsOn' block out - $formattedBicepExample = $formattedBicepExample[0..($dependsOnStartIndex - 1)] + $formattedBicepExample[($dependsOnEndIndex + 1)..($formattedBicepExample.Count)] + # [5/6] Convert Bicep parameter block to JSON parameter block to enable processing + $conversionInputObject = @{ + BicepParamBlock = $paramBlock + CurrentFilePath = $testFilePath } + $paramsInJSONFormat = ConvertTo-FormattedJSONParameterObject @conversionInputObject - # Build result - $SectionContent += @( - '', - '
' - '' - 'via Bicep module' - '' - '```bicep', - ($formattedBicepExample | ForEach-Object { "$_" }).TrimEnd(), - '```', - '', - '
', - '

' - ) - } - - # -------------------- # - # Add JSON example # - # -------------------- # - if ($addJson) { - - # [1/2] Get all parameters from the parameter object and order them recursively - $orderingInputObject = @{ - ParametersJSON = $paramsInJSONFormat | ConvertTo-Json -Depth 99 + # [6/6] Convert JSON parameters back to Bicep and order & format them + $conversionInputObject = @{ + JSONParameters = $paramsInJSONFormat RequiredParametersList = $RequiredParametersList } - $orderedJSONExample = Build-OrderedJSONObject @orderingInputObject - - # [2/2] Create the final content block - $SectionContent += @( - '', - '

' - '' - 'via JSON Parameter file' - '' - '```json', - $orderedJSONExample.Trim() - '```', - '', - '
', - '

' - ) - } - } else { - # ------------------------- # - # Prepare JSON to Bicep # - # ------------------------- # - - $rawContentHashtable = $rawContent | ConvertFrom-Json -Depth 99 -AsHashtable -NoEnumerate - - # First we need to check if we're dealing with classic JSON-Parameter file, or a deployment test file (which contains resource deployments & parameters) - $isParameterFile = $rawContentHashtable.'$schema' -like '*deploymentParameters*' - if (-not $isParameterFile) { - # Case 1: Uses deployment test file (instead of parameter file). - # [1/4] Need to extract parameters. The target is to get an object which 1:1 represents a classic JSON-Parameter file (aside from KeyVault references) - $testResource = $rawContentHashtable.resources | Where-Object { $_.name -like '*-test-*' } - - # [2/4] Build the full ARM-JSON parameter file - $jsonParameterContent = [ordered]@{ - '$schema' = 'https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#' - contentVersion = '1.0.0.0' - parameters = $testResource.properties.parameters - } - $jsonParameterContent = ($jsonParameterContent | ConvertTo-Json -Depth 99).TrimEnd() - - # [3/4] Remove 'externalResourceReferences' that are generated for Bicep's 'existing' resource references. Removing them will make the file more readable - $jsonParameterContentArray = $jsonParameterContent -split '\n' - foreach ($row in ($jsonParameterContentArray | Where-Object { $_ -like '*reference(extensionResourceId*' })) { - if ($row -match '\[.*reference\(extensionResourceId.+\.([a-zA-Z]+)\..*\].*"') { - # e.g. "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', format('{0}-diagnosticDependencies', uniqueString(deployment().name, parameters('location')))), '2020-10-01').outputs.logAnalyticsWorkspaceResourceId.value]" - # e.g. "[format('{0}', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', format('{0}-paramNested', uniqueString(deployment().name, parameters('location')))), '2020-10-01').outputs.managedIdentityResourceId.value)]": {} - $expectedValue = $matches[1] - } elseif ($row -match '\[.*reference\(extensionResourceId.+\.([a-zA-Z]+).*\].*"') { - # e.g. "[reference(extensionResourceId(managementGroup().id, 'Microsoft.Authorization/policySetDefinitions', format('dep-[[namePrefix]]-polSet-{0}', parameters('serviceShort'))), '2021-06-01').policyDefinitions[0].policyDefinitionReferenceId]" - $expectedValue = $matches[1] + $bicepExample = ConvertTo-FormattedBicep @conversionInputObject + + # --------------------- # + # Add Bicep example # + # --------------------- # + if ($addBicep) { + + if ([String]::IsNullOrEmpty($paramBlock)) { + # Handle case where param block is empty + $formattedBicepExample = $rawBicepExample[0..($paramsStartIndex - 1)] + $rawBicepExample[($paramsEndIndex)..($rawBicepExample.Count)] } else { - throw "Unhandled case [$row] in file [$testFilePath]" + $formattedBicepExample = $rawBicepExample[0..($paramsStartIndex - 1)] + ($bicepExample -split '\n') + $rawBicepExample[($paramsEndIndex + 1)..($rawBicepExample.Count)] } - $toReplaceValue = ([regex]::Match($row, '"(\[.+)"')).Captures.Groups[1].Value + # Remove any dependsOn as it it test specific + if ($detected = ($formattedBicepExample | Select-String "^\s{$moduleDeploymentPropertyIndent}dependsOn:\s*\[\s*$" | ForEach-Object { $_.LineNumber - 1 })) { + $dependsOnStartIndex = $detected[0] - $jsonParameterContent = $jsonParameterContent.Replace($toReplaceValue, ('<{0}>' -f $expectedValue)) - } + # Find out where the 'dependsOn' ends + $dependsOnEndIndex = $dependsOnStartIndex + do { + $dependsOnEndIndex++ + } while ($formattedBicepExample[$dependsOnEndIndex] -notmatch '^\s*\]\s*$') - # [4/4] Removing template specific functions - $jsonParameterContentArray = $jsonParameterContent -split '\n' - for ($index = 0; $index -lt $jsonParameterContentArray.Count; $index++) { - if ($jsonParameterContentArray[$index] -match '(\s*"value"): "\[.+\]"') { - # e.g. - # "policyAssignmentId": { - # "value": "[extensionResourceId(managementGroup().id, 'Microsoft.Authorization/policyAssignments', format('dep-[[namePrefix]]-psa-{0}', parameters('serviceShort')))]" - $prefix = $matches[1] - - $headerIndex = $index - while (($jsonParameterContentArray[$headerIndex] -notmatch '.+": (\{|\[)+' -or $jsonParameterContentArray[$headerIndex] -like '*"value"*') -and $headerIndex -gt -1) { - $headerIndex-- - } + # Cut the 'dependsOn' block out + $formattedBicepExample = $formattedBicepExample[0..($dependsOnStartIndex - 1)] + $formattedBicepExample[($dependsOnEndIndex + 1)..($formattedBicepExample.Count)] + } - $value = (($jsonParameterContentArray[$headerIndex] -split ':')[0] -replace '"').Trim() - $jsonParameterContentArray[$index] = ('{0}: "<{1}>"{2}' -f $prefix, $value, ($jsonParameterContentArray[$index].Trim() -like '*,' ? ',' : '')) - } elseif ($jsonParameterContentArray[$index] -match '(\s*)"([\w]+)": "\[.+\]"') { - # e.g. "name": "[format('{0}01', parameters('serviceShort'))]" - $jsonParameterContentArray[$index] = ('{0}"{1}": "<{1}>"{2}' -f $matches[1], $matches[2], ($jsonParameterContentArray[$index].Trim() -like '*,' ? ',' : '')) - } elseif ($jsonParameterContentArray[$index] -match '(\s*)"\[.+\]"') { - # -and $jsonParameterContentArray[$index - 1] -like '*"value"*') { - # e.g. - # "policyDefinitionReferenceIds": { - # "value": [ - # "[reference(subscriptionResourceId('Microsoft.Authorization/policySetDefinitions', format('dep-[[namePrefix]]-polSet-{0}', parameters('serviceShort'))), '2021-06-01').policyDefinitions[0].policyDefinitionReferenceId]" - $prefix = $matches[1] - - $headerIndex = $index - while (($jsonParameterContentArray[$headerIndex] -notmatch '.+": (\{|\[)+' -or $jsonParameterContentArray[$headerIndex] -like '*"value"*') -and $headerIndex -gt -1) { - $headerIndex-- - } + # Build result + $SectionContent += @( + '', + '

' + '' + 'via Bicep module' + '' + '```bicep', + ($formattedBicepExample | ForEach-Object { "$_" }).TrimEnd(), + '```', + '', + '
', + '

' + ) + } - $value = (($jsonParameterContentArray[$headerIndex] -split ':')[0] -replace '"').Trim() + # -------------------- # + # Add JSON example # + # -------------------- # + if ($addJson) { - $jsonParameterContentArray[$index] = ('{0}"<{1}>"{2}' -f $prefix, $value, ($jsonParameterContentArray[$index].Trim() -like '*,' ? ',' : '')) + # [1/2] Get all parameters from the parameter object and order them recursively + $orderingInputObject = @{ + ParametersJSON = $paramsInJSONFormat | ConvertTo-Json -Depth 99 + RequiredParametersList = $RequiredParametersList } + $orderedJSONExample = Build-OrderedJSONObject @orderingInputObject + + # [2/2] Create the final content block + $SectionContent += @( + '', + '

' + '' + 'via JSON Parameter file' + '' + '```json', + $orderedJSONExample.Trim() + '```', + '', + '
', + '

' + ) } - $jsonParameterContent = $jsonParameterContentArray | Out-String } else { - # Case 2: Uses ARM-JSON parameter file - $jsonParameterContent = $rawContent.TrimEnd() - } + # ------------------------- # + # Prepare JSON to Bicep # + # ------------------------- # + + $rawContentHashtable = $rawContent | ConvertFrom-Json -Depth 99 -AsHashtable -NoEnumerate + + # First we need to check if we're dealing with classic JSON-Parameter file, or a deployment test file (which contains resource deployments & parameters) + $isParameterFile = $rawContentHashtable.'$schema' -like '*deploymentParameters*' + if (-not $isParameterFile) { + # Case 1: Uses deployment test file (instead of parameter file). + # [1/4] Need to extract parameters. The target is to get an object which 1:1 represents a classic JSON-Parameter file (aside from KeyVault references) + $testResource = $rawContentHashtable.resources | Where-Object { $_.name -like '*-test-*' } + + # [2/4] Build the full ARM-JSON parameter file + $jsonParameterContent = [ordered]@{ + '$schema' = 'https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#' + contentVersion = '1.0.0.0' + parameters = $testResource.properties.parameters + } + $jsonParameterContent = ($jsonParameterContent | ConvertTo-Json -Depth 99).TrimEnd() + + # [3/4] Remove 'externalResourceReferences' that are generated for Bicep's 'existing' resource references. Removing them will make the file more readable + $jsonParameterContentArray = $jsonParameterContent -split '\n' + foreach ($row in ($jsonParameterContentArray | Where-Object { $_ -like '*reference(extensionResourceId*' })) { + if ($row -match '\[.*reference\(extensionResourceId.+\.([a-zA-Z]+)\..*\].*"') { + # e.g. "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', format('{0}-diagnosticDependencies', uniqueString(deployment().name, parameters('location')))), '2020-10-01').outputs.logAnalyticsWorkspaceResourceId.value]" + # e.g. "[format('{0}', reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, parameters('resourceGroupName')), 'Microsoft.Resources/deployments', format('{0}-paramNested', uniqueString(deployment().name, parameters('location')))), '2020-10-01').outputs.managedIdentityResourceId.value)]": {} + $expectedValue = $matches[1] + } elseif ($row -match '\[.*reference\(extensionResourceId.+\.([a-zA-Z]+).*\].*"') { + # e.g. "[reference(extensionResourceId(managementGroup().id, 'Microsoft.Authorization/policySetDefinitions', format('dep-[[namePrefix]]-polSet-{0}', parameters('serviceShort'))), '2021-06-01').policyDefinitions[0].policyDefinitionReferenceId]" + $expectedValue = $matches[1] + } else { + throw "Unhandled case [$row] in file [$testFilePath]" + } + + $toReplaceValue = ([regex]::Match($row, '"(\[.+)"')).Captures.Groups[1].Value - # --------------------- # - # Add Bicep example # - # --------------------- # - if ($addBicep) { - - # [1/5] Get all parameters from the parameter object - $JSONParametersHashTable = (ConvertFrom-Json $jsonParameterContent -AsHashtable -Depth 99).parameters - - # [2/5] Handle the special case of Key Vault secret references (that have a 'reference' instead of a 'value' property) - # [2.1] Find all references and split them into managable objects - $keyVaultReferences = $JSONParametersHashTable.Keys | Where-Object { $JSONParametersHashTable[$_].Keys -contains 'reference' } - - if ($keyVaultReferences.Count -gt 0) { - $keyVaultReferenceData = @() - foreach ($reference in $keyVaultReferences) { - $resourceIdElem = $JSONParametersHashTable[$reference].reference.keyVault.id -split '/' - $keyVaultReferenceData += @{ - subscriptionId = $resourceIdElem[2] - resourceGroupName = $resourceIdElem[4] - vaultName = $resourceIdElem[-1] - secretName = $JSONParametersHashTable[$reference].reference.secretName - parameterName = $reference + $jsonParameterContent = $jsonParameterContent.Replace($toReplaceValue, ('<{0}>' -f $expectedValue)) + } + + # [4/4] Removing template specific functions + $jsonParameterContentArray = $jsonParameterContent -split '\n' + for ($index = 0; $index -lt $jsonParameterContentArray.Count; $index++) { + if ($jsonParameterContentArray[$index] -match '(\s*"value"): "\[.+\]"') { + # e.g. + # "policyAssignmentId": { + # "value": "[extensionResourceId(managementGroup().id, 'Microsoft.Authorization/policyAssignments', format('dep-[[namePrefix]]-psa-{0}', parameters('serviceShort')))]" + $prefix = $matches[1] + + $headerIndex = $index + while (($jsonParameterContentArray[$headerIndex] -notmatch '.+": (\{|\[)+' -or $jsonParameterContentArray[$headerIndex] -like '*"value"*') -and $headerIndex -gt -1) { + $headerIndex-- + } + + $value = (($jsonParameterContentArray[$headerIndex] -split ':')[0] -replace '"').Trim() + $jsonParameterContentArray[$index] = ('{0}: "<{1}>"{2}' -f $prefix, $value, ($jsonParameterContentArray[$index].Trim() -like '*,' ? ',' : '')) + } elseif ($jsonParameterContentArray[$index] -match '(\s*)"([\w]+)": "\[.+\]"') { + # e.g. "name": "[format('{0}01', parameters('serviceShort'))]" + $jsonParameterContentArray[$index] = ('{0}"{1}": "<{1}>"{2}' -f $matches[1], $matches[2], ($jsonParameterContentArray[$index].Trim() -like '*,' ? ',' : '')) + } elseif ($jsonParameterContentArray[$index] -match '(\s*)"\[.+\]"') { + # -and $jsonParameterContentArray[$index - 1] -like '*"value"*') { + # e.g. + # "policyDefinitionReferenceIds": { + # "value": [ + # "[reference(subscriptionResourceId('Microsoft.Authorization/policySetDefinitions', format('dep-[[namePrefix]]-polSet-{0}', parameters('serviceShort'))), '2021-06-01').policyDefinitions[0].policyDefinitionReferenceId]" + $prefix = $matches[1] + + $headerIndex = $index + while (($jsonParameterContentArray[$headerIndex] -notmatch '.+": (\{|\[)+' -or $jsonParameterContentArray[$headerIndex] -like '*"value"*') -and $headerIndex -gt -1) { + $headerIndex-- + } + + $value = (($jsonParameterContentArray[$headerIndex] -split ':')[0] -replace '"').Trim() + + $jsonParameterContentArray[$index] = ('{0}"<{1}>"{2}' -f $prefix, $value, ($jsonParameterContentArray[$index].Trim() -like '*,' ? ',' : '')) } } + $jsonParameterContent = $jsonParameterContentArray | Out-String + } else { + # Case 2: Uses ARM-JSON parameter file + $jsonParameterContent = $rawContent.TrimEnd() } - # [2.2] Remove any duplicates from the referenced key vaults and build 'existing' Key Vault references in Bicep format from them. - # Also, add a link to the corresponding Key Vault 'resource' to each identified Key Vault secret reference - $extendedKeyVaultReferences = @() - $counter = 0 - foreach ($reference in ($keyVaultReferenceData | Sort-Object -Property 'vaultName' -Unique)) { - $counter++ - $extendedKeyVaultReferences += @( - "resource kv$counter 'Microsoft.KeyVault/vaults@2019-09-01' existing = {", + # --------------------- # + # Add Bicep example # + # --------------------- # + if ($addBicep) { + + # [1/5] Get all parameters from the parameter object + $JSONParametersHashTable = (ConvertFrom-Json $jsonParameterContent -AsHashtable -Depth 99).parameters + + # [2/5] Handle the special case of Key Vault secret references (that have a 'reference' instead of a 'value' property) + # [2.1] Find all references and split them into managable objects + $keyVaultReferences = $JSONParametersHashTable.Keys | Where-Object { $JSONParametersHashTable[$_].Keys -contains 'reference' } + + if ($keyVaultReferences.Count -gt 0) { + $keyVaultReferenceData = @() + foreach ($reference in $keyVaultReferences) { + $resourceIdElem = $JSONParametersHashTable[$reference].reference.keyVault.id -split '/' + $keyVaultReferenceData += @{ + subscriptionId = $resourceIdElem[2] + resourceGroupName = $resourceIdElem[4] + vaultName = $resourceIdElem[-1] + secretName = $JSONParametersHashTable[$reference].reference.secretName + parameterName = $reference + } + } + } + + # [2.2] Remove any duplicates from the referenced key vaults and build 'existing' Key Vault references in Bicep format from them. + # Also, add a link to the corresponding Key Vault 'resource' to each identified Key Vault secret reference + $extendedKeyVaultReferences = @() + $counter = 0 + foreach ($reference in ($keyVaultReferenceData | Sort-Object -Property 'vaultName' -Unique)) { + $counter++ + $extendedKeyVaultReferences += @( + "resource kv$counter 'Microsoft.KeyVault/vaults@2019-09-01' existing = {", (" name: '{0}'" -f $reference.vaultName), (" scope: resourceGroup('{0}','{1}')" -f $reference.subscriptionId, $reference.resourceGroupName), - '}', - '' - ) + '}', + '' + ) - # Add attribute for later correct reference - $keyVaultReferenceData | Where-Object { $_.vaultName -eq $reference.vaultName } | ForEach-Object { - $_['vaultResourceReference'] = "kv$counter" + # Add attribute for later correct reference + $keyVaultReferenceData | Where-Object { $_.vaultName -eq $reference.vaultName } | ForEach-Object { + $_['vaultResourceReference'] = "kv$counter" + } } - } - # [3/5] Replace all 'references' with the link to one of the 'existing' Key Vault resources - foreach ($parameterName in ($JSONParametersHashTable.Keys | Where-Object { $JSONParametersHashTable[$_].Keys -contains 'reference' })) { - $matchingTuple = $keyVaultReferenceData | Where-Object { $_.parameterName -eq $parameterName } - $JSONParametersHashTable[$parameterName] = "{0}.getSecret('{1}')" -f $matchingTuple.vaultResourceReference, $matchingTuple.secretName - } + # [3/5] Replace all 'references' with the link to one of the 'existing' Key Vault resources + foreach ($parameterName in ($JSONParametersHashTable.Keys | Where-Object { $JSONParametersHashTable[$_].Keys -contains 'reference' })) { + $matchingTuple = $keyVaultReferenceData | Where-Object { $_.parameterName -eq $parameterName } + $JSONParametersHashTable[$parameterName] = "{0}.getSecret('{1}')" -f $matchingTuple.vaultResourceReference, $matchingTuple.secretName + } - # [4/5] Convert the JSON parameters to a Bicep parameters block - $conversionInputObject = @{ - JSONParameters = $JSONParametersHashTable - RequiredParametersList = $null -ne $RequiredParametersList ? $RequiredParametersList : @() + # [4/5] Convert the JSON parameters to a Bicep parameters block + $conversionInputObject = @{ + JSONParameters = $JSONParametersHashTable + RequiredParametersList = $null -ne $RequiredParametersList ? $RequiredParametersList : @() + } + $bicepExample = ConvertTo-FormattedBicep @conversionInputObject + + # [5/5] Create the final content block: That means + # - the 'existing' Key Vault resources + # - a 'module' header that mimics a module deployment + # - all parameters in Bicep format + $SectionContent += @( + '', + '

' + '' + 'via Bicep module' + '' + '```bicep', + $extendedKeyVaultReferences, + "module $moduleNameCamelCase 'ts/modules:$(($FullModuleIdentifier -replace '\\|\/', '.').ToLower()):1.0.0 = {" + " name: '`${uniqueString(deployment().name)}-$moduleNamePascalCase'" + ' params: {' + $bicepExample.TrimEnd(), + ' }' + '}' + '```', + '', + '
' + '

' + ) } - $bicepExample = ConvertTo-FormattedBicep @conversionInputObject - - # [5/5] Create the final content block: That means - # - the 'existing' Key Vault resources - # - a 'module' header that mimics a module deployment - # - all parameters in Bicep format - $SectionContent += @( - '', - '

' - '' - 'via Bicep module' - '' - '```bicep', - $extendedKeyVaultReferences, - "module $moduleNameCamelCase 'ts/modules:$(($FullModuleIdentifier -replace '\\|\/', '.').ToLower()):1.0.0 = {" - " name: '`${uniqueString(deployment().name)}-$moduleNamePascalCase'" - ' params: {' - $bicepExample.TrimEnd(), - ' }' - '}' - '```', - '', - '
' - '

' - ) - } - # -------------------- # - # Add JSON example # - # -------------------- # - if ($addJson) { + # -------------------- # + # Add JSON example # + # -------------------- # + if ($addJson) { - # [1/2] Get all parameters from the parameter object and order them recursively - $orderingInputObject = @{ - ParametersJSON = (($jsonParameterContent | ConvertFrom-Json).parameters | ConvertTo-Json -Depth 99) - RequiredParametersList = $null -ne $RequiredParametersList ? $RequiredParametersList : @() + # [1/2] Get all parameters from the parameter object and order them recursively + $orderingInputObject = @{ + ParametersJSON = (($jsonParameterContent | ConvertFrom-Json).parameters | ConvertTo-Json -Depth 99) + RequiredParametersList = $null -ne $RequiredParametersList ? $RequiredParametersList : @() + } + $orderedJSONExample = Build-OrderedJSONObject @orderingInputObject + + # [2/2] Create the final content block + $SectionContent += @( + '', + '

', + '', + 'via JSON Parameter file', + '', + '```json', + $orderedJSONExample.TrimEnd(), + '```', + '', + '
' + '

' + ) } - $orderedJSONExample = Build-OrderedJSONObject @orderingInputObject - - # [2/2] Create the final content block - $SectionContent += @( - '', - '

', - '', - 'via JSON Parameter file', - '', - '```json', - $orderedJSONExample.TrimEnd(), - '```', - '', - '
' - '

' - ) } } + + $SectionContent += @( '' )