Expand-AzTemplate.ps1
function Expand-AzTemplate { <# .Synopsis Expands the contents of an Azure Resource Manager template. .Description Expands an Azure Resource Manager template and related files into a set of well-known parameters Or Expands an Azure Resource Manager template expression .Notes Expand-AzTemplate -Expression expands expressions the resolve to a top-level property (e.g. variables or parameters). It does not expand recursively, and it does not attempt to evaluate complex expressions. #> [CmdletBinding(DefaultParameterSetName='SpecificTemplate')] [OutputType([string],[PSObject])] param( # The path to an Azure resource manager template [Parameter(Mandatory=$true,Position=0,ValueFromPipelineByPropertyName=$true,ParameterSetName='SpecificTemplate')] [Alias('Fullname','Path')] [string] $TemplatePath, # An Azure Template Expression, for example [parameters('foo')].bar. # If this expression was expanded, it would look in -InputObject for a .Parameters object containing the property 'foo'. # Then it would look in that result for a property named bar. [Parameter(Mandatory=$true,Position=0,ValueFromPipelineByPropertyName=$true,ParameterSetName='Expression')] [string] $Expression, # A whitelist of top-level properties to expand. # For example, passing -Include Parameters will only expand out the [Parameters()] function [Parameter(ParameterSetName='Expression')] [string[]] $Include, # A blacklist of top-level properties that will not be expanded. # For example, passing -Exclude Parameters will not expand any [Parameters()] function. [Parameter(ParameterSetName='Expression')] [string[]] $Exclude, # The object that will be used to evaluate the expression. [Parameter(ValueFromPipeline=$true,ParameterSetName='Expression')] [PSObject] $InputObject ) begin { function Expand-Resource ( [Parameter(Mandatory=$true,Position=0,ValueFromPipelineByPropertyName=$true)] [Alias('Resources')] [PSObject[]] $Resource, [PSObject[]] $Parent ) { process { foreach ($r in $Resource) { $r | Add-Member NoteProperty ParentResources $parent -Force -PassThru if ($r.resources) { $r | Expand-Resource -Parent (@($r) + @(if ($parent) { $parent })) } } } } $TemplateLanguageExpression = " \s{0,} # optional whitespace \[ # opening bracket (?<Function>\S{1,}) # the top-level function name (?<Parameters>\( # the opening parenthesis (?>[^\(\)]+|\((?<Depth>)|\)(?<-Depth>))*(?(Depth)(?!)) # anything until we're balanced \)) # the closing parenthesis (?<Index>\[\d{1,}\]){0,1} # an optional index (?<Property>\. # a property (?<PropertyName>[^\.\[\]\s]{1,}){1,1} (?<PropertyIndex>\[\d{1,}\]){0,1} # One or more optional properties ){0,} \] # closing bracket \s{0,} # optional whitespace " $TemplateParametersExpression = " ( (?<Quote>') # a single quote (?<StringLiteral>([^']|(?<=')'){1,}) # anything until the next quote (including '') \k<Quote>| # a closing quote OR (?<Boolean>true|false)| # the literal values true and false OR (?<Number>\d[\d\.]{1,})| # a number OR ( (?<Function>\S{1,}) # the top-level function name (?<Parameters>\( # the opening parenthesis (?>[^\(\)]+|\((?<Depth>)|\)(?<-Depth>))*(?(Depth)(?!)) # anything until we're balanced \)) # the closing parenthesis ) (?<Index>\[\d{1,}\]){0,} # One or more indeces (?<Property>\.[^\.\s]{1,}){0,} # One or more optional properties )\s{0,} " $regexOptions = 'Multiline,IgnoreCase,IgnorePatternWhitespace' $regexTimeout = [Timespan]::FromSeconds(5) } process { if ($PSCmdlet.ParameterSetName -eq 'SpecificTemplate') { # Now let's try to resolve the template path. $resolvedTemplatePath = # If the template path doesn't appear to be a path to a json file, if ($TemplatePath -notlike '*.json') { # see if it looks like a file if ( test-path -path $templatePath -PathType leaf) { $TemplatePath = $TemplatePath | Split-Path # if it does, reassign template path to it's directory. } # Then, go looking beneath that template path $preferredJsonFile = $TemplatePath | Get-ChildItem -Filter *.json | # for a file named azuredeploy.json, prereq.azuredeploy.json or mainTemplate.json Where-Object { 'azuredeploy.json', 'mainTemplate.json', 'prereq.azuredeploy.json' -contains $_.Name } | Select-Object -First 1 -ExpandProperty Fullname # If no file was found, write an error and return. if (-not $preferredJsonFile) { Write-Error "No azuredeploy.json or mainTemplate.json found beneath $TemplatePath" return } $preferredJsonFile } else { $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($templatePath) } # If we couldn't find a template file, return (an error should have already been written). if (-not $resolvedTemplatePath) { return } # Next, we want to pre-populate a number of well-known variables. # These variables will be available to every test case. They are: $WellKnownVariables = 'TemplateFullPath','TemplateText','TemplateObject','TemplateFileName', 'CreateUIDefinitionFullPath','createUIDefinitionText','CreateUIDefinitionObject', 'FolderName', 'HasCreateUIDefinition', 'IsMainTemplate','FolderFiles', 'MainTemplatePath', 'MainTemplateObject', 'MainTemplateText', 'MainTemplateResources','MainTemplateVariables','MainTemplateParameters', 'MainTemplateOutputs' foreach ($_ in $WellKnownVariables) { $ExecutionContext.SessionState.PSVariable.Set($_, $null) } #*$templateFullPath (the full path to the .json file) $TemplateFullPath = "$resolvedTemplatePath" #*$TemplateFileName (the name of the azure template file) $templateFileName = $TemplateFullPath | Split-Path -Leaf #*$IsMainTemplate (if the TemplateFileName is named mainTemplate.json) $isMainTemplate = 'mainTemplate.json', 'azuredeploy.json', 'prereq.azuredeploy.json' -contains $templateFileName $templateFile = Get-Item -LiteralPath "$resolvedTemplatePath" $templateFolder = $templateFile.Directory #*$FolderName (the name of the root folder containing the template) $TemplateName = $templateFolder.Name #*$TemplateText (the text contents of the template file) $TemplateText = [IO.File]::ReadAllText($resolvedTemplatePath) #*$TemplateObject (the template text, converted from JSON) $TemplateObject = Import-Json -FilePath $TemplateFullPath if ($resolvedTemplatePath -like '*.json' -and $TemplateObject.'$schema' -like '*CreateUIDefinition*') { $createUiDefinitionFullPath = "$resolvedTemplatePath" $createUIDefinitionText = [IO.File]::ReadAllText($createUiDefinitionFullPath) $createUIDefinitionObject = Import-Json -FilePath $createUiDefinitionFullPath $HasCreateUIDefinition = $true $isMainTemplate = $false $templateFile = $TemplateText = $templateObject = $TemplateFullPath = $templateFileName = $null } else { #*$CreateUIDefinitionFullPath (the path to CreateUIDefinition.json) $createUiDefinitionFullPath = Join-Path -childPath 'createUiDefinition.json' -Path $templateFolder if (Test-Path $createUiDefinitionFullPath) { #*$CreateUIDefinitionText (the text contents of CreateUIDefinition.json) $createUIDefinitionText = [IO.File]::ReadAllText($createUiDefinitionFullPath) #*$CreateUIDefinitionObject (the createuidefinition text, converted from json) $createUIDefinitionObject = Import-Json -FilePath $createUiDefinitionFullPath #*$HasCreateUIDefinition (indicates if a CreateUIDefinition.json file exists) $HasCreateUIDefinition = $true } else { $HasCreateUIDefinition = $false $createUiDefinitionFullPath = $null } } #*$FolderFiles (a list of objects of each file in the directory) $FolderFiles = @(Get-ChildItem -Path $templateFolder.FullName -Recurse | Where-Object { -not $_.PSIsContainer } | ForEach-Object { $fileInfo = $_ if ($resolvedTemplatePath -like '*.json' -and -not $isMainTemplate -and $fileInfo.FullName -ne $resolvedTemplatePath) { return } if ($fileInfo.DirectoryName -eq '__macosx') { return # (excluding files as side-effects of MAC zips) } # All FolderFile objects will have the following properties: if ($fileInfo.Extension -eq '.json') { $fileObject = [Ordered]@{ Name = $fileInfo.Name #*Name (the name of the file) Extension = $fileInfo.Extension #*Extension (the file extension) Text = [IO.File]::ReadAllText($fileInfo.FullName)#*Text (the file content as text) FullPath = $fileInfo.Fullname#*FullPath (the full path to the file) } # If the file is JSON, two additional properties may be present: #*Object (the file's text, converted from JSON) $fileObject.Object = Import-Json $fileObject.FullPath #*Schema (the value of the $schema property of the JSON object, if present) $fileObject.schema = $fileObject.Object.'$schema' $fileObject } }) if ($isMainTemplate) { # If the file was a main template, # we set a few more variables: #*MainTemplatePath (the path to the main template file) $MainTemplatePath = "$TemplateFullPath" #*MainTemplateText (the text of the main template file) $MainTemplateText = [IO.File]::ReadAllText($MainTemplatePath) #*MainTemplateObject (the main template, converted from JSON) $MainTemplateObject = Import-Json -FilePath $MainTemplatePath #*MainTemplateResources (the resources and child resources in the main template) $MainTemplateResources = if ($mainTemplateObject.Resources) { Expand-Resource -Resource $MainTemplateObject.resources } else { $null } #*MainTemplateParameters (a hashtable of parameters in the main template) $MainTemplateParameters = [Ordered]@{} foreach ($prop in $MainTemplateObject.parameters.psobject.properties) { $MainTemplateParameters[$prop.Name] = $prop.Value } #*MainTemplateVariables (a hashtable of variables in the main template) $MainTemplateVariables = [Ordered]@{} foreach ($prop in $MainTemplateObject.variables.psobject.properties) { $MainTemplateVariables[$prop.Name] = $prop.Value } #*MainTemplateOutputs (a hashtable of outputs in the main template) $MainTemplateOutputs = [Ordered]@{} foreach ($prop in $MainTemplateObject.outputs.psobject.properties) { $MainTemplateOutputs[$prop.Name] = $prop.Value } } # If we've found a CreateUIDefinition, we'll want to process it first. if ($HasCreateUIDefinition) { # Loop over the folder files and get every file that isn't createUIDefinition $otherFolderFiles = @(foreach ($_ in $FolderFiles) { if ($_.Name -ne 'CreateUIDefinition.json') { $_ } else { $createUIDefFile = $_ } }) # Then recreate the list with createUIDefinition that the front. $FolderFiles = @(@($createUIDefFile) + @($otherFolderFiles) -ne $null) } $out = [Ordered]@{} foreach ($v in $WellKnownVariables) { $out[$v] = $ExecutionContext.SessionState.PSVariable.Get($v).Value } $out } elseif ($PSCmdlet.ParameterSetName -eq 'Expression') { # First, we need to see if the expression provided looks like a template language expression $matched? = [Regex]::Match($Expression, $TemplateLanguageExpression, $regexOptions, $regexTimeout) if (-not $matched?.Success) { # If it wasn't Write-Verbose "$Expression is not an expression" # Write to the verbose stream return $Expression # and return the original expression } $functionName = $matched?.Groups["Function"].Value if (-not $InputObject.$functionName) { # If there wasn't a property on the inputobject return $matched?.Value # Return the expression } # Get the parameters $parametersExpression = $matched?.Groups["Parameters"].Value # strip off the () (don't use trim, or we might hurt subexpressions) $parametersExpression = $parametersExpression.Substring(1,$parametersExpression.Length - 1) $functionParameters = @([Regex]::Matches($parametersExpression, $TemplateParametersExpression, $regexOptions, $regexTimeout)) if (-not $functionParameters) { # If there were no parameters return $matched?.Value # return the partially resolved expression. } if (-not $functionParameters[0].Groups["StringLiteral"].Success) { # If we didn't get a literal value return $matched?.Value # return the partially resolved expression. } if ($Include -and $Include -notcontains $functionName) { # If we have a whitelist, and the function isn't in it. return $Expression # don't evaluate. } if ($Exclude -and $Exclude -contains $functionName) { # If we have a blacklist, and the function is in it. return $Expression # don't evaluate. } # Find the target property $targetProperty = $functionParameters[0].Groups["StringLiteral"].Value # and resolve the target object. $targetObject = $InputObject.$functionName.$targetProperty if (-not $targetObject) { # If the object didn't resolve, Write-Error ".$functionName.$targetProperty not found" # error out. return } if ($matched?.Groups["Index"].Success) { # Assuming it did, we have to check for indices $index = $matched?.Groups["Index"].Value -replace '[\[\]]', '' -as [int] if (-not $targetObject[$index]) { Write-Error "Index $index not found" return } else { $targetObject = $targetObject[$index] } } # Since we can nest properties and indices, we just have to work thru each remaining one. $propertyMatchGroup = $matched?.Groups["Property"] if ($propertyMatchGroup.Success) { foreach ($cap in $propertyMatchGroup.Captures) { $propName, $propIndex = $cap.Value -split '[\.\[\]]' -ne '' if (-not $targetObject.$propName) { Write-Error "Property $propName not found" return } $targetObject = $targetObject.$propName if ($propIndex -and $propIndex -as [int] -ne $null) { if (-not $targetObject[$propIndex -as [int]]) { Write-Error "Index $propIndex not found" return } else { $targetObject = $targetObject[$propIndex -as [int]] } } } } # and at last, we can return whatever was resolved. return $targetObject } } } |