TemplateValidator/AzureRM.TemplateValidator.psm1 (1,002 lines of code) (raw):
# Copyright (c) Microsoft Corporation. All rights reserved
# See LICENSE.txt in the project root for license information
<#
SYNOPSIS
Validate Azure ARM Template Capabilities (ARM resources, Api-version, VM Extensions, VM Images, VMSizes, Storage SKU's etc) for Azure Stack and Azure
#>
$Global:VerbosePreference = 'SilentlyContinue'
function Test-AzureRMTemplate() {
[CmdletBinding()]
param(
[Parameter(Mandatory = $true, HelpMessage = "Template directory or TemplateFullPath to Validate", ParameterSetName = "local")]
[ValidateScript( { Test-Path -Path $_ })]
[String] $TemplatePath,
[Parameter(Mandatory = $true, HelpMessage = "Template url to Validate", ParameterSetName = "url")]
[String] $TemplateUrl,
[Parameter(ParameterSetName = "local")]
[Parameter(ParameterSetName = "url")]
[Parameter(HelpMessage = "Template Pattern to match.Default is *azuredeploy.json")]
[String] $TemplatePattern = "*azuredeploy.json",
[Parameter(Mandatory = $true, HelpMessage = "Cloud Capabilities JSON File Name in the current folder or with full path")]
[Parameter(ParameterSetName = "local")]
[Parameter(ParameterSetName = "url")]
[ValidateScript( { Test-Path -Path $_ })]
[String] $CapabilitiesPath,
[Parameter(HelpMessage = "Set to process VMImages , VMExtensions & VMSizes")]
[Parameter(ParameterSetName = "local")]
[Parameter(ParameterSetName = "url")]
[Switch] $IncludeComputeCapabilities,
[Parameter(HelpMessage = "Set to process Storage Skus")]
[Parameter(ParameterSetName = "local")]
[Parameter(ParameterSetName = "url")]
[Switch] $IncludeStorageCapabilities,
[Parameter(HelpMessage = "Output Report FileName")]
[Parameter(ParameterSetName = "local")]
[Parameter(ParameterSetName = "url")]
[String] $Report = "TemplateValidationReport.html"
)
$capabilities = ConvertFrom-Json (Get-Content -Path $CapabilitiesPath -Raw) -ErrorAction Stop
$reportFilePath = Join-Path $PSScriptRoot $Report
if ($PSCmdlet.ParameterSetName -eq "url")
{
$pathTokens = $TemplateUrl.Split("/")
$filename = $pathTokens[-1]
$fileDir = $pathTokens[-2]
$rootPath = $env:TEMP
$localDirPath = Join-Path -Path $rootPath -ChildPath $fileDir
if(Test-Path($localDirPath))
{
Remove-Item -Path $localDirPath -Recurse -Force -ErrorAction Stop
}
New-Item -Path $localDirPath -ItemType Directory -Force | Out-Null
$sourcePath = Join-Path -Path $localDirPath -ChildPath $filename
Invoke-WebRequest -Uri $TemplateUrl -OutFile $sourcePath -Verbose -ErrorAction Stop
if([IO.Path]::GetExtension($filename) -eq ".zip")
{
$expandedPath = Join-Path -Path $localDirPath -ChildPath ([IO.Path]::GetFileNameWithoutExtension($filename))
Expand-archive $sourcePath -DestinationPath $expandedPath -ErrorAction Stop
$sourcePath = $expandedPath
}
}
else
{
$sourcePath = $TemplatePath
}
$TemplateDirectory = Get-ChildItem -Path $sourcePath -Recurse -Include $TemplatePattern
$reportOutPut = @()
$totalCount = 0
$warningCount = 0
$notSupportedCount = 0
$exceptionCount = 0
$passedCount = 0
$recommendCount = 0
foreach ($template in $TemplateDirectory) {
$templateName = (Split-path -Path $template.FullName).Split("\")[-1]
$templateResults = @()
$templateResults.Clear()
Write-Verbose "Template name is $templateName"
try {
$rootTemplateResult = ValidateTemplate -TemplatePath $template.FullName -Capabilities $Capabilities -IncludeComputeCapabilities:$IncludeComputeCapabilities -IncludeStorageCapabilities:$IncludeStorageCapabilities
Write-Verbose "Get nested templates from $templateName"
[System.Collections.Stack] $nestedTemplates = New-Object System.Collections.Stack
$rootTemplateResult.Details += ([Environment]::NewLine) + (Get-NestedTemplate $template.FullName $nestedTemplates)
if ($nestedTemplates.Count) {
$nestedTemplateResults = @()
$templateValidationStatus = @()
$templateValidationStatus.Clear()
$templateValidationStatus += $rootTemplateResult.Status
while ($nestedTemplates.Count) {
$nestedTemplate = $nestedTemplates.Pop()
if ($nestedTemplate.DownloadError) {
$nestedTemplateResult = [PSCustomObject]@{
TemplateName = ""
Status = "Exception"
Details = $nestedTemplate.DownloadError
}
}
else {
$nestedTemplateResult = ValidateTemplate -TemplatePath $nestedTemplate.LocalTemplatePath -Capabilities $Capabilities -IncludeComputeCapabilities:$IncludeComputeCapabilities -IncludeStorageCapabilities:$IncludeStorageCapabilities
Write-Verbose "Get nested templates from $nestedTemplate.TemplateLink"
$rootTemplateResult.Details += ([Environment]::NewLine) + (Get-NestedTemplate $nestedTemplate.LocalTemplatePath $NestedTemplates)
Remove-Item $nestedTemplate.LocalTemplatePath
}
$nestedTemplateResult.TemplateName = "-- " + $nestedTemplate.TemplateLink
$nestedTemplateResults += $nestedTemplateResult
$templateValidationStatus += $nestedTemplateResult.Status
}
$templateResults += [PSCustomObject]@{
TemplateName = $templateName
Status = "Passed"
Details = ""
}
if ($templateValidationStatus.Contains("NotSupported")) {
$templateResults[0].Status = "NotSupported"
}
elseif ($templateValidationStatus.Contains("Exception")) {
$templateResults[0].Status = "Exception"
}
elseif ($templateValidationStatus.Contains("Warning")) {
$templateResults[0].Status = "Warning"
}
elseif ($templateValidationStatus.Contains("Recommend")) {
$templateResults[0].Status = "Recommend"
}
$rootTemplateResult.TemplateName = "-- azuredeploy.json"
$templateResults += $rootTemplateResult
$templateResults += $nestedTemplateResults
}
else {
$templateResults += $rootTemplateResult
}
}
catch {
$templateResults += [PSCustomObject]@{
TemplateName = $templateName
Status = "Exception"
Details = "Exception: $($_.Exception.Message)"
}
}
finally {
$totalcount++
}
if ($templateResults[0].Status -like "NotSupported") {
$notSupportedCount++
}
elseif ($templateResults[0].Status -like "Exception") {
$exceptionCount++
}
elseif ($templateResults[0].Status -like "Warning") {
$warningCount++
}
elseif ($templateResults[0].Status -like "Recommend") {
$recommendCount++
}
else {
$passedCount++
}
$reportOutPut += $templateResults
}
if (([System.IO.FileInfo]$Report).Extension -eq '.csv') {
$reportOutPut | Export-CSV -delimiter ';' -NoTypeInformation -Encoding "unicode" -Path $reportFilePath
}
elseif (([System.IO.FileInfo]$Report).Extension -eq '.html') {
$head = @"
<style>
body { font: verdana}
table { width:100%}
table, th, td {
border: 1px solid black;
border-collapse: collapse;
}
th, td {
padding: 5px;
text-align: left;
}
th { background-color: LightSlateGrey;
color: white;
font-size: large;
}
tr:hover {
background: peachpuff;
}
h1 { font-size:200%; text-align:center;text-decoration: underline;}
.PASS td:nth-child(2){background-color: Lime }
.NOTSUPPORTED td:nth-child(2){background-color: orangered}
.EXCEPTION td:nth-child(2){background-color: gray}
.WARN td:nth-child(2){background-color: yellow}
.RECOMMEND td:nth-child(2){background-color: Orange}
table td:nth-child(2){font-weight: bold;}
table td:nth-child(3){white-space:pre-line}
</style>
"@
$title = "<H1>Template Validation Report</H1>"
$validationSummary = "<H3>Template Validation completed on $(Get-Date)<br>
Passed: $passedCount<br>
NotSupported: $notSupportedCount<br>
Exception: $exceptionCount<br>
Warning: $warningCount<br>
Recommend: $recommendCount<br>
Total Templates: $totalCount</H3>"
[xml] $reportXml = $reportOutPut | ConvertTo-Html -Fragment
for ($i = 1; $i -le $reportXml.table.tr.count - 1; $i++) {
$class = $reportXml.CreateAttribute("class")
if ($reportXml.table.tr[$i].td[0] -like '--*') {
$style = $reportXml.CreateAttribute("style")
$style.Value = "background-color: goldenrod"
$reportXml.table.tr[$i].Attributes.Append($style)| out-null
}
if ($reportXml.table.tr[$i].td[1] -eq 'Passed') {
$class.value = "PASS"
$reportXml.table.tr[$i].Attributes.Append($class)| out-null
}
elseif ($reportXml.table.tr[$i].td[1] -eq 'NotSupported') {
$class.value = "NOTSUPPORTED"
$reportXml.table.tr[$i].Attributes.Append($class)| out-null
}
elseif ($reportXml.table.tr[$i].td[1] -eq 'Exception') {
$class.value = "EXCEPTION"
$reportXml.table.tr[$i].Attributes.Append($class)| out-null
}
elseif ($reportXml.table.tr[$i].td[1] -eq 'Warning') {
$class.value = "WARN"
$reportXml.table.tr[$i].Attributes.Append($class)| out-null
}
elseif ($reportXml.table.tr[$i].td[1] -eq 'Recommend') {
$class.value = "RECOMMEND"
$reportXml.table.tr[$i].Attributes.Append($class)| out-null
}
}
$reportHtml = $title + $validationSummary + $reportXml.OuterXml|Out-String
ConvertTo-Html $postContent -head $head -Body $reportHtml | out-File $reportFilePath
}
Write-Output "Validation Summary:
`Passed: $passedCount
`NotSupported: $notSupportedCount
`Exception: $exceptionCount
`Warning: $warningCount
`Recommend: $recommendCount
`Total Templates: $totalCount"
Write-Output "Report available at - $reportFilePath"
}
function Get-NestedTemplate {
param(
[Parameter(Mandatory = $true, HelpMessage = "Path for a template JSON which will be checked for nested templates")]
[String] $TemplatePath,
[Parameter(Mandatory = $true, HelpMessage = "Stack containing nested templates")]
[System.Collections.Stack] $NestedTemplates
)
try {
$TemplatePS = ConvertFrom-Json (Get-Content -Path $TemplatePath -Raw)
foreach ($resource in $TemplatePS.resources) {
if ($resource.type -eq 'Microsoft.Resources/deployments') {
$templateLink = ""
$err = ""
$localTemplatePath = ""
try {
$templateLink = Get-PropertyValue $resource.properties.templateLink.uri $TemplatePS $resource
Write-Verbose "Download nested template. Template link - $templateLink"
$localTemplatePath = Join-Path $env:TEMP ("NestedTemplate-{0}.json" -f ([DateTime]::Now).Ticks.ToString())
Invoke-RestMethod -Method GET -Uri $templateLink -OutFile $localTemplatePath
}
catch {
$err = "Exception: Unable to get nested template link. Template link - $templateLink. $($_.Exception.Message)"
Write-Error $err
}
$nestedTemplate = [PSCustomObject]@{
TemplateLink = $templateLink
LocalTemplatePath = $localTemplatePath
DownloadError = $err
}
$NestedTemplates.Push($nestedTemplate)
}
}
}
catch {
$err = "Exception: Unable to get nested templates for $TemplatePath. $($_.Exception.Message)"
Write-Error $err
return $err
}
return ""
}
function ValidateTemplate {
param(
[Parameter(Mandatory = $true, HelpMessage = "Template JSON Path")]
[String] $TemplatePath,
[Parameter(Mandatory = $true, HelpMessage = "Cloud Capabilities Json ")]
[PSObject] $Capabilities,
[Parameter(HelpMessage = "Set to process VMImages, VMExtensions and VMSizes")]
[Switch] $IncludeComputeCapabilities,
[Parameter(HelpMessage = "Set to process Storage Skus")]
[Switch] $IncludeStorageCapabilities
)
$ValidationOutput = [PSCustomObject]@{
TemplateName = ""
Status = ""
Details = ""
}
try {
$ValidationOutPut.TemplateName = (Split-path -Path $template.FullName).Split("\")[-1]
$TemplatePS = ConvertFrom-Json (Get-Content -Path $TemplatePath -Raw)
$ErrorList = @()
foreach ($templateResource in $TemplatePS.resources) {
$ErrorList += ValidateResource $templateResource $TemplatePS $Capabilities -IncludeComputeCapabilities:$IncludeComputeCapabilities -IncludeStorageCapabilities:$IncludeStorageCapabilities
foreach ($nestedResource in $templateResource.resources) {
$ErrorList += ValidateResource $nestedResource $TemplatePS $Capabilities -IncludeComputeCapabilities:$IncludeComputeCapabilities -IncludeStorageCapabilities:$IncludeStorageCapabilities
}
}
Write-Verbose "Validating the Storage Endpoint"
$hardCodedStorageURI = (Get-Content $TemplatePath) | Select-String -Pattern "`'.blob.core.windows.net`'" | Select-Object LineNumber, Line | Out-string
if ($hardCodedStorageURI) {
Write-Warning "Warning: Storage Endpoint has a hardcoded URI. This endpoint will not resolve correctly outside of public Azure. It is recommended that you instead use a reference function to derive the correct Storage Endpoint $hardCodedStorageURI"
$ErrorList += "Warning: Storage Endpoint has a hardcoded URI. This endpoint will not resolve correctly outside of public Azure. It is recommended that you instead use a reference function to derive the correct Storage Endpoint $hardCodedStorageURI"
}
if (-not $ErrorList) {
$ValidationOutput.Status = "Passed"
}
else {
if ($ErrorList | Select-String -pattern 'NotSupported') {
$ValidationOutput.Status = "NotSupported"
}
elseif ($ErrorList | Select-String -pattern 'Exception') {
$ValidationOutput.Status = "Exception"
}
elseif ($ErrorList | Select-String -pattern 'Warning') {
$ValidationOutput.Status = "Warning"
}
elseif ($ErrorList | Select-String -pattern 'Recommend') {
$ValidationOutput.Status = "Recommend"
}
}
$ValidationOutPut.Details = $ErrorList | out-string
}
catch {
$ValidationOutput.Status = "Exception"
$ValidationOutPut.Details = "Exception: $($_.Exception.Message)"
}
return $ValidationOutput
}
function ExecuteTemplateFunction {
param(
[string] $Property,
[PSCustomObject] $TemplateJSON,
[PSCustomObject] $Resource
)
if ($Property.StartsWith("[") -and $Property.EndsWith("]")) {
$Property = $Property.Remove(0, 1).Trim()
$Property = $Property.Remove($Property.Length - 1, 1).Trim()
}
$Property = $Property.Trim()
$propertyValue = ""
if ($Property.StartsWith("concat(", 'CurrentCultureIgnoreCase')) {
$Property = TrimFunctionName $Property
$functionParams = GetFunctionParams $Property ","
foreach ($functionParam in $functionParams) {
$funRetVal = ExecuteTemplateFunction $functionParam $TemplateJSON $Resource
$propertyValueType = GetPropertyType $funRetVal
if ($propertyValueType -eq "array") {
$propertyValue += $funRetVal[0]
}
else {
$PropertyValue += $funRetVal
}
}
}
elseif ($Property.StartsWith("length(", 'CurrentCultureIgnoreCase')) {
$Property = TrimFunctionName $Property
$propertyValue = ExecuteTemplateFunction $Property $TemplateJSON $Resource
$propertyValue = $propertyValue.Length
}
elseif ($Property.StartsWith("padleft(", 'CurrentCultureIgnoreCase')) {
$Property = TrimFunctionName $Property
$functionParams = GetFunctionParams $Property ","
$propertyValue = ExecuteTemplateFunction $functionParams[0] $TemplateJSON $Resource
$propertyValue = $propertyValue.PadLeft($functionParams[1], $functionParams[2])
}
elseif ($Property.StartsWith("replace(", 'CurrentCultureIgnoreCase')) {
$Property = TrimFunctionName $Property
$functionParams = GetFunctionParams $Property ","
$propertyValue = ExecuteTemplateFunction $functionParams[0] $TemplateJSON $Resource
$propertyValue = $propertyValue.Replace($functionParams[1], $functionParams[2])
}
elseif ($Property.StartsWith("skip(", 'CurrentCultureIgnoreCase')) {
$Property = TrimFunctionName $Property
$functionParams = GetFunctionParams $Property ","
$propertyValue = ExecuteTemplateFunction $functionParams[0] $TemplateJSON $Resource
if ($functionParams[1] -gt $propertyValue.Length) {
$propertyValue = ""
}
elseif (-not ($functionParams[1] -le 0)) {
$propertyValue = $propertyValue.Replace($functionParams[1])
}
}
elseif ($Property.StartsWith("split(", 'CurrentCultureIgnoreCase')) {
$Property = TrimFunctionName $Property
$functionParams = GetFunctionParams $Property ","
$propertyValue = ExecuteTemplateFunction $functionParams[0] $TemplateJSON $Resource
$propertyValue = $propertyValue.Split($functionParams[1])
}
elseif ($Property.StartsWith("string(", 'CurrentCultureIgnoreCase')) {
$Property = TrimFunctionName $Property
$functionParams = GetFunctionParams $Property ","
$propertyValue = ExecuteTemplateFunction $functionParams[0] $TemplateJSON $Resource
[string] $convertedPropertyValue = $propertyValue
$propertyValue = $convertedPropertyValue
}
elseif ($Property.StartsWith("substring(", 'CurrentCultureIgnoreCase')) {
$Property = TrimFunctionName $Property
$functionParams = GetFunctionParams $Property ","
$propertyValue = ExecuteTemplateFunction $functionParams[0] $TemplateJSON $Resource
$propertyValue = $propertyValue.Substring($functionParams[1], $functionParams[2])
}
elseif ($Property.StartsWith("take(", 'CurrentCultureIgnoreCase')) {
$Property = TrimFunctionName $Property
$functionParams = GetFunctionParams $Property ","
$propertyValue = ExecuteTemplateFunction $functionParams[0] $TemplateJSON $Resource
if ($functionParams[1] -le 0) {
$propertyValue = ""
}
elseif (-not ($functionParams[1] -gt $propertyValue.Length)) {
$propertyValue = $propertyValue.Substring(0, $functionParams[1])
}
}
elseif ($Property.StartsWith("tolower(", 'CurrentCultureIgnoreCase')) {
$Property = TrimFunctionName $Property
$functionParams = GetFunctionParams $Property ","
$propertyValue = ExecuteTemplateFunction $functionParams[0] $TemplateJSON $Resource
$propertyValue = $propertyValue.ToLower()
}
elseif ($Property.StartsWith("toupper(", 'CurrentCultureIgnoreCase')) {
$Property = TrimFunctionName $Property
$functionParams = GetFunctionParams $Property ","
$propertyValue = ExecuteTemplateFunction $functionParams[0] $TemplateJSON $Resource
$propertyValue = $propertyValue.ToUpper()
}
elseif ($Property.StartsWith("trim(", 'CurrentCultureIgnoreCase')) {
$Property = TrimFunctionName $Property
$functionParams = GetFunctionParams $Property ","
$propertyValue = ExecuteTemplateFunction $functionParams[0] $TemplateJSON $Resource
$propertyValue = $propertyValue.Trim()
}
elseif ($Property.StartsWith("parameters(", 'CurrentCultureIgnoreCase')) {
$functionParams = GetFunctionParams $Property "."
$parameterKey = $functionParams[0]
$parameterKey = TrimFunctionName $parameterKey
$parameterKey = ExecuteTemplateFunction $parameterKey $TemplateJSON $Resource
$parameterValue = $TemplateJSON.parameters.$parameterKey.defaultValue
if (-not $parameterValue) {
$parameterValue = $TemplateJSON.parameters.$parameterKey.allowedValues
if (-not $parameterValue) {
throw "Parameter: $parameterKey - No defaultvalue or allowedValues set"
}
}
$parameterValueType = GetPropertyType $parameterValue
if ($parameterValueType -eq "function") {
$propertyValue = ExecuteTemplateFunction $parameterValue $TemplateJSON $Resource
}
else {
$propertyValue = $parameterValue
}
for ($indx = 1; $indx -lt $functionParams.Count; $indx++) {
$functionParamValue = ExecuteTemplateFunction $functionParams[$indx] $TemplateJSON $Resource
$propertyValue = ExecuteTemplateFunction $propertyValue.($functionParamValue) $TemplateJSON $Resource
}
}
elseif ($Property.StartsWith("variables(", 'CurrentCultureIgnoreCase')) {
$functionParams = @($Property)
$functionParams = GetFunctionParams $Property @(".", "[")
$variableKey = $functionParams[0]
$variableKey = TrimFunctionName $variableKey
$variableKey = ExecuteTemplateFunction $variableKey $TemplateJSON $Resource
$variableKeyType = GetPropertyType $variableKey
if ($variableKeyType -eq "array") {
$variableKey = $variableKey[0]
}
$variableValue = $TemplateJSON.variables.$variableKey
$variableValueType = GetPropertyType $variableValue
if ($variableValueType -eq "function") {
$propertyValue = ExecuteTemplateFunction $variableValue $TemplateJSON $Resource
}
else {
$propertyValue = $variableValue
}
for ($indx = 1; $indx -lt $functionParams.Count; $indx++) {
if ($functionParams[$indx].EndsWith("]")) {
$functionParams[$indx] = $functionParams[$indx].Remove($functionParams[$indx].Length - 1, 1).Trim()
}
$functionParamValue = ExecuteTemplateFunction $functionParams[$indx] $TemplateJSON $Resource
$functionParamValueType = GetPropertyType $functionParamValue
if ($functionParamValueType -eq "array") {
$functionParamValue = $functionParamValue[0]
}
$propertyValueType = GetPropertyType $propertyValue
if ($propertyValueType -eq "array") {
$propertyValue = $propertyValue[$functionParamValue]
}
else {
$propertyValue = $propertyValue.($functionParamValue)
}
}
}
elseif ($Property.StartsWith("'") -and $Property.EndsWith("'")) {
return $Property.TrimStart("'").TrimEnd("'").Trim()
}
else {
return $Property
}
$propertyType = GetPropertyType $propertyValue
if ($propertyType -eq "function") {
$propertyValue = ExecuteTemplateFunction $propertyValue $TemplateJSON $Resource
}
return $propertyValue
}
function TrimFunctionName {
param(
[string] $FunctionName
)
$FunctionName = $FunctionName.Remove(0, $FunctionName.IndexOf("(") + 1).Trim()
$FunctionName = $FunctionName.Remove($FunctionName.Length - 1, 1).Trim()
return $FunctionName
}
function GetFunctionParams {
param(
[PSCustomObject] $Property,
$seperator
)
[PSCustomObject] $functionParams = @()
$functionParam = ""
$openingBrackets = 0
$closingBrackets = 0
$propertyLength = $Property.Length
$indx = 0
while ($true) {
if ($indx -eq $propertyLength) {
$functionParams += $functionParam
return , $functionParams
}
elseif (($Property[$indx] -in $seperator) -and ($openingBrackets -eq $closingBrackets)) {
$functionParams += $functionParam
$functionParam = ""
$openingBrackets = 0
$closingBrackets = 0
}
else {
$functionParam += $Property[$indx]
if ($Property[$indx] -eq "(") {
$openingBrackets++
}
elseif ($Property[$indx] -eq ")") {
$closingBrackets++
}
}
$indx++
}
}
function GetPropertyType {
param(
[PSCustomObject] $Property
)
$propertyType = $Property.GetType().Name
$propertyBaseType = $Property.GetType().BaseType.Name
if ($propertyBaseType -eq "ValueType") {
return "literal"
}
elseif ($propertyBaseType -eq "Object") {
if ($propertyType -eq "String") {
if ($Property.StartsWith("[") -and $Property.EndsWith("]")) {
return "function"
}
else {
return "literal"
}
}
elseif ($propertyType -eq "PSCustomObject") {
return "object"
}
}
elseif ($propertyBaseType -eq "Array" -and $propertyType -eq "Object[]") {
return "array"
}
}
function Get-PropertyValue {
param(
[PSCustomObject] $Property,
[PSCustomObject] $Template,
[PSCustomObject] $Resource,
[Parameter(Mandatory = $false)]
[string] $Key
)
if (-not $Property) {
throw "Property is null or empty"
}
$propertyType = GetPropertyType $Property
if ($propertyType -eq "function") {
$Property = ExecuteTemplateFunction $Property $Template $Resource
$propertyType = GetPropertyType $Property
}
if ($propertyType -eq "object") {
$Property = ExecuteTemplateFunction $Property.($Key) $Template $Resource
}
return $Property
}
function CompareValues {
param(
[PSCustomObject] $ProvidedValue,
[PSCustomObject] $SupportedValue
)
if (-not $ProvidedValue -or -not $SupportedValue) {
return $false
}
$result = Compare-Object $ProvidedValue $SupportedValue
if ($result) {
$result = $result | Where-Object { $_.SideIndicator -eq "<="}
if (($result -and $ProvidedValue.GetType().Name -eq "Object[]" -and $result.Count -eq $ProvidedValue.Count) -or ($result -and $ProvidedValue.GetType().Name -eq "String")) {
$result = [PSCustomObject]@{
NoneSupported = $true
NotSupportedValues = $result.InputObject
}
}
elseif ($result) {
$result = [PSCustomObject]@{
NoneSupported = $false
NotSupportedValues = $result.InputObject
}
}
}
return $result
}
function ValidateResource {
param(
[ValidateNotNullOrEmpty()]
[PSObject] $resource,
[ValidateNotNullOrEmpty()]
[PSCustomObject] $Template,
[ValidateNotNullOrEmpty()]
[PSObject] $Capabilities,
[Switch] $IncludeComputeCapabilities,
[Switch] $IncludeStorageCapabilities
)
$ResourceProviderNameSpace = $resource.type.Split("/")[0]
$ResourceTypeName = ""
if ($resource.type.Contains('/')) {
$ResourceTypeName = $resource.type.Substring($resource.type.indexOf('/') + 1)
}
$ResourceTypeProperties = $capabilities.resources | Where-Object { $_.providerNameSpace -eq $ResourceProviderNameSpace } | Where-Object { $_.ResourcetypeName -eq $ResourceTypeName }
$resourceOutput = @()
Write-Verbose "Validating ProviderNameSpace and ResourceType"
if (-not $ResourceTypeProperties) {
$msg = "NotSupported: Resource type '$($resource.type)'"
Write-Error $msg
$resourceOutput += $msg
}
else {
try {
$templateResApiversion = $resource.apiversion
if($templateResApiversion) {
Write-Verbose "Validating API version for $ResourceProviderNameSpace\$ResourceTypeName"
$templateResApiversionType = GetPropertyType $templateResApiversion
if ($templateResApiversionType -eq "function") {
$msg = "Recommend: apiVersion (Resource type: $($resource.type)). It is recommended to set it as a literal value."
Write-Warning $msg
$resourceOutput += $msg
}
$templateResApiversion = Get-PropertyValue $templateResApiversion $Template $resource
$supportedApiVersions = $ResourceTypeProperties.Apiversions
$notSupported = CompareValues $templateResApiversion $supportedApiVersions
if ($notSupported) {
if ($notSupported.NoneSupported) {
$msg = "NotSupported: apiversion (Resource type: $($resource.type)). Not Supported Values - $($notSupported.NotSupportedValues)"
}
else {
$msg = "Warning: apiversion (Resource type: $($resource.type)). Not Supported Values - $($notSupported.NotSupportedValues)"
}
Write-Warning $msg
$resourceOutput += $msg
}
}
else {
$templateResApiProfileVersion = $Template.apiProfile
if( -not $templateResApiProfileVersion) {
$msg = "Exception: apiVersion and apiProfile for resource of type $ResourceTypeName is not specified. One of them has to be specified. "
Write-Error $msg
$resourceOutput += $msg
}
else {
Write-Verbose "Validating API profile version for $ResourceProviderNameSpace\$ResourceTypeName"
$supportedApiProfileVersions = $ResourceTypeProperties.ApiProfiles
if( -not $supportedApiProfileVersions) {
$msg = "NotSupported: No supported api profile versions for this resource, $ResourceProviderNameSpace\$ResourceTypeName."
Write-Warning $msg
$resourceOutput += $msg
}
else{
$notSupported = CompareValues $templateResApiProfileVersion $supportedApiProfileVersions
if ($notSupported) {
if ($notSupported.NoneSupported) {
$msg = "NotSupported: api profile version for (Resource type: $($resource.type)). Not Supported Values - $($notSupported.NotSupportedValues)"
}
else {
$msg = "Warning: apiversion (Resource type: $($resource.type)). Not Supported Values - $($notSupported.NotSupportedValues)"
}
Write-Warning $msg
$resourceOutput += $msg
}
}
}
}
}
catch {
$msg = "Exception: apiVersion. $($_.Exception.Message)"
Write-Error $msg
$resourceOutput += $msg
}
Write-Verbose "Validating Location info for $ResourceProviderNameSpace\$ResourceTypeName"
try {
if (-not $resource.location) {
Write-Warning "Location property is not required or has not been set for $ResourceProviderNameSpace\$ResourceTypeName."
}
else {
$locationToCheck = Get-PropertyValue $resource.location $Template $resource
$supportedLocations = $ResourceTypeProperties.Locations
$notSupported = CompareValues $locationToCheck $supportedLocations
if (($notSupported) -and ($locationToCheck -notlike "*resourceGroup().location*")) {
if ($notSupported.NoneSupported) {
$msg = "NotSupported: Location (Resource type: $($resource.type)). Not supported values - $($notSupported.NotSupportedValues). It is recommended to set it as resourceGroup().location."
}
else {
$msg = "Warning: Location (Resource type: $($resource.type)). Not supported values - $($notSupported.NotSupportedValues). It is recommended to set it as resourceGroup().location."
}
Write-Warning $msg
$resourceOutput += $msg
}
elseif ((-not $notSupported) -and ($locationToCheck -notlike "*resourceGroup().location*")) {
$msg = "Recommend: Location (Resource type: $($resource.type)). It is recommended to set it as resourceGroup().location."
Write-Warning $msg
$resourceOutput += $msg
}
}
}
catch {
$msg = "Exception: Location (Resource type: $($resource.type)). $($_.Exception.Message)"
Write-Error $msg
$resourceOutput += $msg
}
Write-Verbose "Process VMImages"
if ($IncludeComputeCapabilities) {
if ($ResourceTypeName -Like '*extensions') {
Write-Verbose "Validating VMExtension $ResourceProviderNameSpace/$ResourceTypeName"
if (-not $capabilities.VMExtensions) {
Write-Warning "Warning: No VMExtensions found in Capabilities json file. Run Get-AzureRMCloudCapabilities with -IncludeComputeCapabilities"
}
else {
try {
$templateextPublisher = Get-PropertyValue $resource.properties.publisher $Template $resource
$templateextType = Get-PropertyValue $resource.properties.type $Template $resource
$templateextVersion = Get-PropertyValue $resource.properties.typeHandlerVersion $Template $resource
$supportedPublisher = $capabilities.VMExtensions | Where-Object { $_.publisher -eq $templateextPublisher }
if (-not $supportedPublisher) {
$msg = "NotSupported: VMExtension publisher '$templateextPublisher'"
Write-Warning $msg
$resourceOutput += $msg
}
else {
$supportedType = $supportedPublisher.types| Where-Object { $_.type -eq $templateextType }
if (-not $supportedType) {
$msg = "NotSupported: VMExtension type '$templateextPublisher\$templateextType'"
Write-Error $msg
$resourceOutput += $msg
}
else {
$supportedVersions = $supportedType.versions
if ($templateextVersion -notin $supportedVersions) {
if ($templateextVersion.Split(".").Count -eq 2) {
$templateextVersion = $templateextVersion + ".0.0"
}
elseif ($templateextVersion.Split(".").Count -eq 3) {
$templateextVersion = $templateextVersion + ".0"
}
if ($templateextVersion -notin $supportedVersions) {
$autoupgradeSupported = $supportedVersions | Where-Object { (([version]$_).Major -eq ([version]$templateextVersion).Major) -and (([version]$_).Minor -ge ([version]$templateextVersion).Minor) }
if ($autoupgradeSupported) {
if ((-not $resource.properties.autoupgrademinorversion) -or ($resource.properties.autoupgrademinorversion -eq $false)) {
$msg = "Warning: Exact Match for VMExtension version ($templateextPublisher\$templateextType\$templateextVersion) not found in supported versions ($supportedVersions). It is recommended to set autoupgrademinorversion property to true."
Write-warning $msg
$resourceOutput += $msg
}
}
else {
$msg = "Warning: VMExtension version ($templateextPublisher\$templateextType\$templateextVersion) not found in supported versions ($supportedVersions)."
Write-Warning $msg
$resourceOutput += $msg
}
}
}
}
}
}
catch {
$msg = "Exception: VMExtension. $($_.Exception.Message)"
Write-Error $msg
$resourceOutput += $msg
}
}
}
if ($resource.type -eq "Microsoft.Compute/virtualMachines") {
Write-Verbose "Validating VMImages"
if ($resource.properties.storageprofile.imagereference) {
if (-not $capabilities.VMImages) {
Write-Warning "Warning: No VMImages found in Capabilities.json. Run Get-AzureRMCloudCapabilities with -IncludeComputeCapabilities"
}
else {
try {
$templateImagePublisher = Get-PropertyValue $resource.properties.storageprofile.imagereference $Template $resource "publisher"
$notSupported = CompareValues $templateImagePublisher $capabilities.VMImages.publisherName
if ($notSupported) {
if ($notSupported.NoneSupported) {
$msg = "NotSupported: VMImage publisher (Resource type: $($resource.type)). Not supported values - $($notSupported.NotSupportedValues)"
}
else {
$msg = "Warning: VMImage publisher (Resource type: $($resource.type)). Not supported values - $($notSupported.NotSupportedValues)"
}
Write-Warning $msg
$resourceOutput += $msg
}
else {
if ($templateImagePublisher.GetType().Name -eq "Object[]") {
$templateImagePublisher = $templateImagePublisher[0]
}
$supportedPublisher = $capabilities.VMImages | Where-Object { $_.publisherName -eq $templateImagePublisher }
try {
$templateImageOffer = Get-PropertyValue $resource.properties.storageprofile.imagereference $Template $resource "offer"
$notSupported = CompareValues $templateImageOffer $supportedPublisher.Offers.offerName
if ($notSupported) {
if ($notSupported.NoneSupported) {
$msg = "NotSupported: VMImage Offer (Publisher: $templateImagePublisher). Not supported values - $($notSupported.NotSupportedValues)"
}
else {
$msg = "Warning: VMImage Offer (Publisher: $templateImagePublisher). Not supported values - $($notSupported.NotSupportedValues)"
}
Write-Warning $msg
$resourceOutput += $msg
}
else {
if ($templateImageOffer.GetType().Name -eq "Object[]") {
$templateImageOffer = $templateImageOffer[0]
}
$supportedOffer = $supportedPublisher.Offers | Where-Object { $_.offerName -eq $templateImageOffer }
try {
$templateImagesku = Get-PropertyValue $resource.properties.storageprofile.imagereference $Template $resource "sku"
$notSupported = CompareValues $templateImagesku $supportedOffer.skus.skuName
if ($notSupported) {
if ($notSupported.NoneSupported) {
$msg = "NotSupported: VMImage SKu (Offer: $templateImagePublisher\$templateImageOffer). Not supported values - $($notSupported.NotSupportedValues)"
}
else {
$msg = "Warning: VMImage SKu (Offer: $templateImagePublisher\$templateImageOffer). Not supported values - $($notSupported.NotSupportedValues)"
}
Write-Warning $msg
$resourceOutput += $msg
}
else {
if ($templateImagesku.GetType().Name -eq "Object[]") {
$templateImagesku = $templateImagesku[0]
}
$supportedSku = $supportedOffer.skus | Where-Object { $_.skuName -eq $templateImagesku }
try {
$templateImageskuVersion = Get-PropertyValue $resource.properties.storageprofile.imagereference $Template $resource "version"
$notSupported = CompareValues $templateImageskuVersion $supportedSku.versions
if (($templateImageskuVersion -ne "latest") -and ($notSupported)) {
if ($notSupported.NoneSupported) {
$msg = "NotSupported: VMImage SKu version (Sku: $templateImagePublisher\$templateImageOffer\$templateImageSku). Not supported values - $($notSupported.NotSupportedValues)"
}
else {
$msg = "Warning: VMImage SKu version (Sku: $templateImagePublisher\$templateImageOffer\$templateImageSku). Not supported values - $($notSupported.NotSupportedValues)"
}
Write-Warning $msg
$resourceOutput += $msg
}
elseif (($templateImageskuVersion -ne "latest") -and (-not $notSupported)) {
$msg = "Recommend: It is recommended to set storageprofile.imagereference.version to 'latest'"
Write-Warning $msg
$resourceOutput += $msg
}
}
catch {
$msg = "Exception: VMImage SKu version (Sku: $templateImagePublisher\$templateImageOffer\$templateImageSku). $($_.Exception.Message)"
Write-Error $msg
$resourceOutput += $msg
}
}
}
catch {
$msg = "Exception: VMImage SKu (Offer: $templateImagePublisher\$templateImageOffer). $($_.Exception.Message)"
Write-Error $msg
$resourceOutput += $msg
}
}
}
catch {
$msg = "Exception: VMImage Offer (Publisher: $templateImagePublisher). $($_.Exception.Message)"
Write-Error $msg
$resourceOutput += $msg
}
}
}
catch {
$msg = "Exception: VMImage publisher (Resource type: $($resource.type)). $($_.Exception.Message)"
Write-Error $msg
$resourceOutput += $msg
}
}
}
if (-not $capabilities.VMSizes) {
Write-Warning "Warning: No VMSizes found in Capabilities.json. Run Get-AzureRMCloudCapabilities with -IncludeComputeCapabilities"
}
else {
try {
$templateVMSize = Get-PropertyValue $resource.properties.hardwareprofile $Template $resource "vmSize"
$notSupported = CompareValues $templateVMSize $capabilities.VMSizes
if ($notSupported) {
if ($notSupported.NoneSupported) {
$msg = "NotSupported: VMSize. Not Supported Values - $($notSupported.NotSupportedValues)"
}
else {
$msg = "Warning: VMSize. Not Supported Values - $($notSupported.NotSupportedValues)"
}
Write-Warning $msg
$resourceOutput += $msg
}
}
catch {
$msg = "Exception: VMSize. $($_.Exception.Message)"
Write-Error $msg
$resourceOutput += $msg
}
}
}
}
if ($IncludeStorageCapabilities) {
if ($resource.type -eq "Microsoft.Storage/StorageAccounts") {
Write-Verbose "Validating StorageAcount Sku"
if (-not $capabilities.StorageSkus) {
Write-Warning "Warning: No StorageSkus found in Capabilities.json. Run Get-AzureRMCloudCapabilities with -IncludeStorageCapabilities"
}
else {
try {
$templateStorageKind = Get-PropertyValue $resource.kind $Template $resource
$supportedKind = ($capabilities.StorageSkus | Where-Object { $_.kind -eq $templateStorageKind }).kind
$notSupported = CompareValues $templateStorageKind $supportedKind
if ($notSupported) {
if ($notSupported.NoneSupported) {
$msg = "NotSupported: Storage kind. Not Supported Values - $($notSupported.NotSupportedValues)"
}
else {
$msg = "Warning: Storage kind. Not Supported Values - $($notSupported.NotSupportedValues)"
}
Write-Warning $msg
$resourceOutput += $msg
}
else {
$templateStorageSku = Get-PropertyValue $resource.sku.Name $Template $resource
$supportedSkus = ($capabilities.StorageSkus | Where-Object { $_.kind -eq $templateStorageKind }).skus
$notSupported = CompareValues $templateStorageSku $supportedSkus
if ($notSupported) {
if ($notSupported.NoneSupported) {
$msg = "NotSupported: Storage sku '$templateStorageKind\$templateStorageSku'. Not Supported Values - $($notSupported.NotSupportedValues)"
}
else {
$msg = "Warning: Storage sku '$templateStorageKind\$templateStorageSku'. Not Supported Values - $($notSupported.NotSupportedValues)"
}
Write-Warning $msg
$resourceOutput += $msg
}
}
}
catch {
Write-Warning "$($_.Exception.Message). Proceeding to see if there is Storage Accountype"
try {
$templateStorageAccountType = Get-PropertyValue $resource.properties.accountType $Template $resource
$supportedTypes = ($capabilities.StorageSkus | Where-Object { $_.kind -eq 'Storage' }).skus
$notSupported = CompareValues $templateStorageAccountType $supportedTypes
if ($notSupported) {
if ($notSupported.NoneSupported) {
$msg = "NotSupported: Storage AccountType. Not Supported Values - $($notSupported.NotSupportedValues)"
}
else {
$msg = "Warning: Storage AccountType. Not Supported Values - $($notSupported.NotSupportedValues)"
}
Write-Warning $msg
$resourceOutput += $msg
}
}
catch {
$msg = "Exception: Storage AccountType. $($_.Exception.Message)"
Write-Error $msg
$resourceOutput += $msg
}
}
}
}
}
}
return $resourceOutput
}