From 7f1680528daa2ce6a1c5f16df3632df95292dc54 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Tue, 19 Jan 2021 11:37:23 +1000 Subject: [PATCH 1/2] Outputs are added to generated document #28 --- .vscode/tasks.json | 11 + CHANGELOG.md | 3 + README.md | 12 +- pipeline.build.ps1 | 10 + src/PSDocs.Azure/docs/Azure.Template.Doc.ps1 | 49 +++- src/PSDocs.Azure/en/PSDocs-strings.psd1 | 4 + templates/storage/v1/README.md | 11 +- templates/storage/v1/template.json | 20 +- .../PSDocs.Azure.Tests/Azure.Common.Tests.ps1 | 10 + tests/PSDocs.Azure.Tests/basic.template.json | 262 ++++++++++++++++++ 10 files changed, 383 insertions(+), 9 deletions(-) create mode 100644 tests/PSDocs.Azure.Tests/basic.template.json diff --git a/.vscode/tasks.json b/.vscode/tasks.json index c1fe2dae..e4391ad6 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -39,6 +39,17 @@ "detail": "Clean output directories.", "command": "Invoke-Build Clean", "problemMatcher": [] + }, + { + "label": "Update template docs", + "detail": "Generate template markdown docs.", + "type": "shell", + "command": "Invoke-Build UpdateTemplateDocs", + "problemMatcher": [], + "presentation": { + "clear": true, + "panel": "dedicated" + } } ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index fe5e187c..765df4a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,11 @@ What's changed since v0.1.0: +- New features: + - Template outputs are added to generated document. [#28](https://github.com/Azure/PSDocs.Azure/issues/28) - Bug fixes: - Fixed snippet with short relative template causes exception. [#26](https://github.com/Azure/PSDocs.Azure/issues/26) + - Fixed cannot bind argument when metadata name is not set. [#35](https://github.com/Azure/PSDocs.Azure/issues/35) ## v0.1.0 diff --git a/README.md b/README.md index 8ec5c902..0d1fa355 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,16 @@ For example: } }, "resources": [ - ] + ], + "outputs": { + "resourceId": { + "type": "string", + "value": "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]", + "metadata": { + "description": "A unique resource identifier for the storage account." + } + } + } } ``` @@ -108,6 +117,7 @@ Field | Scope | Type | Description `description` | Parameter | `string` | Used as the description for the parameter. `example` | Parameter | `string`, `boolean`, `object`, or `array` | An example use of the parameter. The example is included in the JSON snippet. If an example is not included the default value is used instead. `ignore` | Parameter | `boolean` | When `true` the parameter is not included in the JSON snippet. +`description` | Output | `string` | Used as the description for the output. An example of an Azure Storage Account template with metadata included is available [here](templates/storage/v1/template.json). diff --git a/pipeline.build.ps1 b/pipeline.build.ps1 index f6ac60a5..bfd6290f 100644 --- a/pipeline.build.ps1 +++ b/pipeline.build.ps1 @@ -272,6 +272,16 @@ task TestModule ModuleDependencies, Pester, PSScriptAnalyzer, { } } +task UpdateTemplateDocs Build, { + Import-Module (Join-Path -Path $PWD -ChildPath 'out/modules/PSDocs.Azure') + + # Scan for Azure template file recursively in the templates/ directory + Get-AzDocTemplateFile -Path templates/ | ForEach-Object { + $template = Get-Item -Path $_.TemplateFile; + Invoke-PSDocument -Module PSDocs.Azure -OutputPath $template.Directory.FullName -InputObject $template.FullName; + } +} + # Synopsis: Run script analyzer task Analyze Build, PSScriptAnalyzer, { Invoke-ScriptAnalyzer -Path out/modules/PSDocs.Azure; diff --git a/src/PSDocs.Azure/docs/Azure.Template.Doc.ps1 b/src/PSDocs.Azure/docs/Azure.Template.Doc.ps1 index a4daf774..8ef1d95b 100644 --- a/src/PSDocs.Azure/docs/Azure.Template.Doc.ps1 +++ b/src/PSDocs.Azure/docs/Azure.Template.Doc.ps1 @@ -106,7 +106,33 @@ function global:GetTemplateMetadata { [String]$Path ) process { - return (Get-Content -Path $Path -Raw | ConvertFrom-Json).metadata; + $template = Get-Content -Path $Path -Raw | ConvertFrom-Json; + if ([bool]$template.PSObject.Properties['metadata']) { + return $template.metadata; + } + } +} + +# A function to import outputs +function global:GetTemplateOutput { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $True)] + [String]$Path + ) + process { + $template = Get-Content -Path $Path -Raw | ConvertFrom-Json; + foreach ($property in $template.outputs.PSObject.Properties) { + $output = [PSCustomObject]@{ + Name = $property.Name + Type = $property.Value.type + Description = '' + } + if ([bool]$property.Value.PSObject.Properties['metadata'] -and [bool]$property.Value.metadata.PSObject.Properties['description']) { + $output.Description = $property.Value.metadata.description + } + $output; + } } } @@ -117,16 +143,23 @@ Document 'README' { $templatePath = $InputObject; $parameters = GetTemplateParameter -Path $templatePath; $metadata = GetTemplateMetadata -Path $templatePath; + $outputs = GetTemplateOutput -Path $templatePath; # Set document title - Title $metadata.name + if ($Null -ne $metadata -and [bool]$metadata.PSObject.Properties['name']) { + Title $metadata.name + } + else { + Title $LocalizedData.DefaultTitle + } # Write opening line $metadata.Description - # Add each parameter to a table + # Add table and detail for each parameter Section $LocalizedData.Parameters { - $parameters | Table -Property @{ Name = $LocalizedData.ParameterName; Expression = { $_.Name }}, $LocalizedData.Description + $parameters | Table -Property @{ Name = $LocalizedData.ParameterName; Expression = { $_.Name }}, + @{ Name = $LocalizedData.Description; Expression = { $_.Description }} foreach ($parameter in $parameters) { Section $parameter.Name { @@ -145,6 +178,14 @@ Document 'README' { } } + # Add table for outputs + Section $LocalizedData.Outputs { + $outputs | Table -Property @{ Name = $LocalizedData.Name; Expression = { $_.Name }}, + @{ Name = $LocalizedData.Type; Expression = { $_.Type }}, + @{ Name = $LocalizedData.Description; Expression = { $_.Description }} + } + + # Insert snippet $example = GetTemplateExample -Path $templatePath; Section $LocalizedData.Snippets { $example | Code 'json' diff --git a/src/PSDocs.Azure/en/PSDocs-strings.psd1 b/src/PSDocs.Azure/en/PSDocs-strings.psd1 index 974cd9d1..93f2e4fb 100644 --- a/src/PSDocs.Azure/en/PSDocs-strings.psd1 +++ b/src/PSDocs.Azure/en/PSDocs-strings.psd1 @@ -4,8 +4,12 @@ @{ Parameters = 'Parameters' Snippets = 'Snippets' + Outputs = 'Outputs' DefaultValue = "- Default value: {0}" AllowedValues = "- Allowed values: {0}" ParameterName = 'Parameter name' Description = 'Description' + Type = 'Type' + Name = 'Name' + DefaultTitle = 'Azure template' } diff --git a/templates/storage/v1/README.md b/templates/storage/v1/README.md index c56da257..c2db8690 100644 --- a/templates/storage/v1/README.md +++ b/templates/storage/v1/README.md @@ -8,7 +8,7 @@ Parameter name | Description -------------- | ----------- storageAccountName | Required. The name of the Storage Account. location | Optional. The Azure region to deploy to. -sku | Optional. Crease the Storage Account as LRS or GRS. +sku | Optional. Create the Storage Account as LRS or GRS. suffixLength | Optional. Determine how many additional characters are added to the storage account name as a suffix. containers | Optional. An array of storage containers to create on the storage account. lifecycleRules | Optional. An array of lifecycle management policies for the storage account. @@ -33,7 +33,7 @@ Optional. The Azure region to deploy to. ### sku -Optional. Crease the Storage Account as LRS or GRS. +Optional. Create the Storage Account as LRS or GRS. - Default value: `Standard_LRS` @@ -95,6 +95,13 @@ Optional. Set to the objectId of Azure Key Vault to delegated permission for use Optional. Tags to apply to the resource. +## Outputs + +Name | Type | Description +---- | ---- | ----------- +blobEndpoint | string | A URI to the blob storage endpoint. +resourceId | string | A unique resource identifier for the storage account. + ## Snippets ```json diff --git a/templates/storage/v1/template.json b/templates/storage/v1/template.json index 574cc3f2..8173a2c5 100644 --- a/templates/storage/v1/template.json +++ b/templates/storage/v1/template.json @@ -29,7 +29,7 @@ "Standard_GRS" ], "metadata": { - "description": "Optional. Crease the Storage Account as LRS or GRS." + "description": "Optional. Create the Storage Account as LRS or GRS." } }, "suffixLength": { @@ -346,5 +346,21 @@ "principalType": "ServicePrincipal" } } - ] + ], + "outputs": { + "blobEndpoint": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2019-06-01').primaryEndpoints.blob]", + "metadata": { + "description": "A URI to the blob storage endpoint." + } + }, + "resourceId": { + "type": "string", + "value": "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]", + "metadata": { + "description": "A unique resource identifier for the storage account." + } + } + } } diff --git a/tests/PSDocs.Azure.Tests/Azure.Common.Tests.ps1 b/tests/PSDocs.Azure.Tests/Azure.Common.Tests.ps1 index 43492958..0afcb80f 100644 --- a/tests/PSDocs.Azure.Tests/Azure.Common.Tests.ps1 +++ b/tests/PSDocs.Azure.Tests/Azure.Common.Tests.ps1 @@ -42,6 +42,16 @@ Describe 'PSDocs' -Tag 'PSDocs', 'Common' { $result | Should -Not -BeNullOrEmpty; } + It 'With basic template' { + $invokeParams = @{ + Module = 'PSDocs.Azure' + OutputPath = $outputPath + InputObject = (Join-Path -Path $rootPath -ChildPath 'tests/PSDocs.Azure.Tests/basic.template.json') + } + $result = Invoke-PSDocument @invokeParams; + $result | Should -Not -BeNullOrEmpty; + } + It 'With relative path' { Push-Location -Path (Join-Path -Path $rootPath -ChildPath 'templates/storage/v1/'); try { diff --git a/tests/PSDocs.Azure.Tests/basic.template.json b/tests/PSDocs.Azure.Tests/basic.template.json new file mode 100644 index 00000000..c449bfa7 --- /dev/null +++ b/tests/PSDocs.Azure.Tests/basic.template.json @@ -0,0 +1,262 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "storageAccountName": { + "type": "string" + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]" + }, + "sku": { + "type": "string", + "defaultValue": "Standard_LRS", + "allowedValues": [ + "Standard_LRS", + "Standard_GRS" + ], + "metadata": { + "description": "Optional. Create the Storage Account as LRS or GRS." + } + }, + "suffixLength": { + "type": "int", + "defaultValue": 0, + "minValue": 0, + "maxValue": 13 + }, + "containers": { + "type": "array", + "defaultValue": [ + ] + }, + "lifecycleRules": { + "type": "array", + "defaultValue": [ + ] + }, + "blobSoftDeleteDays": { + "type": "int", + "defaultValue": 0, + "minValue": 0, + "maxValue": 365 + }, + "containerSoftDeleteDays": { + "type": "int", + "defaultValue": 0, + "minValue": 0, + "maxValue": 365 + }, + "shares": { + "type": "array", + "defaultValue": [ + ] + }, + "useLargeFileShares": { + "type": "bool", + "defaultValue": false + }, + "shareSoftDeleteDays": { + "type": "int", + "defaultValue": 0, + "minValue": 0, + "maxValue": 365 + }, + "allowBlobPublicAccess": { + "type": "bool", + "defaultValue": false + }, + "keyVaultPrincipalId": { + "type": "string", + "defaultValue": "" + }, + "tags": { + "type": "object", + "defaultValue": { + } + } + }, + "variables": { + "storageAccountName": "[concat(parameters('storageAccountName'), if(greater(parameters('suffixLength'), 0), substring(uniqueString(resourceGroup().id), 0, parameters('suffixLength')), ''))]", + "blobSoftDeleteLookup": { + "true": { + "enabled": true, + "days": "[parameters('blobSoftDeleteDays')]" + }, + "false": { + "enabled": false + } + }, + "containerSoftDeleteLookup": { + "true": { + "enabled": true, + "days": "[parameters('containerSoftDeleteDays')]" + }, + "false": null + }, + "shareSoftDeleteLookup": { + "true": { + "enabled": true, + "days": "[parameters('shareSoftDeleteDays')]" + }, + "false": { + "enabled": false + } + }, + "largeFileSharesState": "[if(parameters('useLargeFileShares'), 'Enabled', 'Disabled')]", + "storageAccountKeyOperatorRoleId": "[resourceId('Microsoft.Authorization/roleDefinitions', '81a9662b-bebf-436f-a333-f67b29880f12')]" + }, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2019-06-01", + "name": "[variables('storageAccountName')]", + "location": "[parameters('location')]", + "sku": { + "name": "[parameters('sku')]", + "tier": "Standard" + }, + "kind": "StorageV2", + "properties": { + "networkAcls": { + "bypass": "AzureServices", + "virtualNetworkRules": [ + ], + "ipRules": [ + ], + "defaultAction": "Allow" + }, + "supportsHttpsTrafficOnly": true, + "encryption": { + "services": { + "file": { + "enabled": true + }, + "blob": { + "enabled": true + } + }, + "keySource": "Microsoft.Storage" + }, + "accessTier": "Hot", + "largeFileSharesState": "[variables('largeFileSharesState')]", + "allowBlobPublicAccess": "[parameters('allowBlobPublicAccess')]", + "minimumTlsVersion": "TLS1_2" + }, + "tags": "[parameters('tags')]", + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts/blobServices", + "apiVersion": "2019-06-01", + "name": "[concat(variables('storageAccountName'), '/default')]", + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]" + ], + "sku": { + "name": "[parameters('sku')]" + }, + "properties": { + "cors": { + "corsRules": [ + ] + }, + "deleteRetentionPolicy": "[variables('blobSoftDeleteLookup')[string(greater(parameters('blobSoftDeleteDays'), 0))]]", + "containerDeleteRetentionPolicy": "[variables('containerSoftDeleteLookup')[string(greater(parameters('containerSoftDeleteDays'), 0))]]" + } + }, + { + "type": "Microsoft.Storage/storageAccounts/fileServices", + "apiVersion": "2019-06-01", + "name": "[concat(variables('storageAccountName'), '/default')]", + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]" + ], + "sku": { + "name": "[parameters('sku')]" + }, + "properties": { + "shareDeleteRetentionPolicy": "[variables('shareSoftDeleteLookup')[string(greater(parameters('shareSoftDeleteDays'), 0))]]" + } + } + ] + }, + { + "condition": "[not(equals(length(parameters('containers')), 0))]", + "type": "Microsoft.Storage/storageAccounts/blobServices/containers", + "apiVersion": "2019-06-01", + "name": "[if(equals(length(parameters('containers')), 0), concat(variables('storageAccountName'), '/default/empty'), concat(variables('storageAccountName'), '/default/', parameters('containers')[copyIndex('containerIndex')].name))]", + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts/blobServices', variables('storageAccountName'), 'default')]", + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]" + ], + "copy": { + "mode": "Parallel", + "count": "[if(equals(length(parameters('containers')), 0), 1, length(parameters('containers')))]", + "name": "containerIndex" + }, + "properties": { + "metadata": "[parameters('containers')[copyIndex('containerIndex')].metadata]", + "publicAccess": "[parameters('containers')[copyIndex('containerIndex')].publicAccess]" + } + }, + { + "condition": "[not(empty(parameters('lifecycleRules')))]", + "name": "[concat(variables('storageAccountName'), '/default')]", + "type": "Microsoft.Storage/storageAccounts/managementPolicies", + "apiVersion": "2019-06-01", + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]" + ], + "properties": { + "policy": { + "rules": "[parameters('lifecycleRules')]" + } + } + }, + { + "condition": "[not(equals(length(parameters('shares')), 0))]", + "type": "Microsoft.Storage/storageAccounts/fileServices/shares", + "apiVersion": "2019-06-01", + "name": "[if(equals(length(parameters('shares')), 0), concat(variables('storageAccountName'), '/default/empty'), concat(variables('storageAccountName'), '/default/', parameters('shares')[copyIndex('shareIndex')].name))]", + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts/fileServices', variables('storageAccountName'), 'default')]", + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]" + ], + "copy": { + "mode": "Parallel", + "count": "[if(equals(length(parameters('shares')), 0), 1, length(parameters('shares')))]", + "name": "shareIndex" + }, + "properties": { + "metadata": "[parameters('shares')[copyIndex('shareIndex')].metadata]", + "shareQuota": "[parameters('shares')[copyIndex('shareIndex')].shareQuota]" + } + }, + { + "condition": "[not(empty(parameters('keyVaultPrincipalId')))]", + "type": "Microsoft.Storage/storageAccounts/providers/roleAssignments", + "apiVersion": "2018-09-01-preview", + "name": "[concat(variables('storageAccountName'), '/Microsoft.Authorization/', guid(parameters('keyVaultPrincipalId'), variables('storageAccountKeyOperatorRoleId')))]", + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]" + ], + "properties": { + "roleDefinitionId": "[variables('storageAccountKeyOperatorRoleId')]", + "principalId": "[parameters('keyVaultPrincipalId')]", + "scope": "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]", + "principalType": "ServicePrincipal" + } + } + ], + "outputs": { + "blobEndpoint": { + "type": "string", + "value": "[reference(resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName')), '2019-06-01').primaryEndpoints.blob]" + }, + "resourceId": { + "type": "string", + "value": "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]" + } + } +} From 01a76ea159f68d5c74d2da1d0ca6f7ce13453429 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Tue, 19 Jan 2021 12:37:37 +1000 Subject: [PATCH 2/2] Additional logical improvements --- src/PSDocs.Azure/docs/Azure.Template.Doc.ps1 | 29 ++++++++++++++------ 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/PSDocs.Azure/docs/Azure.Template.Doc.ps1 b/src/PSDocs.Azure/docs/Azure.Template.Doc.ps1 index 8ef1d95b..ff2321b2 100644 --- a/src/PSDocs.Azure/docs/Azure.Template.Doc.ps1 +++ b/src/PSDocs.Azure/docs/Azure.Template.Doc.ps1 @@ -15,12 +15,22 @@ function global:GetTemplateParameter { process { $template = Get-Content -Path $Path -Raw | ConvertFrom-Json; foreach ($property in $template.parameters.PSObject.Properties) { - [PSCustomObject]@{ + $result = [PSCustomObject]@{ Name = $property.Name - Description = $property.Value.metadata.description - DefaultValue = $property.Value.defaultValue - AllowedValues = $property.Value.allowedValues + Description = '' + DefaultValue = $Null + AllowedValues = $Null + } + if ([bool]$property.Value.PSObject.Properties['metadata'] -and [bool]$property.Value.metadata.PSObject.Properties['description']) { + $result.Description = $property.Value.metadata.description; } + if ([bool]$property.Value.PSObject.Properties['defaultValue']) { + $result.DefaultValue = $property.Value.defaultValue; + } + if ([bool]$property.Value.PSObject.Properties['allowedValues']) { + $result.AllowedValues = $property.Value.allowedValues; + } + $result; } } } @@ -52,8 +62,9 @@ function global:GetTemplateExample { } foreach ($property in $template.parameters.PSObject.Properties) { $propertyValue = $Null; + $hasMetadata = [bool]$property.Value.PSObject.Properties['metadata']; - if ($True -eq $property.Value.metadata.ignore) { + if ($hasMetadata -and [bool]$property.Value.metadata.PSObject.Properties['ignore'] -and $True -eq $property.Value.metadata.ignore) { continue; } @@ -70,10 +81,10 @@ function global:GetTemplateExample { continue; } - if ($Null -ne $property.Value.metadata.example) { + if ($hasMetadata -and [bool]$property.Value.metadata.PSObject.Properties['example'] -and $Null -ne $property.Value.metadata.example) { $propertyValue = $property.Value.metadata.example; } - elseif ($Null -ne $property.Value.defaultValue) { + elseif ([bool]$property.Value.PSObject.Properties['defaultValue'] -and $Null -ne $property.Value.defaultValue) { $propertyValue = $property.Value.defaultValue; } elseif ($property.Value.type -eq 'array') { @@ -154,7 +165,9 @@ Document 'README' { } # Write opening line - $metadata.Description + if ($Null -ne $metadata -and [bool]$metadata.PSObject.Properties['description']) { + $metadata.description + } # Add table and detail for each parameter Section $LocalizedData.Parameters {