diff --git a/.github/actions/templates/avm-validateModulePester/action.yml b/.github/actions/templates/avm-validateModulePester/action.yml index 83c5e7b97a..e2a613a0a4 100644 --- a/.github/actions/templates/avm-validateModulePester/action.yml +++ b/.github/actions/templates/avm-validateModulePester/action.yml @@ -10,34 +10,34 @@ ## ACTION PARAMETERS ## ##-------------------------------------------## ## -## |==================================================================================================================================================| -## | Parameter | Required | Default | Description | Example | -## |--------------------------|----------|---------|--------------------------------------|-----------------------------------------------------------| -## | modulePath | true | '' | The path to the module's folder | 'modules/api-management/service' | -## | moduleTestFilePath | true | '' | The path to the module Pester tests. | 'utilities/pipelines/staticValidation/module.tests.ps1' | -## |==================================================================================================================================================| +## |===========================================================================================================================================================| +## | Parameter | Required | Default | Description | Example | +## |--------------------------|----------|---------|--------------------------------------|--------------------------------------------------------------------| +## | modulePath | true | '' | The path to the module's folder | 'modules/api-management/service' | +## | moduleTestFilePath | true | '' | The path to the module Pester tests. | 'utilities/pipelines/staticValidation/compliance/module.tests.ps1' | +## |===========================================================================================================================================================| ## ##---------------------------------------------## -name: 'Execute Pester module tests' -description: 'Execute Pester module tests (if any)' +name: "Execute Pester module tests" +description: "Execute Pester module tests (if any)" inputs: modulePath: description: "The path to the module's folder" required: true - default: '' + default: "" moduleTestFilePath: - description: 'The path to the test file' + description: "The path to the test file" required: true - default: '' + default: "avm/utilities/pipelines/staticValidation/compliance/module.tests.ps1" runs: - using: 'composite' + using: "composite" steps: # [Module Pester Test] task(s) #----------------------------- - - name: 'Run Pester tests' + - name: "Run Pester tests" id: pester_run_step shell: pwsh run: | @@ -45,7 +45,7 @@ runs: Write-Output '::group::Run Pester tests' # Load used functions - . (Join-Path $env:GITHUB_WORKSPACE 'avm' 'utilities' 'pipelines' 'staticValidation' 'Set-PesterGitHubOutput.ps1') + . (Join-Path $env:GITHUB_WORKSPACE 'avm' 'utilities' 'pipelines' 'staticValidation' 'compliance' 'Set-PesterGitHubOutput.ps1') # Set repo root path $repoRootPath = $env:GITHUB_WORKSPACE @@ -86,7 +86,7 @@ runs: $pesterConfiguration = @{ Run = @{ Container = New-PesterContainer -Path (Join-Path $repoRootPath $moduleTestFilePath) -Data @{ - moduleFolderPaths = $moduleFolderPaths + moduleFolderPaths = $moduleFolderPaths # tokenConfiguration = $tokenConfiguration # allowPreviewVersionsInAPITests = [System.Convert]::ToBoolean('${{ env.allowPreviewVersionsInAPITests }}') } @@ -120,7 +120,7 @@ runs: Write-Output ('{0}={1}' -f 'formattedPesterResultsPath', $functionInput.outputFilePath) >> $env:GITHUB_OUTPUT - - name: 'Output to GitHub job summaries' + - name: "Output to GitHub job summaries" if: always() shell: pwsh run: | diff --git a/.github/workflows/avm.template.module.yml b/.github/workflows/avm.template.module.yml index 59283eea51..26ffced333 100644 --- a/.github/workflows/avm.template.module.yml +++ b/.github/workflows/avm.template.module.yml @@ -43,7 +43,6 @@ jobs: uses: ./.github/actions/templates/avm-validateModulePester with: modulePath: '${{ inputs.modulePath }}' - moduleTestFilePath: '${{ env.moduleTestFilePath }}' ######################### # PSRule validation # diff --git a/avm/res/cognitive-services/account/main.bicep b/avm/res/cognitive-services/account/main.bicep index d6545080cd..033fa5bdf1 100644 --- a/avm/res/cognitive-services/account/main.bicep +++ b/avm/res/cognitive-services/account/main.bicep @@ -113,7 +113,7 @@ param restrictOutboundNetworkAccess bool = true @description('Optional. The storage accounts for this resource.') param userOwnedStorage array = [] -@description('Optional. The managed identity definition for this resource') +@description('Optional. The managed identity definition for this resource.') param managedIdentities managedIdentitiesType @description('Optional. Enable/Disable usage telemetry for module.') diff --git a/avm/res/cognitive-services/account/main.json b/avm/res/cognitive-services/account/main.json index f56d28b94e..534bfbbd3b 100644 --- a/avm/res/cognitive-services/account/main.json +++ b/avm/res/cognitive-services/account/main.json @@ -6,7 +6,7 @@ "_generator": { "name": "bicep", "version": "0.21.1.54444", - "templateHash": "2011438504132117881" + "templateHash": "12649951839001498344" }, "name": "Cognitive Services", "description": "This module deploys a Cognitive Service.", @@ -390,6 +390,31 @@ } }, "nullable": true + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + } + }, + "nullable": true } }, "parameters": { @@ -501,15 +526,9 @@ } }, "lock": { - "type": "string", - "defaultValue": "", - "allowedValues": [ - "", - "CanNotDelete", - "ReadOnly" - ], + "$ref": "#/definitions/lockType", "metadata": { - "description": "Optional. Specify the type of lock." + "description": "Optional. The lock settings of the service." } }, "roleAssignments": { @@ -590,7 +609,7 @@ "managedIdentities": { "$ref": "#/definitions/managedIdentitiesType", "metadata": { - "description": "Optional. The managed identity definition for this resource" + "description": "Optional. The managed identity definition for this resource." } }, "enableTelemetry": { @@ -720,14 +739,14 @@ ] }, "cognitiveService_lock": { - "condition": "[not(empty(parameters('lock')))]", + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", "type": "Microsoft.Authorization/locks", "apiVersion": "2020-05-01", "scope": "[format('Microsoft.CognitiveServices/accounts/{0}', parameters('name'))]", - "name": "[format('{0}-{1}-lock', parameters('name'), parameters('lock'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", "properties": { - "level": "[parameters('lock')]", - "notes": "[if(equals(parameters('lock'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot modify the resource or child resources.')]" + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.')]" }, "dependsOn": [ "cognitiveService" @@ -848,7 +867,7 @@ "_generator": { "name": "bicep", "version": "0.21.1.54444", - "templateHash": "15547486634180132508" + "templateHash": "13438182834931170603" }, "name": "Private Endpoints", "description": "This module deploys a Private Endpoint.", @@ -1158,7 +1177,7 @@ "condition": "[not(empty(parameters('privateDnsZoneResourceIds')))]", "type": "Microsoft.Resources/deployments", "apiVersion": "2022-09-01", - "name": "[format('{0}-PrivateEndpoint-PrivateDnsZoneGroup', uniqueString(deployment().name, parameters('location')))]", + "name": "[format('{0}-PrivateEndpoint-PrivateDnsZoneGroup', uniqueString(deployment().name))]", "properties": { "expressionEvaluationOptions": { "scope": "inner" @@ -1328,7 +1347,7 @@ }, "value": "[reference('cognitiveService').endpoint]" }, - "systemAssignedPrincipalId": { + "systemAssignedMIPrincipalId": { "type": "string", "metadata": { "description": "The principal ID of the system assigned identity." diff --git a/avm/res/key-vault/vault/main.json b/avm/res/key-vault/vault/main.json index 551f810129..9d89eba1f2 100644 --- a/avm/res/key-vault/vault/main.json +++ b/avm/res/key-vault/vault/main.json @@ -6,7 +6,7 @@ "_generator": { "name": "bicep", "version": "0.21.1.54444", - "templateHash": "6314634577812331017" + "templateHash": "52641334487503796" }, "name": "Key Vaults", "description": "This module deploys a Key Vault.", @@ -290,22 +290,14 @@ } }, "lock": { - "type": "string", - "allowedValues": [ - "", - "CanNotDelete", - "ReadOnly" - ], + "$ref": "#/definitions/lockType", "nullable": true, "metadata": { "description": "Optional. Specify the type of lock." } }, "roleAssignments": { - "type": "array", - "items": { - "$ref": "#/definitions/roleAssignmentType" - }, + "$ref": "#/definitions/roleAssignmentType", "nullable": true, "metadata": { "description": "Optional. 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'." @@ -335,6 +327,31 @@ } }, "nullable": true + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + } + }, + "nullable": true } }, "parameters": { @@ -460,15 +477,9 @@ } }, "lock": { - "type": "string", - "defaultValue": "", - "allowedValues": [ - "", - "CanNotDelete", - "ReadOnly" - ], + "$ref": "#/definitions/lockType", "metadata": { - "description": "Optional. Specify the type of lock." + "description": "Optional. The lock settings of the service." } }, "roleAssignments": { @@ -583,14 +594,14 @@ } }, "keyVault_lock": { - "condition": "[not(empty(parameters('lock')))]", + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", "type": "Microsoft.Authorization/locks", "apiVersion": "2020-05-01", "scope": "[format('Microsoft.KeyVault/vaults/{0}', parameters('name'))]", - "name": "[format('{0}-{1}-lock', parameters('name'), parameters('lock'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", "properties": { - "level": "[parameters('lock')]", - "notes": "[if(equals(parameters('lock'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot modify the resource or child resources.')]" + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.')]" }, "dependsOn": [ "keyVault" @@ -1393,7 +1404,7 @@ "_generator": { "name": "bicep", "version": "0.21.1.54444", - "templateHash": "13250116040740648439" + "templateHash": "13438182834931170603" }, "name": "Private Endpoints", "description": "This module deploys a Private Endpoint.", @@ -1465,6 +1476,31 @@ } }, "nullable": true + }, + "lockType": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "metadata": { + "description": "Optional. Specify the name of lock." + } + }, + "kind": { + "type": "string", + "allowedValues": [ + "CanNotDelete", + "None", + "ReadOnly" + ], + "nullable": true, + "metadata": { + "description": "Optional. Specify the type of lock." + } + } + }, + "nullable": true } }, "parameters": { @@ -1528,15 +1564,9 @@ } }, "lock": { - "type": "string", - "defaultValue": "", - "allowedValues": [ - "", - "CanNotDelete", - "ReadOnly" - ], + "$ref": "#/definitions/lockType", "metadata": { - "description": "Optional. Specify the type of lock." + "description": "Optional. The lock settings of the service." } }, "roleAssignments": { @@ -1587,7 +1617,7 @@ "Reader": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]", "Role Based Access Control Administrator (Preview)": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'f58310d9-a9f6-439a-9e8d-f62e7b41a168')]" }, - "moduleVersion": "#_moduleVersion_#" + "moduleVersion": "1.0.0" }, "resources": { "avmTelemetry": { @@ -1645,14 +1675,14 @@ } }, "privateEndpoint_lock": { - "condition": "[not(empty(parameters('lock')))]", + "condition": "[and(not(empty(coalesce(parameters('lock'), createObject()))), not(equals(tryGet(parameters('lock'), 'kind'), 'None')))]", "type": "Microsoft.Authorization/locks", "apiVersion": "2020-05-01", "scope": "[format('Microsoft.Network/privateEndpoints/{0}', parameters('name'))]", - "name": "[format('{0}-{1}-lock', parameters('name'), parameters('lock'))]", + "name": "[coalesce(tryGet(parameters('lock'), 'name'), format('lock-{0}', parameters('name')))]", "properties": { - "level": "[parameters('lock')]", - "notes": "[if(equals(parameters('lock'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot modify the resource or child resources.')]" + "level": "[coalesce(tryGet(parameters('lock'), 'kind'), '')]", + "notes": "[if(equals(tryGet(parameters('lock'), 'kind'), 'CanNotDelete'), 'Cannot delete resource or child resources.', 'Cannot delete or modify the resource or child resources.')]" }, "dependsOn": [ "privateEndpoint" @@ -1684,7 +1714,7 @@ "condition": "[not(empty(parameters('privateDnsZoneResourceIds')))]", "type": "Microsoft.Resources/deployments", "apiVersion": "2022-09-01", - "name": "[format('{0}-PrivateEndpoint-PrivateDnsZoneGroup', uniqueString(deployment().name, parameters('location')))]", + "name": "[format('{0}-PrivateEndpoint-PrivateDnsZoneGroup', uniqueString(deployment().name))]", "properties": { "expressionEvaluationOptions": { "scope": "inner" diff --git a/avm/utilities/pipelines/staticValidation/Set-PesterGitHubOutput.ps1 b/avm/utilities/pipelines/staticValidation/compliance/Set-PesterGitHubOutput.ps1 similarity index 100% rename from avm/utilities/pipelines/staticValidation/Set-PesterGitHubOutput.ps1 rename to avm/utilities/pipelines/staticValidation/compliance/Set-PesterGitHubOutput.ps1 diff --git a/avm/utilities/pipelines/staticValidation/helper/helper.psm1 b/avm/utilities/pipelines/staticValidation/compliance/helper/helper.psm1 similarity index 100% rename from avm/utilities/pipelines/staticValidation/helper/helper.psm1 rename to avm/utilities/pipelines/staticValidation/compliance/helper/helper.psm1 diff --git a/avm/utilities/pipelines/staticValidation/module.tests.ps1 b/avm/utilities/pipelines/staticValidation/compliance/module.tests.ps1 similarity index 95% rename from avm/utilities/pipelines/staticValidation/module.tests.ps1 rename to avm/utilities/pipelines/staticValidation/compliance/module.tests.ps1 index 1121edaceb..0163e55c61 100644 --- a/avm/utilities/pipelines/staticValidation/module.tests.ps1 +++ b/avm/utilities/pipelines/staticValidation/compliance/module.tests.ps1 @@ -678,17 +678,33 @@ Describe 'Module tests' -Tag 'Module' { } } - It '[] Resource name output should exist.' -TestCases $deploymentFolderTestCases { + It '[] Resource modules should have a name output.' -TestCases $deploymentFolderTestCases { param( [string] $moduleFolderName, [hashtable] $templateContent, - $templateFilePath + [string] $readMeFilePath ) - # check if module contains a 'primary' resource we could draw a name from - $moduleResourceType = (Split-Path (($templateFilePath -replace '\\', '/') -split '/avm/')[1] -Parent) -replace '\\', '/' - if ($templateContent.resources.type -notcontains $moduleResourceType) { + # We're fetching the primary resource type from the first line of the readme + $readMeFileContentHeader = (Get-Content -Path $readMeFilePath)[0] + if ($readMeFileContentHeader -match '^.*`\[(.+)\]`.*') { + $primaryResourceType = $matches[1] + } + else { + Write-Error "Cannot identity primary resource type in readme header [$readMeFileContentHeader] and cannot execute the test." + return + } + + # With the introduction of user defined types, the way resources are configured in the schema slightly changed. We have to account for that. + if ($templateContent.resources.GetType().Name -eq 'Object[]') { + $templateResources = $templateContent.resources + } + else { + $templateResources = $templateContent.resources.Keys | ForEach-Object { $templateContent.resources[$_] } + } + + if ($templateResources.type -notcontains $primaryResourceType) { Set-ItResult -Skipped -Because 'the module template has no primary resource to fetch a name from.' return } @@ -698,12 +714,11 @@ Describe 'Module tests' -Tag 'Module' { $outputs | Should -Contain 'name' } - It '[] Resource ID output should exist.' -TestCases $deploymentFolderTestCases { + It '[] Resource modules should have a Resource ID output.' -TestCases $deploymentFolderTestCases { param( [string] $moduleFolderName, [hashtable] $templateContent, - [string] $templateFilePath, [string] $readMeFilePath ) @@ -717,8 +732,16 @@ Describe 'Module tests' -Tag 'Module' { return } + # With the introduction of user defined types, the way resources are configured in the schema slightly changed. We have to account for that. + if ($templateContent.resources.GetType().Name -eq 'Object[]') { + $templateResources = $templateContent.resources + } + else { + $templateResources = $templateContent.resources.Keys | ForEach-Object { $templateContent.resources[$_] } + } + # check if module contains a 'primary' resource we could draw a resource ID from - if ($templateContent.resources.type -notcontains $primaryResourceType) { + if ($templateResources.type -notcontains $primaryResourceType) { Set-ItResult -Skipped -Because 'the module template has no primary resource to fetch a resource ID from.' return } @@ -728,7 +751,7 @@ Describe 'Module tests' -Tag 'Module' { $outputs | Should -Contain 'resourceId' } - It '[] Principal ID output should exist, if supported.' -TestCases $deploymentFolderTestCases { + It '[] Resource modules Principal ID output should exist, if supported.' -TestCases $deploymentFolderTestCases { param( [string] $moduleFolderName, @@ -741,6 +764,12 @@ Describe 'Module tests' -Tag 'Module' { return } + $typeRef = Split-Path $templateContent.parameters.managedIdentities.'$ref' -Leaf + $typeProperties = ($templateContent.definitions[$typeRef]).properties + if ($typeProperties.Keys -notcontains 'systemAssigned') { + Set-ItResult -Skipped -Because 'the managedIdentities input does not support system-assigned identities.' + return + } # Otherwise test for standard outputs $outputs = $templateContent.outputs.Keys