Bicep.psm1
#Region './Classes/10.BicepDiagnosticLevel.ps1' -1 enum BicepDiagnosticLevel { Off Info Warning Error } #EndRegion './Classes/10.BicepDiagnosticLevel.ps1' 7 #Region './Classes/20.BicepDiagnosticEntry.ps1' -1 class BicepDiagnosticEntry { [string] $LocalPath [int[]] $Position [BicepDiagnosticLevel] $Level [string] $Code [string] $Message BicepDiagnosticEntry ([object]$Entry) { if($Entry.pstypenames[0] -ne 'PSBicep.Core.DiagnosticEntry') { throw "Requires type 'PSBicep.Core.DiagnosticEntry'" } $this.LocalPath = $Entry.LocalPath $this.Position = $Entry.Position[0], $Entry.Position[1] $this.Level = $Entry.Level.ToString() $this.Code = $Entry.Code $this.Message = $Entry.Message } } #EndRegion './Classes/20.BicepDiagnosticEntry.ps1' 19 #Region './Classes/BicepTypesCompleters.ps1' -1 class BicepResourceProviderCompleter : System.Management.Automation.IArgumentCompleter{ [System.Collections.Generic.IEnumerable[System.Management.Automation.CompletionResult]] CompleteArgument( [string] $CommandName, [string] $ParameterName, [string] $wordToComplete, [System.Management.Automation.Language.CommandAst] $CommandAst, [Collections.IDictionary] $fakeBoundParameters ) { [array]$ResourceProviders = (GetBicepTypes).ResourceProvider | Where-Object { $_ -like "$wordToComplete*" } | Sort-Object -Unique $list = [System.Collections.Generic.List[System.Management.Automation.CompletionResult]]::new() foreach ($ResourceProvider in $ResourceProviders) { $CompletionText = $ResourceProvider $ListItemText = $ResourceProvider $ResultType = [System.Management.Automation.CompletionResultType]::ParameterValue $ToolTip = $ResourceProvider $obj = [System.Management.Automation.CompletionResult]::new($CompletionText, $ListItemText, $ResultType, $Tooltip) $list.add($obj) } return $list } } class BicepResourceCompleter : System.Management.Automation.IArgumentCompleter{ [System.Collections.Generic.IEnumerable[System.Management.Automation.CompletionResult]] CompleteArgument( [string] $CommandName, [string] $ParameterName, [string] $wordToComplete, [System.Management.Automation.Language.CommandAst] $CommandAst, [Collections.IDictionary] $fakeBoundParameters ) { if ($fakeBoundParameters.ContainsKey('ResourceProvider')) { [array]$Resources = GetBicepTypes | Where-Object { $_.ResourceProvider -eq $fakeBoundParameters.ResourceProvider -and $_.Resource -like "$wordToComplete*" } | Select-Object -ExpandProperty Resource -Unique | Sort-Object } else { [array]$Resources = (GetBicepTypes).Resource | Where-Object { $_ -like "$wordToComplete*" } | Sort-Object -Unique } $list = [System.Collections.Generic.List[System.Management.Automation.CompletionResult]]::new() foreach ($Resource in $Resources) { $CompletionText = $Resource $ListItemText = $Resource $ResultType = [System.Management.Automation.CompletionResultType]::ParameterValue $ToolTip = '{0}/{1}' -f $fakeBoundParameters.ResourceProvider, $Resource $ToolTip = $ToolTip.TrimEnd('/') $obj = [System.Management.Automation.CompletionResult]::new($CompletionText, $ListItemText, $ResultType, $Tooltip) $list.add($obj) } return $list } } class BicepResourceChildCompleter : System.Management.Automation.IArgumentCompleter{ [System.Collections.Generic.IEnumerable[System.Management.Automation.CompletionResult]] CompleteArgument( [string] $CommandName, [string] $ParameterName, [string] $wordToComplete, [System.Management.Automation.Language.CommandAst] $CommandAst, [Collections.IDictionary] $fakeBoundParameters ) { if ($fakeBoundParameters.ContainsKey('ResourceProvider') -and $fakeBoundParameters.ContainsKey('Resource')) { $Children = GetBicepTypes | Where-Object { $_.ResourceProvider -eq $fakeBoundParameters.ResourceProvider -and $_.Resource -eq $fakeBoundParameters.Resource -and $fakeBoundParameters.Child -like "$wordToComplete*" } | Select-Object -ExpandProperty Child -Unique | Sort-Object } else { $Children = (GetBicepTypes).Child | Where-Object { $_ -like "$wordToComplete*" } | Sort-Object -Unique -Descending } $list = [System.Collections.Generic.List[System.Management.Automation.CompletionResult]]::new() foreach ($Child in $Children) { $CompletionText = $Child $ListItemText = $Child $ResultType = [System.Management.Automation.CompletionResultType]::ParameterValue $ToolTip = '{0}/{1}/{2}' -f $fakeBoundParameters.ResourceProvider, $fakeBoundParameters.Resource, $Child $ToolTip = $ToolTip.TrimEnd('/') $obj = [System.Management.Automation.CompletionResult]::new($CompletionText, $ListItemText, $ResultType, $Tooltip) $list.add($obj) } return $list } } class BicepResourceApiVersionCompleter : System.Management.Automation.IArgumentCompleter{ [System.Collections.Generic.IEnumerable[System.Management.Automation.CompletionResult]] CompleteArgument( [string] $CommandName, [string] $ParameterName, [string] $wordToComplete, [System.Management.Automation.Language.CommandAst] $CommandAst, [Collections.IDictionary] $fakeBoundParameters ) { if ($fakeBoundParameters.ContainsKey('ResourceProvider') -and $fakeBoundParameters.ContainsKey('Resource')) { $ApiVersions = GetBicepTypes | Where-Object { $_.ResourceProvider -eq $fakeBoundParameters.ResourceProvider -and $_.Resource -eq $fakeBoundParameters.Resource -and $fakeBoundParameters.ApiVersion -like "$wordToComplete*" } | Select-Object -ExpandProperty ApiVersion -Unique | Sort-Object -Descending } elseif ($fakeBoundParameters.ContainsKey('ResourceProvider') -and $fakeBoundParameters.ContainsKey('Resource') -and $fakeBoundParameters.ContainsKey('Child')) { $ApiVersions = GetBicepTypes | Where-Object { $_.ResourceProvider -eq $fakeBoundParameters.ResourceProvider -and $_.Resource -eq $fakeBoundParameters.Resource -and $_.Child -eq $fakeBoundParameters.Child -and $fakeBoundParameters.ApiVersion -like "$wordToComplete*" } | Select-Object -ExpandProperty ApiVersion -Unique | Sort-Object -Descending } else { $ApiVersions = (GetBicepTypes).ApiVersion | Where-Object { $_ -like "$wordToComplete*" } | Sort-Object -Unique -Descending } $list = [System.Collections.Generic.List[System.Management.Automation.CompletionResult]]::new() foreach ($ApiVersion in $ApiVersions) { $CompletionText = $ApiVersion $ListItemText = $ApiVersion $ResultType = [System.Management.Automation.CompletionResultType]::ParameterValue $ToolTip = '{0}/{1}/{2}' -f $fakeBoundParameters.ResourceProvider, $fakeBoundParameters.Resource, $fakeBoundParameters.Child $ToolTip = $ToolTip.TrimEnd('/') + "@$ApiVersion" $obj = [System.Management.Automation.CompletionResult]::new($CompletionText, $ListItemText, $ResultType, $Tooltip) $list.add($obj) } return $list } } class BicepTypeCompleter : System.Management.Automation.IArgumentCompleter{ [System.Collections.Generic.IEnumerable[System.Management.Automation.CompletionResult]] CompleteArgument( [string] $CommandName, [string] $ParameterName, [string] $wordToComplete, [System.Management.Automation.Language.CommandAst] $CommandAst, [Collections.IDictionary] $fakeBoundParameters ) { $Types = (GetBicepTypes).FullName | Where-Object { $_ -like "$wordToComplete*" } | Sort-Object -Unique -Descending $list = [System.Collections.Generic.List[System.Management.Automation.CompletionResult]]::new() foreach ($Type in $Types) { $CompletionText = $Type $ListItemText = $Type $ResultType = [System.Management.Automation.CompletionResultType]::ParameterValue $ToolTip = $Type $obj = [System.Management.Automation.CompletionResult]::new($CompletionText, $ListItemText, $ResultType, $Tooltip) $list.add($obj) } return $list } } #EndRegion './Classes/BicepTypesCompleters.ps1' 180 #Region './Classes/BicepVersionsCompleters.ps1' -1 class BicepVersionCompleter : System.Management.Automation.IArgumentCompleter{ [System.Collections.Generic.IEnumerable[System.Management.Automation.CompletionResult]] CompleteArgument( [string] $CommandName, [string] $ParameterName, [string] $wordToComplete, [System.Management.Automation.Language.CommandAst] $CommandAst, [Collections.IDictionary] $fakeBoundParameters ) { [array]$BicepVersions = (ListBicepVersions) | Where-Object { $_ -like "$wordToComplete*" } $list = [System.Collections.Generic.List[System.Management.Automation.CompletionResult]]::new() foreach ($BicepVersion in $BicepVersions) { $CompletionText = $BicepVersion $ListItemText = $BicepVersion $ResultType = [System.Management.Automation.CompletionResultType]::ParameterValue $ToolTip = $BicepVersion $obj = [System.Management.Automation.CompletionResult]::new($CompletionText, $ListItemText, $ResultType, $Tooltip) $list.add($obj) } return $list } } #EndRegion './Classes/BicepVersionsCompleters.ps1' 29 #Region './Private/AssertAzureConnection.ps1' -1 function AssertAzureConnection { [CmdletBinding()] param ( [Parameter(Mandatory)] [hashtable]$TokenSplat, [Parameter()] [string]$CertificatePath, [Parameter()] [PSBicep.Models.BicepConfigInfo]$BicepConfig, [Parameter()] [string]$Resource = 'https://management.azure.com' ) $LocalTokenSplat = $TokenSplat.Clone() $LocalTokenSplat['Resource'] = $Resource $NotConnectedErrorMessage = 'Not connected to Azure. Please connect to Azure by running Connect-Bicep before running this command.' # Connect-Bicep has not been run and we can try to get a token based on credential precedence. if ($script:TokenSource -ne 'PSBicep' -and $null -ne $BicepConfig) { try { $NewToken = Get-AzToken @LocalTokenSplat -CredentialPrecedence $BicepConfig.cloud.credentialPrecedence -ErrorAction 'Stop' $script:Token = $NewToken # Only make assignment to script scope if no exception is thrown return } catch { Write-Error -Exception $_.Exception -Message $NotConnectedErrorMessage -ErrorAction 'Stop' } } # If token is null, about to expire or has wrong resource/audience, try to refresh it if (-not (ValidateAzureToken -Token $script:Token -Resource $Resource)) { try { if ($CertificatePath) { $Certificate = Get-Item $CertificatePath # If platform is Windows, the Certificate is a cert object, otherwise a file if ($Certificate -is [System.Security.Cryptography.X509Certificates.X509Certificate2]) { $LocalTokenSplat['ClientCertificate'] = Get-Item $CertificatePath } else { $LocalTokenSplat['ClientCertificatePath'] = $CertificatePath } } # If we reach this point, we know that we have run Connect-Bicep # We can therefore safely remove parameters from the local LocalTokenSplat # The non-interative token we get here should only be session-local from the previous auth if ($LocalTokenSplat.ContainsKey('Interactive')) { $LocalTokenSplat.Remove('Interactive') } if ($LocalTokenSplat.ContainsKey('ClientId')) { $LocalTokenSplat.Remove('ClientId') } $NewToken = Get-AzToken @LocalTokenSplat -ErrorAction 'Stop' $script:Token = $NewToken # Only make assignment to script scope if no exception is thrown } catch { Write-Error -Exception $_.Exception -Message $NotConnectedErrorMessage -ErrorAction 'Stop' } } } #EndRegion './Private/AssertAzureConnection.ps1' 63 #Region './Private/CompareBicepVersion.ps1' -1 function CompareBicepVersion { $installedVersion = InstalledBicepVersion $latestVersion = ListBicepVersions -Latest if ($installedVersion -eq $latestVersion) { $true } else { $false } } #EndRegion './Private/CompareBicepVersion.ps1' 12 #Region './Private/ConvertToHashtable.ps1' -1 function ConvertToHashtable { param( [Parameter(ValueFromPipeline)] [object] $InputObject, [switch] $Ordered ) process { # If InputObject is string or valuetype, don't recurse, just return the value. if ( $null -eq $InputObject -or $InputObject.GetType().FullName -eq 'System.String' -or $InputObject.GetType().IsValueType -or $InputObject -is [System.Collections.Specialized.OrderedDictionary] -or $InputObject -is [System.Collections.Hashtable] ) { return $InputObject } # Else, create a hashtable and loop over properties. if ($Ordered.IsPresent) { $HashTable = [ordered]@{} } else { $HashTable = @{} } foreach ($Prop in $InputObject.psobject.Properties) { if ( $null -eq $Prop.Value -or $Prop.TypeNameOfValue -eq 'System.String' -or $Prop.Value.GetType().IsValueType -or $InputObject -is [System.Collections.Specialized.OrderedDictionary] -or $InputObject -is [System.Collections.Hashtable] ) { $HashTable.Add($Prop.Name, $Prop.Value) } else { if ($Prop.TypeNameOfValue -eq 'System.Object[]' -and (-not $Prop.Value)) { $Value = @() } elseif ($Prop.TypeNameOfValue -eq 'System.Object[]' -and $Prop.Value) { $Value = @($Prop.Value) } else { $Value = $Prop.Value | ConvertToHashtable -Ordered:$Ordered } $HashTable.Add($Prop.Name, $Value) } } return $HashTable } } #EndRegion './Private/ConvertToHashtable.ps1' 55 #Region './Private/GenerateParameterFile.ps1' -1 function GenerateParameterFile { [CmdletBinding(DefaultParameterSetName = 'FromFile', SupportsShouldProcess)] param ( [Parameter(Mandatory, ParameterSetName = 'FromFile')] [object]$File, [Parameter(Mandatory, ParameterSetName = 'FromContent')] [ValidateNotNullOrEmpty()] [string]$Content, [Parameter(Mandatory, ParameterSetName = 'FromContent')] [ValidateNotNullOrEmpty()] [string]$DestinationPath, [Parameter(ParameterSetName = 'FromFile')] [Parameter(ParameterSetName = 'FromContent')] [string]$Parameters ) if ($PSCmdlet.ParameterSetName -eq 'FromFile') { $fileName = $file.Name -replace ".bicep", "" $ARMTemplate = Get-Content "$($file.DirectoryName)\$filename.json" -Raw | ConvertFrom-Json } elseif ($PSCmdlet.ParameterSetName -eq 'FromContent') { $ARMTemplate = $Content | ConvertFrom-Json } $parameterBase = [ordered]@{ '$schema' = 'https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#' 'contentVersion' = '1.0.0.0' } $parameterNames = $ARMTemplate.Parameters.psobject.Properties.Name if (-not $parameterNames) { switch($PSCmdlet.ParameterSetName) { 'FromFile' { Write-Host "No parameters declared in the specified bicep file $($file.FullName)." } 'FromContent' { Write-Host "No parameters declared in the specified bicep content." } Default { Write-Error "Unable to generate parameter file. Unknown parameter set: $($PSCmdlet.ParameterSetName)" } } break } else { $parameterHash = [ordered]@{} foreach ($parameterName in $parameterNames) { $ParameterObject = $ARMTemplate.Parameters.$ParameterName if (($Parameters -eq "Required" -and $null -eq $ParameterObject.defaultValue) -or ($Parameters -eq "All")) { if ($null -eq $ParameterObject.defaultValue) { if ($ParameterObject.type -eq 'Array') { $defaultValue = @() } elseif ($ParameterObject.type -eq 'Object') { $defaultValue = @{} } elseif ($ParameterObject.type -eq 'int') { $defaultValue = 0 } else { $defaultValue = "" } } elseif ($ParameterObject.defaultValue -like "*()*") { $defaultValue = "" } else { $defaultValue = $ParameterObject.defaultValue } $parameterHash[$parameterName] = @{ value = $defaultValue } } } $parameterBase['parameters'] = $parameterHash $ConvertedToJson = ConvertTo-Json -InputObject $parameterBase -Depth 100 switch ($PSCmdlet.ParameterSetName) { 'FromFile' { Out-File -InputObject $ConvertedToJson -FilePath "$($file.DirectoryName)\$filename.parameters.json" -WhatIf:$WhatIfPreference } 'FromContent' { Out-File -InputObject $ConvertedToJson -FilePath $DestinationPath -WhatIf:$WhatIfPreference } Default { Write-Error "Unable to generate parameter file. Unknown parameter set: $($PSCmdlet.ParameterSetName)" } } } } #EndRegion './Private/GenerateParameterFile.ps1' 97 #Region './Private/GetBicepTypes.ps1' -1 function GetBicepTypes { [CmdletBinding()] param ( [ValidateNotNullOrEmpty()] [string]$Path ) if (-not [string]::IsNullOrEmpty($Path)) { Write-Verbose "Importing Bicep Types" $types = Get-Content -Path $Path | ConvertFrom-Json -AsHashtable $allResourceProviders = [System.Collections.ArrayList]::new() $allResourceProvidersTable = @{} foreach ($type in $types) { # Type looks like this: Microsoft.Aad/domainServicess@2017-01-01 # We want to split here: ^ ^ # Or like this: Microsoft.ApiManagement/service/certificates@2019-12-01 # Then we split here: ^ ^ ^ # First check if we have three parts before the @ # In that case the last one should be the child if (($type -split '/' ).count -eq 3) { $child = ( ($type -split '@') -split '/' )[2] } else { $child = $null } $FullTypeName = ($type -split '@')[0] $ApiVersion = ($type -split '@')[1] $ResourceProviders = [PSCustomObject]@{ ResourceProvider = ( ($type -split '@') -split '/' )[0] Resource = ( ($type -split '@') -split '/' )[1] Child = $child ApiVersion = $ApiVersion FullName = $type } if($allResourceProvidersTable[$FullTypeName] -isnot [hashtable]) { $allResourceProvidersTable[$FullTypeName] = @{} } if($ApiVersion -gt ($allResourceProvidersTable[$FullTypeName].Keys.Where{$_ -ne 'latest'} | Sort-Object -Descending | Select-Object -First 1)) { $allResourceProvidersTable[$FullTypeName]['latest'] = $ResourceProviders } $allResourceProvidersTable[$FullTypeName][$ApiVersion] = $ResourceProviders $null = $allResourceProviders.Add($ResourceProviders) } Set-Variable -Name 'BicepResourceProviders' -Value $allResourceProviders -Scope 'Script' Set-Variable -Name 'BicepResourceProvidersTable' -Value $allResourceProvidersTable -Scope 'Script' } Write-Output -InputObject $Script:BicepResourceProviders } #EndRegion './Private/GetBicepTypes.ps1' 59 #Region './Private/GetGitHubReleaseVersion.ps1' -1 function GetGithubReleaseVersion { [CmdletBinding()] param( [string]$Organization, [string]$Repository, [switch]$Latest ) $Url = 'https://api.github.com/repos/{0}/{1}/releases' -f $Organization, $Repository if ($Latest.IsPresent) { $Url = '{0}/latest' -f $Url } try { $Versions = Invoke-RestMethod -Uri $Url -ErrorAction 'Stop' return ($Versions.tag_name -replace '[v]', '' | Foreach-Object -Process {$_ -as [version]}) } catch { Write-Error -Message "Could not get version of $Organization/$Repository from GitHub. $_" -Category ObjectNotFound } } #EndRegion './Private/GetGitHubReleaseVersion.ps1' 20 #Region './Private/InstalledBicepVersion.ps1' -1 function InstalledBicepVersion { if (TestBicep) { $Version=((bicep --version) -split "\s+")[3] "$Version" } else { "Not installed" } } #EndRegion './Private/InstalledBicepVersion.ps1' 9 #Region './Private/ListBicepVersions.ps1' -1 function ListBicepVersions { [CmdletBinding()] param ( [switch]$Latest ) if($null -eq $Script:AvailableBicepVersions) { # Get all available versions try { $Script:AvailableBicepVersions = GetGithubReleaseVersion -Organization 'Azure' -Repository 'bicep' -ErrorAction 'Stop' } catch { $Script:AvailableBicepVersions = @() Write-Verbose "Failed to retrieve versions with error: $_" } } if($null -eq $Script:LatestBicepVersion) { # Call the /latest endpoint to get only the latest version try { $Script:LatestBicepVersion = GetGithubReleaseVersion -Organization 'Azure' -Repository 'bicep' -Latest -ErrorAction 'Stop' } catch { $Script:LatestBicepVersion = '' Write-Verbose "Failed to retrieve latest version with error: $_" } } if($Latest.IsPresent) { if($Script:LatestBicepVersion -is [version]) { Write-Output -InputObject $Script:LatestBicepVersion } } else { Write-Output -InputObject $Script:AvailableBicepVersions } } #EndRegion './Private/ListBicepVersions.ps1' 37 #Region './Private/NewMDMetadata.ps1' -1 function NewMDMetadata { [CmdletBinding()] param( [object]$Metadata ) if ($null -eq $Metadata) { return 'n/a' } $MetadataNames = ($Metadata | Get-Member -MemberType NoteProperty).Name | Where-Object {$_ -NotLike '_*'} $MDMetadata = NewMDTableHeader -Headers 'Name', 'Value' foreach ($var in $MetadataNames) { $Param = $Metadata.$var if ($Param.GetType().Name -eq 'PSCustomObject') { $tempArr = @() $tempObj = ($Param | Get-Member -MemberType NoteProperty).Name foreach ($item in $tempObj) { $tempArr += $item + ': ' + $Param.$($item) + '<br/>' } $MDMetadata += "| $var | $tempArr |`n" } else { $MDMetadata += "| $var | $Param |`n" } } $MDMetadata = $MDMetadata -replace ', ', '<br/>' $MDMetadata } #EndRegion './Private/NewMDMetadata.ps1' 34 #Region './Private/NewMDModules.ps1' -1 function NewMDModules { [CmdletBinding()] param( [object[]]$Modules ) if (-not $Modules -or $Modules.Count -eq 0) { return 'n/a' } $MDModules = NewMDTableHeader -Headers 'Name', 'Path' foreach ($Module in $Modules) { $MDModules += "| $($Module.Name) | $($Module.Path) |`n" } $MDModules } #EndRegion './Private/NewMDModules.ps1' 19 #Region './Private/NewMDOutputs.ps1' -1 function NewMDOutputs { [CmdletBinding()] param( [object]$Outputs ) if ($null -eq $Outputs) { return 'n/a' } $OutputNames = ($Outputs | Get-Member -MemberType NoteProperty).Name $MDOutputs = NewMDTableHeader -Headers 'Name', 'Type', 'Value' foreach ($OutputName in $OutputNames) { $OutputValues = $Outputs.$OutputName $MDOutputs += "| $OutputName | $($OutputValues.type) | $($OutputValues.value) |`n" } $MDOutputs } #EndRegion './Private/NewMDOutputs.ps1' 21 #Region './Private/NewMDParameters.ps1' -1 function NewMDParameters { [CmdletBinding()] param( [object]$Parameters ) if ($null -eq $Parameters) { return 'n/a' } $ParameterNames = ($Parameters | Get-Member -MemberType NoteProperty).Name $MDParameters = NewMDTableHeader -Headers 'Name', 'Type', 'AllowedValues', 'Metadata' foreach ($Parameter in $ParameterNames) { $Param = $Parameters.$Parameter $MDParameters += "| $Parameter | $($Param.type) | $( if ($Param.allowedValues) { forEach ($value in $Param.allowedValues) { "$value <br/>" } } else { "n/a" } ) | $( forEach ($item in $Param.metadata) { $res = $item.PSObject.members | Where-Object { $_.MemberType -eq 'NoteProperty' } if ($null -ne $res) { $res.Name + ': ' + $res.Value + '<br/>' } }) |`n" } $MDParameters } #EndRegion './Private/NewMDParameters.ps1' 38 #Region './Private/NewMDProviders.ps1' -1 function NewMDProviders { [CmdletBinding()] param( [Parameter(Mandatory)] [AllowEmptyCollection()] [AllowNull()] [object[]]$Resources, [Parameter()] [string]$LanguageVersion = '1.0' ) if (-not $Resources -or $Resources.Count -eq 0) { return 'n/a' } # If language version is 2.0, $Resources is a dictionary and we need to adapt the object if ($LanguageVersion -eq '2.0') { $Resources = foreach ($ResourceName in $Resources[0].psobject.properties.name) { $Resource = $Resources."$ResourceName" $Hash = @{} foreach ($PropertyName in $Resource.psobject.properties.name) { $Hash[$PropertyName] = $Resource."$PropertyName" } [pscustomobject]$Hash } } $MDProviders = NewMDTableHeader -Headers 'Type', 'Version' $Providers = @() foreach ($Resource in $Resources) { $Provider = "$($Resource.Type)@$($Resource.apiVersion)" if ($Providers -notcontains $Provider) { $MDProviders += "| $($Resource.Type) | $($Resource.apiVersion) |`n" } $Providers += $Provider } $MDProviders } #EndRegion './Private/NewMDProviders.ps1' 42 #Region './Private/NewMDResources.ps1' -1 function NewMDResources { [CmdletBinding()] param( [Parameter(Mandatory)] [AllowEmptyCollection()] [AllowNull()] [object[]]$Resources, [Parameter()] [string]$LanguageVersion = '1.0' ) if (-not $Resources -or $Resources.Count -eq 0) { return 'n/a' } $MDResources = NewMDTableHeader -Headers 'Name', 'Link', 'Location' # If language version is 2.0, $Resources is a dictionary and we need to adapt the object if ($LanguageVersion -eq '2.0') { $Resources = foreach ($ResourceName in $Resources[0].psobject.properties.name) { $Resource = $Resources."$ResourceName" $Hash = @{} foreach ($PropertyName in $Resource.psobject.properties.name) { $Hash[$PropertyName] = $Resource."$PropertyName" } [pscustomobject]$Hash } } foreach ($Resource in $Resources) { try { $URI = Get-BicepApiReference -Type "$($Resource.Type)@$($Resource.apiVersion)" -ReturnUri -Force } catch { # If no uri is found this is the base path for template $URI = 'https://docs.microsoft.com/en-us/azure/templates' } $MDResources += "| $($Resource.name) | [$($Resource.Type)@$($Resource.apiVersion)]($URI) | $($Resource.location) |`n" } $MDResources } #EndRegion './Private/NewMDResources.ps1' 44 #Region './Private/NewMDTableHeader.ps1' -1 function NewMDTableHeader { [CmdletBinding()] param( [string[]]$Headers ) if (-not $Headers -or $Headers.Count -eq 0) { throw 'Headers cannot be empty!' } $r = '|' foreach ($Head in $Headers) { $r += " $Head |" } $r = "$r`n|" 1..($Headers.Count) | ForEach-Object { $r += "----|" } $r = "$r`n" $r } #EndRegion './Private/NewMDTableHeader.ps1' 26 #Region './Private/NewMDVariables.ps1' -1 function NewMDVariables { [CmdletBinding()] param( [object]$Variables ) if ($null -eq $Variables) { return 'n/a' } $VariableNames = ($Variables | Get-Member -MemberType NoteProperty).Name $MDVariables = NewMDTableHeader -Headers 'Name', 'Value' foreach ($var in $VariableNames) { $Param = $Variables.$var $MDVariables += "| $var | $Param |`n" } $MDVariables } #EndRegion './Private/NewMDVariables.ps1' 21 #Region './Private/SearchAzureResourceGraph.ps1' -1 function SearchAzureResourceGraph { [CmdletBinding(DefaultParameterSetName = 'String')] param( # Path to the KQL query file [Parameter(Mandatory, ParameterSetName = 'Path')] [ValidateNotNullOrEmpty()] [string]$QueryPath, # KQL query string [Parameter(Mandatory, ParameterSetName = 'String')] [ValidateNotNullOrEmpty()] [string]$Query, # Subscription IDs to run the query against [Parameter(ParameterSetName = 'Path')] [Parameter(ParameterSetName = 'String')] [string[]]$SubscriptionId, # Management Groups to run the query againstß [Parameter(ParameterSetName = 'Path')] [Parameter(ParameterSetName = 'String')] [string[]]$ManagementGroup, # Scope filter for the authorization [Parameter(ParameterSetName = 'Path')] [Parameter(ParameterSetName = 'String')] [ValidateSet('AtScopeAboveAndBelow', 'AtScopeAndAbove', 'AtScopeAndBelow', 'AtScopeExact')] [string]$AuthorizationScopeFilter = 'AtScopeAndBelow', # Allow partial scopes in the query [Parameter(ParameterSetName = 'Path')] [Parameter(ParameterSetName = 'String')] [bool]$AllowPartialScopes = $false, # PageSize for the query [Parameter(ParameterSetName = 'Path')] [Parameter(ParameterSetName = 'String')] [ValidateRange(1, 1000)] [int]$PageSize = 1000 ) # Ensure only one of SubscriptionId or ManagementGroup is provided if ($PSBoundParameters.ContainsKey('SubscriptionId') -and $PSBoundParameters.ContainsKey('ManagementGroup')) { throw 'KQL Query can only be run against either a Subscription or a Management Group, not both.' } AssertAzureConnection -TokenSplat $script:TokenSplat if ($PSCmdlet.ParameterSetName -eq 'Path') { $Query = Get-Content $QueryPath -Raw } $Uri = 'https://management.azure.com/providers/Microsoft.ResourceGraph/resources?api-version=2022-10-01' $Body = @{ query = $Query options = @{ resultFormat = 'objectArray' authorizationScopeFilter = $AuthorizationScopeFilter allowPartialScopes = $AllowPartialScopes '$top' = $PageSize '$skip' = 0 } } if ($PSBoundParameters.ContainsKey('SubscriptionId')) { $Body['subscriptions'] = @($SubscriptionId) } if ($PSBoundParameters.ContainsKey('ManagementGroup')) { $Body['managementGroups'] = @($ManagementGroup) } $Headers = @{ 'Authorization' = "Bearer $($script:Token.Token)" 'Content-Type' = 'application/json' } $PageParams = @{ Uri = $Uri Body = $Body Headers = $Headers TotalRecords = 0 ResultHeaders = @{} Output = [System.Collections.ArrayList]::new() } while ($PageParams['TotalRecords'] -eq 0 -or $PageParams['TotalRecords'] -gt $PageParams['Body']['options']['$skip']) { $PageParams = GetAzResourceGraphPage @PageParams if($PageParams.Output.Count -gt 0) { Write-Verbose "Outputting $($PageParams.Output.Count) records." Write-Output $PageParams.Output $PageParams.Output.Clear() } } } function GetAzResourceGraphPage { [CmdletBinding()] param ( [string]$Uri, [hashtable]$Body, [hashtable]$Headers, [int]$TotalRecords, [System.Collections.ArrayList]$Output, [hashtable]$ResultHeaders ) # Check if we hit the quota limit if($ResultHeaders.ContainsKey('x-ms-user-quota-remaining') -and $ResultHeaders['x-ms-user-quota-remaining'][0] -lt 1) { # Hit the quota limit, wait before retrying $QuotaResetAfter = $ResultHeaders['x-ms-user-quota-resets-after'] | Select-Object -First 1 $SleepTime = [TimeSpan]$QuotaResetAfter Write-Warning "Quota limit reached. Waiting $($SleepTime.TotalMilliseconds) milliseconds before retrying." Start-Sleep -Milliseconds $SleepTime.TotalMilliseconds } # Check if we are at the end of the records if ($TotalRecords -gt 0 -and $Body['options']['$top'] -gt ($TotalRecords - $Body['options']['$skip'])) { $Body['options']['$top'] = $TotalRecords - $Body['options']['$skip'] } # Check if there are any more records to retrieve if($Body['options']['$top'] -gt 0) { Write-Verbose "Retrieving next page of $($Body['options']['$top']) items." try { $Result = Invoke-WebRequest -Uri $Uri -Method 'POST' -Body ($Body | ConvertTo-Json -Compress) -Headers $Headers -ErrorAction 'Stop' $ResultData = $Result.Content | ConvertFrom-Json -Depth 100 $Output.AddRange($ResultData.data) $TotalRecords = $ResultData.totalRecords $Body['options']['$skip'] += $ResultData.data.Count Write-Verbose "Successfully retrieved $($Body['options']['$skip']) of $TotalRecords records. Next batch sice: $($Body['options']['$top'])." $PageParams = @{ Uri = $Uri Body = $Body Headers = $Headers TotalRecords = $TotalRecords ResultHeaders = $Result.Headers Output = $Output } } catch { try { # If the error is due to payload size, reduce the batch size and call recursively $ErrorDetails = $_.ErrorDetails.Message | ConvertFrom-Json -ErrorAction 'Stop' if ($ErrorDetails.error.details.code -eq 'ResponsePayloadTooLarge') { # There is a payload size limit of 16777216 bytes if($ErrorDetails.error.details.message -match 'Response payload size is (?<ResponseSize>\d+), and has exceeded the limit of (?<Limit>\d+). Please consider querying less data at a time and make paginated call if needed.') { # Estimate new batch size based on the response size ratio to limit, add 1 to be on the safe side. $OriginalBatchSize = $Body['options']['$top'] $ReductionRatio = [Math]::Ceiling($Matches['ResponseSize'] / $Matches['Limit']) + 1 [int]$NewBatchSize = $Body['options']['$top'] / $ReductionRatio $Body['options']['$top'] = $NewBatchSize Write-Verbose "Response payload too large ($($Matches['ResponseSize'])). Retrying with smaller batch size: $($Body['options']['$top'])." for ($i = 0; $i -lt $ReductionRatio; $i++) { $PageParams['Body'] = $Body $PageParams = GetAzResourceGraphPage @PageParams } Write-Verbose "Resetting batch size to original value: $OriginalBatchSize." $PageParams['Body']['options']['$top'] = $OriginalBatchSize } } } catch { Write-Error "Failed to parse error details: $_" -TargetObject $ErrorDetails -ErrorAction 'Stop' } } } return $PageParams } #EndRegion './Private/SearchAzureResourceGraph.ps1' 167 #Region './Private/TestBicep.ps1' -1 function TestBicep { $bicep = (Get-Command bicep -ErrorAction SilentlyContinue) if ($bicep) { $true } else { $false } } #EndRegion './Private/TestBicep.ps1' 10 #Region './Private/TestModuleVersion.ps1' -1 function TestModuleVersion { if ($null -eq $Script:LatestModuleVersion) { try { $Script:LatestModuleVersion = GetGithubReleaseVersion -Organization 'PSBicep' -Repository 'PSBicep' -Latest -ErrorAction 'Stop' $ModuleManifest = Import-PowerShellDataFile -Path $Script:ModuleManifestPath $InstalledModuleVersion = $ModuleManifest.ModuleVersion Write-Verbose "Installed module version: $InstalledModuleVersion" if ($Script:LatestModuleVersion -is [version]) { if ([version]$LatestModuleVersion -gt $InstalledModuleVersion) { Write-Host "A new version of the Bicep module ($Script:LatestModuleVersion) is available. Update the module using 'Update-Module -Name Bicep'" -ForegroundColor 'DarkYellow' } } } catch { $Script:LatestModuleVersion = '' Write-Verbose "Failed to retrieve latest version with error: $_" } } } #EndRegion './Private/TestModuleVersion.ps1' 22 #Region './Private/ValidateAzureToken.ps1' -1 function ValidateAzureToken { param ( [Parameter(Mandatory)] [AllowNull()] $Token, [Parameter()] $Resource = 'https://management.azure.com' ) return ( $null -ne $Token -and $Token.ExpiresOn -ge [System.DateTimeOffset]::Now.AddMinutes(15) -and $Token.Claims['aud'] -eq $Resource ) } #EndRegion './Private/ValidateAzureToken.ps1' 16 #Region './Private/WriteBicepDiagnostic.ps1' -1 function WriteBicepDiagnostic { [CmdletBinding()] param ( [Parameter(ValueFromPipeline)] [BicepDiagnosticEntry[]] $Diagnostic ) process { foreach($DiagnosticEntry in $Diagnostic) { $LocalPath = $DiagnosticEntry.LocalPath [int]$Line = $DiagnosticEntry.Position[0] + 1 [int]$Character = $DiagnosticEntry.Position[1] + 1 $Level = $DiagnosticEntry.Level.ToString() $Code = $DiagnosticEntry.Code $Message = $DiagnosticEntry.Message $OutputString = "$LocalPath(${Line},$Character) : $Level ${Code}: $Message" switch ($Level) { 'Info' { $Params = @{ MessageData = [System.Management.Automation.HostInformationMessage]@{ Message = $OutputString ForegroundColor = $Host.PrivateData.VerboseForegroundColor BackgroundColor = $Host.PrivateData.VerboseBackgroundColor } Tag = 'Information' } } 'Warning' { $Params = @{ MessageData = [System.Management.Automation.HostInformationMessage]@{ Message = $OutputString ForegroundColor = $Host.PrivateData.WarningForegroundColor BackgroundColor = $Host.PrivateData.WarningBackgroundColor } Tag = 'Warning' } } 'Error' { $Params = @{ MessageData = [System.Management.Automation.HostInformationMessage]@{ Message = $OutputString ForegroundColor = $Host.PrivateData.ErrorForegroundColor BackgroundColor = $Host.PrivateData.ErrorBackgroundColor } Tag = 'Error' } } 'Off' { $Params = @{ MessageData = [System.Management.Automation.HostInformationMessage]@{ Message = $OutputString ForegroundColor = $Host.PrivateData.VerboseForegroundColor BackgroundColor = $Host.PrivateData.VerboseBackgroundColor } Tag = 'Off' } } default { Write-Warning "Unhandled diagnostic level: $_" } } Write-Information @Params } } } #EndRegion './Private/WriteBicepDiagnostic.ps1' 71 #Region './Private/WriteErrorStream.ps1' -1 function WriteErrorStream { [CmdletBinding()] param ( [string]$String ) if ($Host.Name -eq 'ConsoleHost') { [Console]::Error.WriteLine($String) } else { $Host.UI.WriteErrorLine($String) } } #EndRegion './Private/WriteErrorStream.ps1' 14 #Region './Public/Build-Bicep.ps1' -1 function Build-Bicep { [CmdletBinding(DefaultParameterSetName = 'Default', SupportsShouldProcess)] [Alias('Invoke-BicepBuild')] param ( [Parameter(ParameterSetName = 'Default', Position = 1)] [Parameter(ParameterSetName = 'AsString', Position = 1)] [Parameter(ParameterSetName = 'AsHashtable', Position = 1)] [Parameter(ParameterSetName = 'OutputPath', Position = 1)] [string]$Path = $pwd.path, [Parameter(ParameterSetName = 'Default', Position = 2)] [ValidateNotNullOrEmpty()] [string]$OutputDirectory, [Parameter(ParameterSetName = 'OutputPath', Position = 2)] [ValidateNotNullOrEmpty()] [ValidateScript( { (Split-path -path $_ -leaf) -match "\.json$" } , ErrorMessage = 'OutputPath needs to be a .JSON-file, e.g. "C:\Output\template.json"')] [string]$OutputPath, [Parameter(ParameterSetName = 'Default')] [Parameter(ParameterSetName = 'AsString')] [Parameter(ParameterSetName = 'AsHashtable')] [Parameter(ParameterSetName = 'OutputPath')] [string[]]$ExcludeFile, [Parameter(ParameterSetName = 'Default')] [Parameter(ParameterSetName = 'OutputPath')] [switch]$GenerateAllParametersFile, [Parameter(ParameterSetName = 'Default')] [Parameter(ParameterSetName = 'OutputPath')] [switch]$GenerateRequiredParametersFile, [Parameter(ParameterSetName = 'AsString')] [switch]$AsString, [Parameter(ParameterSetName = 'AsHashtable')] [switch]$AsHashtable, [Parameter(ParameterSetName = 'Default')] [Parameter(ParameterSetName = 'AsString')] [Parameter(ParameterSetName = 'AsHashtable')] [Parameter(ParameterSetName = 'OutputPath')] [switch]$NoRestore, [Parameter(ParameterSetName = 'Default')] [Parameter(ParameterSetName = 'OutputPath')] [switch]$Compress ) begin { if ($PSBoundParameters.ContainsKey('OutputDirectory') -and (-not (Test-Path $OutputDirectory))) { $null = New-Item $OutputDirectory -Force -ItemType Directory -WhatIf:$WhatIfPreference } if ($PSBoundParameters.ContainsKey('OutputPath') -and (-not (Test-Path $OutputPath))) { $null = New-Item (Split-Path -Path $OutputPath) -Force -ItemType Directory -WhatIf:$WhatIfPreference } if ($PSBoundParameters.ContainsKey('OutputPath') -and ((Split-path -path $Path -leaf) -notmatch "\.bicep$")) { Write-Error 'If -Path and -OutputPath parameters are used, only one .bicep file can be used as input to -Path. E.g. -Path "C:\Output\template.bicep" -OutputPath "C:\Output\newtemplate.json".' Break } } process { $files = Get-Childitem -Path $Path *.bicep -File if ($files) { foreach ($file in $files) { if ($file.Name -notin $ExcludeFile) { if ($VerbosePreference -eq [System.Management.Automation.ActionPreference]::Continue) { $bicepConfig= Get-BicepConfig -Path $file Write-Verbose -Message "Using Bicep configuration: $($bicepConfig.Path)" } $ARMTemplate = Build-BicepFile -Path $file.FullName -NoRestore:$NoRestore.IsPresent if (-not [string]::IsNullOrWhiteSpace($ARMTemplate)) { $BicepModuleVersion = Get-Module -Name Bicep | Sort-Object -Descending | Select-Object -First 1 $ARMTemplateObject = ConvertFrom-Json -InputObject $ARMTemplate if($ARMTemplateObject.'$schema' -ne 'https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#') { # Only add metadata to the ARM template if it is not a parameter file if (-not [string]::IsNullOrWhiteSpace($BicepModuleVersion.PrivateData.Values.Prerelease)) { $ARMTemplateObject.metadata._generator.name += " (Bicep PowerShell $($BicepModuleVersion.Version)-$($BicepModuleVersion.PrivateData.Values.Prerelease))" } else { $ARMTemplateObject.metadata._generator.name += " (Bicep PowerShell $($BicepModuleVersion.Version))" } } $ARMTemplate = ConvertTo-Json -InputObject $ARMTemplateObject -Depth 100 if ($AsString.IsPresent) { Write-Output $ARMTemplate } elseif ($AsHashtable.IsPresent) { $ARMTemplateObject | ConvertToHashtable -Ordered } else { if ($PSBoundParameters.ContainsKey('OutputPath')) { $OutputFilePath = $OutputPath $ParameterFilePath = Join-Path -Path (Split-Path -Path $OutputPath) -ChildPath ('{0}.parameters.json' -f (Split-Path -Path $OutputPath -Leaf).Split(".")[0]) } elseif ($PSBoundParameters.ContainsKey('OutputDirectory')) { $OutputFilePath = Join-Path -Path $OutputDirectory -ChildPath ('{0}.json' -f $file.BaseName) $ParameterFilePath = Join-Path -Path $OutputDirectory -ChildPath ('{0}.parameters.json' -f $file.BaseName) } else { $OutputFilePath = $file.FullName -replace '\.bicep', '.json' $ParameterFilePath = $file.FullName -replace '\.bicep', '.parameters.json' } if ($Compress.IsPresent) { $compressedARMTemplate= $ARMTemplate | ConvertFrom-Json | ConvertTo-Json -Depth 100 -Compress $null = Out-File -Path $OutputFilePath -InputObject $compressedARMTemplate -Encoding utf8 -WhatIf:$WhatIfPreference } else { $null = Out-File -Path $OutputFilePath -InputObject $ARMTemplate -Encoding utf8 -WhatIf:$WhatIfPreference } if ($GenerateRequiredParametersFile.IsPresent -and $GenerateAllParametersFile.IsPresent) { $parameterType = 'All' Write-Warning "Both -GenerateAllParametersFile and -GenerateRequiredParametersFile is present. A parameter file with all parameters will be generated." } elseif ($GenerateRequiredParametersFile.IsPresent) { $parameterType = 'Required' } elseif ($GenerateAllParametersFile.IsPresent) { $parameterType = 'All' } if ($GenerateAllParametersFile.IsPresent -or $GenerateRequiredParametersFile.IsPresent) { GenerateParameterFile -Content $ARMTemplate -DestinationPath $ParameterFilePath -Parameters $parameterType -WhatIf:$WhatIfPreference } } } } } } else { Write-Host "No bicep files located in path $Path" } } } #EndRegion './Public/Build-Bicep.ps1' 144 #Region './Public/Build-BicepParam.ps1' -1 function Build-BicepParam { [CmdletBinding(DefaultParameterSetName = 'Default', SupportsShouldProcess)] param ( [Parameter(ParameterSetName = 'Default', Position = 1)] [Parameter(ParameterSetName = 'AsString', Position = 1)] [Parameter(ParameterSetName = 'AsHashtable', Position = 1)] [Parameter(ParameterSetName = 'OutputPath', Position = 1)] [string]$Path = $pwd.path, [Parameter(ParameterSetName = 'Default', Position = 2)] [ValidateNotNullOrEmpty()] [string]$OutputDirectory, [Parameter(ParameterSetName = 'OutputPath', Position = 2)] [ValidateNotNullOrEmpty()] [ValidateScript( { (Split-path -path $_ -leaf) -match "\.json$" } , ErrorMessage = 'OutputPath needs to be a .JSON-file, e.g. "C:\Output\template.json"')] [string]$OutputPath, [Parameter(ParameterSetName = 'Default')] [Parameter(ParameterSetName = 'AsString')] [Parameter(ParameterSetName = 'AsHashtable')] [Parameter(ParameterSetName = 'OutputPath')] [string[]]$ExcludeFile, [Parameter(ParameterSetName = 'AsString')] [switch]$AsString, [Parameter(ParameterSetName = 'AsHashtable')] [switch]$AsHashtable, [Parameter(ParameterSetName = 'Default')] [Parameter(ParameterSetName = 'OutputPath')] [switch]$Compress ) begin { if ($PSBoundParameters.ContainsKey('OutputDirectory') -and (-not (Test-Path $OutputDirectory))) { $null = New-Item $OutputDirectory -Force -ItemType Directory -WhatIf:$WhatIfPreference } if ($PSBoundParameters.ContainsKey('OutputPath') -and (-not (Test-Path $OutputPath)) -and -not [string]::IsNullOrEmpty((Split-Path -Path $OutputPath))) { $null = New-Item (Split-Path -Path $OutputPath) -Force -ItemType Directory -WhatIf:$WhatIfPreference } if ($PSBoundParameters.ContainsKey('OutputPath') -and ((Split-path -path $Path -leaf) -notmatch "\.bicepparam$")) { Write-Error 'If -Path and -OutputPath parameters are used, only one .bicepparam file can be used as input to -Path. E.g. -Path "C:\Output\template.bicepparm" -OutputPath "C:\Output\template.parameters.json".' Break } } process { $files = Get-Childitem -Path $Path *.bicep -File if ($files) { foreach ($file in $files) { if ($file.Name -notin $ExcludeFile) { if ($VerbosePreference -eq [System.Management.Automation.ActionPreference]::Continue) { $bicepConfig= Get-BicepConfig -Path $file Write-Verbose -Message "Using Bicep configuration: $($bicepConfig.Path)" } $ARMTemplate = Build-BicepParamFile -Path $file.FullName if (-not [string]::IsNullOrWhiteSpace($ARMTemplate)) { if ($AsString.IsPresent) { Write-Output $ARMTemplate } elseif ($AsHashtable.IsPresent) { $ARMTemplateObject | ConvertToHashtable -Ordered } else { if ($PSBoundParameters.ContainsKey('OutputPath')) { $OutputFilePath = $OutputPath } elseif ($PSBoundParameters.ContainsKey('OutputDirectory')) { $OutputFilePath = Join-Path -Path $OutputDirectory -ChildPath ('{0}.parameters.json' -f $file.BaseName) } else { $OutputFilePath = $file.FullName -replace '\.bicepparam', '.parameters.json' } if ($Compress.IsPresent) { $ARMTemplate = $ARMTemplate | ConvertFrom-Json | ConvertTo-Json -Depth 100 -Compress } $null = Out-File -Path $OutputFilePath -InputObject $ARMTemplate -Encoding utf8 -WhatIf:$WhatIfPreference } } } } } else { Write-Host "No bicep files located in path $Path" } } } #EndRegion './Public/Build-BicepParam.ps1' 97 #Region './Public/Clear-BicepModuleCache.ps1' -1 function Clear-BicepModuleCache { [CmdletBinding(DefaultParameterSetName = 'Oci')] param ( [Parameter(ParameterSetName = 'Oci', Position = 1)] [switch]$Oci, [Parameter(ParameterSetName = 'Oci', Position = 2)] [ValidateNotNullOrEmpty()] [string]$Registry, [Parameter(ParameterSetName = 'Oci', Position = 3)] [ValidateNotNullOrEmpty()] [string]$Repository, [Parameter(ParameterSetName = 'TemplateSpecs', Position = 1)] [switch]$TemplateSpecs, [Parameter(ParameterSetName = 'TemplateSpecs', Position = 2)] [ValidateNotNullOrEmpty()] [string]$SubscriptionId, [Parameter(ParameterSetName = 'TemplateSpecs', Position = 3)] [ValidateNotNullOrEmpty()] [string]$ResourceGroup, [Parameter(ParameterSetName = 'TemplateSpecs', Position = 4)] [ValidateNotNullOrEmpty()] [string]$Spec, [Parameter(ParameterSetName = 'Oci', Position = 4)] [Parameter(ParameterSetName = 'TemplateSpecs', Position = 5)] [ValidateNotNullOrEmpty()] [string]$Path, [Parameter(ParameterSetName = 'Oci', Position = 5)] [Parameter(ParameterSetName = 'TemplateSpecs', Position = 6)] [ValidateNotNullOrEmpty()] [string]$Version, [Parameter(ParameterSetName = 'All', Position = 1)] [switch]$All ) process { $PathSplat = @{} if([string]::IsNullOrEmpty($Path) -eq $false) {$PathSplat.Add('Path', (Resolve-Path -Path $Path).Path)} if ($Oci -or $All) { $OciPath = Get-BicepCachePath -Oci @PathSplat Write-Verbose "Using cache path [$OciPath]" $RepositoryPath = $Repository -replace '\\', '$' if (($Registry) -and ($Repository) -and ($Version)) { Remove-Item -Recurse -Path "$OciPath/$Registry/$RepositoryPath/$Version`$" -Force Write-Verbose "Cleared version [$Version] of [$Repository] in [$Registry] from local module cache" } elseif (($Registry) -and ($Repository)) { Remove-Item -Recurse -Path "$OciPath/$Registry/$RepositoryPath" -Force Write-Verbose "Cleared [$Repository] in [$Registry] from local module cache" } elseif ($Registry) { Remove-Item -Recurse -Path "$OciPath/$Registry" -Force Write-Verbose "Cleared [$Registry] from local module cache" } else { if (Test-Path -Path $OciPath) { Remove-Item -Recurse -Path $OciPath -Force Write-Verbose "Cleared Oci local module cache" } else { Write-Verbose "No Oci local module cache found" } } } if ($TemplateSpecs -or $All) { $TSPath = Get-BicepCachePath -TemplateSpecs @PathSplat Write-Verbose "Using cache path [$TSPath]" if (($SubscriptionId) -and ($ResourceGroup) -and ($Spec) -and ($Version)) { Remove-Item -Recurse -Path "$TSPath/$SubscriptionId/$ResourceGroup/$Spec/$Version" -Force Write-Verbose "Cleared version [$Version] of [$Spec] in [$ResourceGroup] in [$SubscriptionId] from local module cache" } elseif (($SubscriptionId) -and ($ResourceGroup) -and ($Spec)) { Remove-Item -Recurse -Path "$TSPath/$SubscriptionId/$ResourceGroup/$Spec" -Force Write-Verbose "Cleared [$Spec] in [$ResourceGroup] in [$SubscriptionId] from local module cache" } elseif (($SubscriptionId) -and ($ResourceGroup)) { Remove-Item -Recurse -Path "$TSPath/$SubscriptionId/$ResourceGroup" -Force Write-Verbose "Cleared [$ResourceGroup] in [$SubscriptionId] from local module cache" } elseif ($SubscriptionId) { Remove-Item -Recurse -Path "$TSPath/$SubscriptionId" -Force Write-Verbose "Cleared [$SubscriptionId] from local module cache" } else { if (Test-Path -Path $TSPath) { Remove-Item -Recurse -Path $TSPath -Force Write-Verbose "Cleared Template Spec local module cache" } else { Write-Verbose "No Template Spec local module cache found" } } } } } #EndRegion './Public/Clear-BicepModuleCache.ps1' 108 #Region './Public/Connect-Bicep.ps1' -1 function Connect-Bicep { [Diagnostics.CodeAnalysis.SuppressMessageAttribute( "PSAvoidDefaultValueForMandatoryParameter", "ClientId", Justification = "Client Id is only mandatory for certain auth flows." )] [CmdletBinding(DefaultParameterSetName = 'Interactive')] param ( [Parameter(ParameterSetName = 'ManagedIdentity')] [Parameter(ParameterSetName = 'Interactive')] [Parameter(Mandatory, ParameterSetName = 'Certificate')] [ValidateNotNullOrEmpty()] [string]$Tenant, [Parameter(ParameterSetName = 'ManagedIdentity')] [Parameter(ParameterSetName = 'Interactive')] [Parameter(Mandatory, ParameterSetName = 'Certificate')] [ValidateNotNullOrEmpty()] [string]$ClientId = '1950a258-227b-4e31-a9cf-717495945fc2', [Parameter(Mandatory, ParameterSetName = 'Certificate')] [string]$CertificatePath, [Parameter(Mandatory, ParameterSetName = 'ManagedIdentity')] [switch]$ManagedIdentity ) # Set up module-scoped variables for getting tokens $script:TokenSplat = @{} $script:CertificatePath = $null $script:TokenSplat['ClientId'] = $ClientId if ($PSBoundParameters.ContainsKey('Tenant')) { $script:TokenSplat['TenantId'] = $Tenant } if ($PSBoundParameters.ContainsKey('CertificatePath')) { $script:CertificatePath = $CertificatePath $Certificate = Get-Item $CertificatePath if ($Certificate -is [System.Security.Cryptography.X509Certificates.X509Certificate2]) { $script:TokenSplat['ClientCertificate'] = Get-Item $CertificatePath } else { $script:TokenSplat['ClientCertificatePath'] = $CertificatePath } } if ($PSCmdlet.ParameterSetName -eq 'Interactive') { $script:TokenSplat['Interactive'] = $true } if ($ManagedIdentity.IsPresent) { $script:TokenSplat['ManagedIdentity'] = $true } $script:Token = Get-AzToken @script:TokenSplat # Save the source of the token to module scope for AssertAzureConnection to know how to refresh it $script:TokenSource = 'PSBicep' } #EndRegion './Public/Connect-Bicep.ps1' 52 #Region './Public/Convert-JsonToBicep.ps1' -1 function Convert-JsonToBicep { [CmdletBinding()] param( [Parameter(Mandatory, ValueFromPipeline = $true, ParameterSetName = 'String')] [ValidateNotNullOrEmpty()] [ValidateScript( { try { $_ | Convertfrom-Json } catch { $false } }, ErrorMessage = 'The string is not a valid json')] [string]$String, [Parameter(ParameterSetName = 'Path')] [ValidateNotNullOrEmpty()] [ValidateScript( { try { Get-Content -Path $_ | Convertfrom-Json } catch { $false } }, ErrorMessage = 'The file does not contain a valid json')] [string]$Path, [switch]$ToClipboard ) begin { $tempPath = [System.Io.Path]::GetTempPath() } process { if($String) { $inputObject = $String | ConvertFrom-Json } else { $inputObject = Get-Content -Path $Path | ConvertFrom-Json } if ((-not $IsWindows) -and $ToClipboard.IsPresent) { Write-Error -Message "The -ToClipboard switch is only supported on Windows systems." break } $hashTable = ConvertToHashtable -InputObject $inputObject -Ordered $variables = [ordered]@{} $templateBase = [ordered]@{ '$schema' = 'https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#' 'contentVersion' = '1.0.0.0' } $variables['temp'] = $hashTable.SyncRoot $templateBase['variables'] = $variables $tempTemplate = ConvertTo-Json -InputObject $templateBase -Depth 100 Out-File -InputObject $tempTemplate -FilePath "$tempPath\tempfile.json" $file = Get-ChildItem -Path "$tempPath\tempfile.json" if ($file) { $BicepObject = ConvertTo-BicepFile -Path $file.FullName foreach ($BicepFile in $BicepObject.Keys) { $bicepData = $BicepObject[$BicepFile] } $bicepOutput = $bicepData.Replace("var temp = ", "") if ($ToClipboard.IsPresent) { Set-Clipboard -Value $bicepOutput Write-Host "Bicep object saved to clipboard" } else { Write-Output $bicepOutput } } Remove-Item $file } } #EndRegion './Public/Convert-JsonToBicep.ps1' 72 #Region './Public/ConvertTo-Bicep.ps1' -1 function ConvertTo-Bicep { [CmdletBinding(DefaultParameterSetName = 'Decompile')] param( [Parameter(ParameterSetName = 'Decompile')] [string]$Path = $pwd.path, [Parameter(ParameterSetName = 'Decompile')] [string]$OutputDirectory, [Parameter(ParameterSetName = 'Decompile')] [switch]$AsString, [Parameter(ParameterSetName = 'Decompile')] [switch]$Force, [Parameter(Mandatory, ParameterSetName = 'ConvertFromBody')] [string]$ResourceId, [Parameter(Mandatory, ParameterSetName = 'ConvertFromBody')] [string]$ResourceBody, [Parameter(ParameterSetName = 'ConvertFromBodyHash')] [hashtable]$ResourceDictionary, [Parameter(ParameterSetName = 'ConvertFromBody')] [Parameter(ParameterSetName = 'ConvertFromBodyHash')] [switch]$RemoveUnknownProperties ) begin { if ($PSCmdlet.ParameterSetName -eq 'Decompile') { Write-Warning -Message 'Decompilation is a best-effort process, as there is no guaranteed mapping from ARM JSON to Bicep. You may need to fix warnings and errors in the generated bicep file(s), or decompilation may fail entirely if an accurate conversion is not possible. If you would like to report any issues or inaccurate conversions, please see https://github.com/Azure/bicep/issues.' if ($PSBoundParameters.ContainsKey('OutputDirectory') -and (-not (Test-Path $OutputDirectory))) { $null = New-Item $OutputDirectory -Force -ItemType Directory } } } process { if ($PSCmdlet.ParameterSetName -eq 'Decompile') { $files = Get-Childitem -Path $Path -Filter '*.json' -File | Select-String -Pattern "schema.management.azure.com/schemas/.*deploymentTemplate.json#" | Select-Object -ExpandProperty 'Path' if ($files) { foreach ($File in $files) { $BicepObject = ConvertTo-BicepFile -Path $File foreach ($BicepFile in $BicepObject.Keys) { if ($AsString.IsPresent) { Write-Output $BicepObject[$BicepFile] $TempFolder = New-Item -Path ([system.io.path]::GetTempPath()) -Name (New-Guid).Guid -ItemType 'Directory' $OutputDirectory = $TempFolder.FullName } if (-not [string]::IsNullOrEmpty($OutputDirectory)) { $FileName = Split-Path -Path $BicepFile -Leaf $FilePath = Join-Path -Path $OutputDirectory -ChildPath $FileName } else { $FilePath = $BicepFile } if ((Test-Path $FilePath) -and (-not $Force)) { Write-Error -Message "$FilePath Already exists. Use -Force to overwrite." -Category ResourceExists -TargetObject $FilePath $VerifyBicepBuild = $false } else { $null = Out-File -InputObject $BicepObject[$BicepFile] -FilePath $FilePath -Encoding utf8 $VerifyBicepBuild = $true } } if ($VerifyBicepBuild) { $null = Build-Bicep -Path $FilePath -AsString } if ($null -ne $TempFolder) { Remove-Item -Path $TempFolder -Recurse -Force } } } else { Write-Host "No ARM template files located in path $Path" } } elseif ($PSCmdlet.ParameterSetName -eq 'ConvertFromBody') { Convert-ARMResourceToBicep -ResourceId $ResourceId -ResourceBody $ResourceBody -RemoveUnknownProperties:$RemoveUnknownProperties.IsPresent } elseif ($PSCmdlet.ParameterSetName -eq 'ConvertFromBodyHash') { Convert-ArmResourceToBicep -ResourceDictionary $ResourceDictionary -RemoveUnknownProperties:$RemoveUnknownProperties.IsPresent } else { throw [System.Exception.NotimplementedException]::new() } } } #EndRegion './Public/ConvertTo-Bicep.ps1' 97 #Region './Public/Export-BicepResource.ps1' -1 function Export-BicepResource { [CmdletBinding()] param ( [Parameter(Mandatory, ParameterSetName = 'ByQueryOutPath')] [Parameter(Mandatory, ParameterSetName = 'ByQueryOutStream')] [string]$KQLQuery, [Parameter(ParameterSetName = 'ByQueryOutPath')] [Parameter(ParameterSetName = 'ByQueryOutStream')] [switch]$UseKQLResult, [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'ByIdOutPath')] [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, ParameterSetName = 'ByIdOutStream')] [Alias('id')] [string[]]$ResourceId, [Parameter(Mandatory, ParameterSetName = 'ByQueryOutPath')] [Parameter(Mandatory, ParameterSetName = 'ByIdOutPath')] [string]$OutputDirectory, [Parameter(ParameterSetName = 'ByQueryOutPath')] [Parameter(ParameterSetName = 'ByQueryOutStream')] [Parameter(ParameterSetName = 'ByIdOutPath')] [Parameter(ParameterSetName = 'ByIdOutStream')] [switch]$IncludeTargetScope, [Parameter(ParameterSetName = 'ByQueryOutPath')] [Parameter(ParameterSetName = 'ByQueryOutStream')] [Parameter(ParameterSetName = 'ByIdOutPath')] [Parameter(ParameterSetName = 'ByIdOutStream')] [switch]$RemoveUnknownProperties, [Parameter(ParameterSetName = 'ByQueryOutStream')] [Parameter(ParameterSetName = 'ByIdOutStream')] [switch]$AsString, [Parameter(ParameterSetName = 'ByQueryOutPath')] [Parameter(ParameterSetName = 'ByQueryOutStream')] [Parameter(ParameterSetName = 'ByIdOutPath')] [Parameter(ParameterSetName = 'ByIdOutStream')] [switch]$Raw ) begin { # Get bicepconfig based on current location $ConfigPath = Get-Location $Config = Get-BicepConfig -Path $ConfigPath -Merged AssertAzureConnection -TokenSplat $script:TokenSplat -BicepConfig $Config if ($PSCmdlet.ParameterSetName -like 'ByQuery*') { $Resources = SearchAzureResourceGraph -Query $KQLQuery if ($null -eq $Resources.id) { throw 'KQL query must return a column named "id"' } $ResourceId = @($Resources.id) } $hash = [hashtable]::Synchronized(@{}) $hash['config'] = @{} $hash['config']['backoff'] = $false $hash['config']['token'] = $script:Token $hash['config']['tokensplat'] = $script:TokenSplat $AssertFunction = Get-Item "function:\AssertAzureConnection" } process { if ($UseKQLResult.IsPresent) { Write-Warning 'Using KQL result from Azure Resource Graph is experimental and may generate invalid or incomplete bicep files' foreach ($resource in $Resources) { $hash[$resource.id] = $resource | ConvertTo-Json -Depth 100 } } else { $ResourceId | Foreach-Object { try{ $resolvedType = Resolve-BicepResourceType -ResourceId $_ -ErrorAction 'Stop' } catch { Write-Warning "Failed to resolve resource type for $_, skipping." return } [pscustomobject]@{ ApiVersions = Get-BicepApiVersion -ResourceTypeReference $resolvedType ResourceId = $_ TypeIndex = 0 } } | Foreach-Object -ThrottleLimit 50 -Parallel { $ResourceId = $_.ResourceId $ApiVersionList = $_.ApiVersions $TypeIndex = $_.TypeIndex $hashtable = $using:hash $maxRetries = 50 $retryCount = 0 $backoffInterval = 2 $TokenThreshold = (Get-Date).AddMinutes(-5) while ($retryCount -lt $maxRetries) { $ApiVersion = $ApiVersionList[$TypeIndex] while ($hashtable['config']['backoff']) { Write-Warning "$ResourceId is backing off" Start-Sleep -Seconds 10 } $hashtable['config']['token'] = $using:Token if (-not $hashtable['config']['backoff'] -and $hashtable['config']['token'].ExpiresOn -lt $TokenThreshold) { $hashtable['config']['backoff'] = $true & $using:AssertFunction -TokenSplat $hash['config']['TokenSplat'] $hashtable['config']['token'] = $script:Token $hashtable['config']['backoff'] = $false } try { $uri = "https://management.azure.com/${ResourceId}?api-version=$ApiVersion" $Headers = @{ authorization = "Bearer $($hashtable['config']['token'].Token)" contentType = 'application/json' } $Response = Invoke-WebRequest -Uri $uri -Method 'Get' -Headers $Headers -UseBasicParsing if ($Response.StatusCode -eq 200) { $hashtable[$ResourceId] = $Response.Content } else { Write-Warning "Failed to get $_ with status code $($Response.StatusCode)" } return } catch { $CurrentError = $_ # Retrylogic for 400 errors if ($CurrentError.Exception.Response.StatusCode -eq 400) { # If error is due to no registered provider, try next api version if($CurrentError.ErrorDetails.Message -like '*"code": "NoRegisteredProviderFound"*') { $TypeIndex++ $retryCount++ if ($TypeIndex -ge $ApiVersionList.Count) { Write-Warning "No more api versions to try for $ResourceId" throw $CurrentError } continue } } if ($CurrentError.Exception.Response.StatusCode -eq 429) { if (-not $hashtable['config']['backoff']) { $hashtable['config']['backoff'] = $true Start-Sleep -Seconds ($backoffInterval * [math]::Pow(2, $retryCount)) $retryCount++ $hashtable['config']['backoff'] = $false } } Write-Warning ("Failed to get resource! {0}" -f $CurrentError.Exception.Message) throw $CurrentError } } throw "Max retries reached for $_" } } } end { # Ensure that we use any new tokens in module $script:Token = $hash['config']['token'] $hash.Remove('config') if (-not $Raw.IsPresent) { $hash = ConvertTo-Bicep -ResourceDictionary $hash -RemoveUnknownProperties:$RemoveUnknownProperties.IsPresent -ErrorAction 'Stop' } $hash.GetEnumerator() | ForEach-Object { $Id = $_.Key $Template = $_.Value if ($PSCmdlet.ParameterSetName -like '*OutPath') { if (-not (Test-Path -Path $OutputDirectory -PathType 'Container')) { $null = New-Item -Path $OutputDirectory -ItemType 'Directory' } $FileName = $Id -replace '/', '_' $OutputFilePath = Join-Path -Path $OutputDirectory -ChildPath "$FileName.bicep" $null = Out-File -InputObject $Template -FilePath $OutputFilePath -Encoding utf8 } elseif ($PSCmdlet.ParameterSetName -like '*OutStream') { if ($AsString.IsPresent) { $Template } else { [pscustomobject]@{ ResourceId = $Id Template = $Template } } } else { throw [System.NotImplementedException]::new() } } } } #EndRegion './Public/Export-BicepResource.ps1' 203 #Region './Public/Find-BicepModule.ps1' -1 function Find-BicepModule { [CmdletBinding()] param ( [Parameter()] [ValidateNotNullOrEmpty()] [ValidateScript( { Test-Path $_ })] [string]$Path, [Parameter()] [ValidateNotNullOrEmpty()] [string]$Registry, [Parameter()] [ValidateNotNullOrEmpty()] [switch]$Cache ) process { # Find modules used in local bicep file if ($Path) { $BicepFile = Get-Childitem -Path $Path -File try { $validBicep = Test-BicepFile -Path $BicepFile.FullName -IgnoreDiagnosticOutput -AcceptDiagnosticLevel Warning if (-not ($validBicep)) { throw "The provided bicep is not valid. Make sure that your bicep file builds successfully before publishing." } else { Write-Verbose "[$($BicepFile.Name)] is valid" Find-BicepModule -Path $Path Write-Verbose -Message "Finding modules used in [$($BicepFile.Name)]" } } catch { Throw $_ } } # Find modules in ACR if ($Registry) { try { Find-BicepModule -Registry $Registry Write-Verbose -Message "Finding all modules stored in: [$Registry]" } catch { Throw $_ } } # Find modules in the local cache if ($Cache) { # Find module try { Find-BicepModule -Cache Write-Verbose -Message "Finding modules in the local module cache" } catch { } } } } #EndRegion './Public/Find-BicepModule.ps1' 64 #Region './Public/Format-BicepFile.ps1' -1 function Format-BicepFile { [CmdletBinding(DefaultParameterSetName = 'Default', SupportsShouldProcess)] param ( [Parameter(ParameterSetName = 'Default', Position = 1)] [Parameter(ParameterSetName = 'AsString', Position = 1)] [Parameter(ParameterSetName = 'OutputDirectory', Position = 1)] [Parameter(ParameterSetName = 'OutputPath', Position = 1)] [string]$Path = $pwd.path, [Parameter(ParameterSetName = 'OutputDirectory', Position = 2)] [ValidateNotNullOrEmpty()] [string]$OutputDirectory, [Parameter(ParameterSetName = 'OutputPath', Position = 2)] [ValidateNotNullOrEmpty()] [ValidateScript( { (Split-Path -Path $_ -Leaf) -match '\.bicep$' } , ErrorMessage = 'OutputPath needs to be a .bicep-file, e.g. "C:\Output\template.bicep"')] [string]$OutputPath, [Parameter(ParameterSetName = 'Default', Position = 3)] [Parameter(ParameterSetName = 'AsString', Position = 3)] [Parameter(ParameterSetName = 'OutputDirectory', Position = 3)] [Parameter(ParameterSetName = 'OutputPath', Position = 3)] [ValidateNotNullOrEmpty()] [ValidateSet('Auto','LF','CRLF')] [string]$NewlineOption = 'Auto', [Parameter(ParameterSetName = 'Default', Position = 4)] [Parameter(ParameterSetName = 'AsString', Position = 4)] [Parameter(ParameterSetName = 'OutputDirectory', Position = 4)] [Parameter(ParameterSetName = 'OutputPath', Position = 4)] [ValidateNotNullOrEmpty()] [ValidateSet('Space','Tab')] [string]$IndentKindOption = 'Space', [Parameter(ParameterSetName = 'Default', Position = 5)] [Parameter(ParameterSetName = 'AsString', Position = 5)] [Parameter(ParameterSetName = 'OutputDirectory', Position = 5)] [Parameter(ParameterSetName = 'OutputPath', Position = 5)] [ValidateNotNullOrEmpty()] [int]$IndentSize = 2, [Parameter(ParameterSetName = 'Default')] [Parameter(ParameterSetName = 'AsString')] [Parameter(ParameterSetName = 'OutputDirectory')] [Parameter(ParameterSetName = 'OutputPath')] [switch]$InsertFinalNewline, [Parameter(ParameterSetName = 'AsString')] [switch]$AsString ) begin { if ($PSBoundParameters.ContainsKey('OutputDirectory') -and (-not (Test-Path $OutputDirectory))) { $null = New-Item $OutputDirectory -Force -ItemType Directory -WhatIf:$WhatIfPreference } if ($PSBoundParameters.ContainsKey('OutputPath') -and (-not (Test-Path $OutputPath)) -and -not [string]::IsNullOrEmpty((Split-Path -Path $OutputPath))) { $null = New-Item (Split-Path -Path $OutputPath) -Force -ItemType Directory -WhatIf:$WhatIfPreference } if ($PSBoundParameters.ContainsKey('OutputPath') -and ((Split-Path -Path $Path -Leaf) -notmatch '\.bicep$')) { Write-Error 'If -Path and -OutputPath parameters are used, only one .bicep file can be used as input to -Path. E.g. -Path "C:\Output\template.bicep" -OutputPath "C:\Output\formatted.bicep".' Break } if ($VerbosePreference -eq [System.Management.Automation.ActionPreference]::Continue) { $FullVersion = Get-BicepVersion -Verbose:$false Write-Verbose -Message "Using Bicep version: $FullVersion" } } process { $files = Get-ChildItem -Path $Path *.bicep -File if ($files) { foreach ($file in $files) { if ($VerbosePreference -eq [System.Management.Automation.ActionPreference]::Continue) { $bicepConfig = Get-BicepConfig -Path $file Write-Verbose -Message "Using Bicep configuration: $($bicepConfig.Path)" } # Set up splatting with common parameters for Bicep $Params = @{ 'NewlineOption' = $NewlineOption 'IndentKindOption' = $IndentKindOption 'IndentSize' = $IndentSize 'InsertFinalNewline' = $InsertFinalNewline.IsPresent 'Content' = Get-Content -Path $file -Raw } $FormattedBicep = Format-Bicep @Params if (-not [string]::IsNullOrWhiteSpace($FormattedBicep)) { if ($AsString.IsPresent) { Write-Output $FormattedBicep } else { if ($PSBoundParameters.ContainsKey('OutputPath')) { $OutputFilePath = $OutputPath } elseif ($PSBoundParameters.ContainsKey('OutputDirectory')) { $OutputFilePath = Join-Path -Path $OutputDirectory -ChildPath ('{0}.bicep' -f $file.BaseName) } else { $OutputFilePath = $file.FullName } $null = Out-File -Path $OutputFilePath -InputObject $FormattedBicep -Encoding utf8 -WhatIf:$WhatIfPreference } } } } else { Write-Host "No bicep files located in path $Path" } } } #EndRegion './Public/Format-BicepFile.ps1' 116 #Region './Public/Get-BicepApiReference.ps1' -1 function Get-BicepApiReference { [CmdletBinding(DefaultParameterSetName = 'TypeString')] param( [Parameter(Mandatory, ParameterSetName = 'ResourceProvider')] [ValidateNotNullOrEmpty()] [ValidateScript( { (GetBicepTypes).ResourceProvider -contains $_ }, ErrorMessage = "ResourceProvider '{0}' was not found.")] [ArgumentCompleter([BicepResourceProviderCompleter])] [string]$ResourceProvider, [Parameter(Mandatory, ParameterSetName = 'ResourceProvider')] [ValidateNotNullOrEmpty()] [ValidateScript( { (GetBicepTypes).Resource -contains $_ }, ErrorMessage = "Resource '{0}' was not found.")] [ArgumentCompleter([BicepResourceCompleter])] [string]$Resource, [Parameter(ParameterSetName = 'ResourceProvider')] [ValidateNotNullOrEmpty()] [ValidateScript( { (GetBicepTypes).Child -contains $_ }, ErrorMessage = "Child '{0}' was not found.")] [ArgumentCompleter([BicepResourceChildCompleter])] [string]$Child, [Parameter(ParameterSetName = 'ResourceProvider')] [ValidateNotNullOrEmpty()] [ValidateScript( { (GetBicepTypes).ApiVersion -contains $_ }, ErrorMessage = "ApiVersion '{0}' was not found.")] [ArgumentCompleter([BicepResourceApiVersionCompleter])] [string]$ApiVersion, [Parameter(ParameterSetName = 'TypeString', Position = 0)] [ValidateScript( { $_ -like '*/*' -and $_ -like '*@*' }, ErrorMessage = "Type must contain '/' and '@'.")] [ArgumentCompleter([BicepTypeCompleter])] [string]$Type, [Parameter(ParameterSetName = 'TypeString')] [switch]$Latest, [Parameter(ParameterSetName = 'ResourceProvider')] [Parameter(ParameterSetName = 'TypeString')] [Alias('Please')] [switch]$Force, [Parameter(ParameterSetName = 'ResourceProvider')] [Parameter(ParameterSetName = 'TypeString')] [switch]$ReturnUri ) process { $baseUrl = "https://docs.microsoft.com/en-us/azure/templates" $suffix = '?tabs=bicep' switch ($PSCmdlet.ParameterSetName) { 'ResourceProvider' { $url = "$BaseUrl/$ResourceProvider" # if ApiVersion is provided, we use that. Otherwise we skip version and go for latest if ($PSBoundParameters.ContainsKey('ApiVersion')) { $url += "/$ApiVersion" } $url += "/$Resource" # Child is optional, so we only add it if provided if ($PSBoundParameters.ContainsKey('Child')) { $url += "/$Child" } $url += $suffix } 'TypeString' { if ($PSBoundParameters.ContainsKey('Type')) { # Type looks like this: Microsoft.Aad/domainServicess@2017-01-01 # Then we split here: ^ ^ # Or it looks like this: Microsoft.ApiManagement/service/certificates@2019-12-01 # Then we split here: ^ ^ ^ # Lets not use regex. regex kills kittens # First check if we have three parts before the @ # In that case the last one should be the child if (($type -split '/' ).count -eq 3) { $TypeChild = ( ($type -split '@') -split '/' )[2] } else { $TypeChild = $null } $TypeResourceProvider = ( ($type -split '@') -split '/' )[0] $TypeResource = ( ($type -split '@') -split '/' )[1] $TypeApiVersion = ( $type -split '@' )[1] if ([string]::IsNullOrEmpty($TypeChild) -and ($Latest.IsPresent)) { $url = "$BaseUrl/$TypeResourceProvider/$TypeResource" } elseif ([string]::IsNullOrEmpty($TypeChild)) { $url = "$BaseUrl/$TypeResourceProvider/$TypeApiVersion/$TypeResource" } elseif ($Latest.IsPresent) { $url = "$BaseUrl/$TypeResourceProvider/$TypeResource/$TypeChild" } else { $url = "$BaseUrl/$TypeResourceProvider/$TypeApiVersion/$TypeResource/$TypeChild" } $url += $suffix } else { # If Type is not provided, open the template start page $url = $BaseUrl } } } # Check if there is any valid page on the generated Url # We don't want to send users to broken urls. try { $null = Invoke-WebRequest -Uri $url -ErrorAction Stop $DocsFound = $true } catch { $DocsFound = $false } # Now we know if its working or not. Open page or provide error message. if ($DocsFound -or $Force.IsPresent) { if ($ReturnUri) { Write-Output $url } else { Start-Process $url } } else { Write-Error "No documentation found. This usually means that no documentation has been written. Use the -Latest parameter to open the latest available API Version. Or if you would like to try anyway, use the -Force parameter. Url: $url" } } } #EndRegion './Public/Get-BicepApiReference.ps1' 144 #Region './Public/Get-BicepMetadata.ps1' -1 function Get-BicepMetadata { [CmdletBinding()] param ( # Specifies a path to one or more locations. [Parameter( Mandatory = $true )] [Alias("PSPath")] [ValidateNotNullOrEmpty()] [string] $Path, # Set output type [Parameter()] [ValidateSet('PSObject', 'Json', 'Hashtable')] [String] $OutputType = 'PSObject', [switch]$IncludeReservedMetadata ) process { $file = Get-Item -Path $Path try { $ARMTemplate = Build-BicepFile -Path $file.FullName $ARMTemplateObject = ConvertFrom-Json -InputObject $ARMTemplate $templateMetadata=$ARMTemplateObject.metadata if (!$IncludeReservedMetadata.IsPresent) { $templateMetadata=Select-Object -InputObject $templateMetadata -ExcludeProperty '_*' } } catch { # We don't care about errors here. } switch ($OutputType) { 'PSObject' { $templateMetadata } 'Json' { $templateMetadata | ConvertTo-Json } 'Hashtable' { $templateMetadata | ConvertToHashtable -Ordered } } } } #EndRegion './Public/Get-BicepMetadata.ps1' 50 #Region './Public/Get-BicepUsedModules.ps1' -1 Function Get-BicepUsedModules { param( [cmdletbinding()] [Parameter(Mandatory = $true)] [string[]]$Path ) try { $BicepFile = Get-Content -Path $Path -Raw | Out-String } catch { throw "Could not read file $Path" } $UsedModules = @() [regex]::matches($BicepFile, "^\s*module\s+(\w+)\s+'([^']+)'", "Multiline") | ForEach-Object { $_ | ForEach-Object { $ModuleName = $_.Groups[1].value $ModulePath = $_.Groups[2].Value $UsedModules += [PSCustomObject]@{ Name = $ModuleName Path = $ModulePath } } } if ($UsedModules.Count -eq 0) { Write-Verbose -Verbose "No modules were found in the Bicep file." } # Make sure it returns an array of objects return ,$UsedModules } #EndRegion './Public/Get-BicepUsedModules.ps1' 38 #Region './Public/Get-BicepVersion.ps1' -1 function Get-BicepVersion { param ( [switch]$All ) if ($All.IsPresent) { ListBicepVersions } else { $installedVersion = InstalledBicepVersion $latestVersion = ListBicepVersions -Latest [pscustomobject]@{ InstalledVersion = $installedVersion LatestVersion = $latestVersion } } } #EndRegion './Public/Get-BicepVersion.ps1' 20 #Region './Public/Install-BicepCLI.ps1' -1 function Install-BicepCLI { [CmdLetBinding()] param( [ValidateScript( { (ListBicepVersions).Contains($_) }, ErrorMessage = "Bicep Version '{0}' was not found.")] [ArgumentCompleter([BicepVersionCompleter])] [string]$Version, [switch]$Force ) $BicepInstalled = TestBicep if (-not $IsWindows) { Write-Error -Message "This cmdlet is only supported for Windows systems. ` To install Bicep on your system see instructions on https://github.com/Azure/bicep" Write-Host "`nList the available module cmdlets by running 'Get-Help -Name Bicep'`n" break } $BaseURL = 'https://api.github.com/repos/Azure/bicep/releases' if ($PSBoundParameters.ContainsKey('Version')) { $BicepRelease = ('{0}/tags/v{1}' -f $BaseURL, $Version) } else { $BicepRelease = ('{0}/latest' -f $BaseURL) } if ($Force.IsPresent -and $BicepInstalled -eq $true) { Write-Warning 'You are running multiple installations of Bicep CLI. You can correct this by running Update-BicepCLI.' } if ($Force.IsPresent -or $BicepInstalled -eq $false) { # Fetch the latest Bicep CLI installer $DownloadFileName = 'bicep-setup-win-x64.exe' $TargetFileName = Join-Path -Path $env:TEMP -ChildPath $DownloadFileName # Fetch data about latest Bicep release from Github API $GetBicepRelease = Invoke-RestMethod -Uri $BicepRelease $RequestedGithubAsset = $GetBicepRelease.assets | Where-Object -Property Name -eq $DownloadFileName #Download and show progress. (New-Object Net.WebClient).DownloadFileAsync($RequestedGithubAsset.browser_download_url, $TargetFileName) Write-Verbose "Downloading $($RequestedGithubAsset.browser_download_url) to location $TargetFileName" do { $PercentComplete = [math]::Round((Get-Item $TargetFileName).Length / $RequestedGithubAsset.size * 100) Write-Progress -Activity 'Downloading Bicep' -PercentComplete $PercentComplete start-sleep 1 } while ((Get-Item $TargetFileName).Length -lt $RequestedGithubAsset.size) # Wait for a bit to make sure we don't run into file locks Start-Sleep -Seconds 2 # Run the installer in silent mode Write-Verbose "Running installer $TargetFileName /VERYSILENT" Start-Process $TargetFileName -ArgumentList '/VERYSILENT' -Wait -ErrorAction Stop $i = 0 do { $i++ Write-Progress -Activity 'Installing Bicep CLI' -CurrentOperation "$i - Running $DownloadFileName" Start-Sleep -Seconds 1 } while (Get-Process $DownloadFileName.Replace('.exe', '') -ErrorAction SilentlyContinue) # Since bicep installer updates the $env:PATH we reload it in order to verify installation. $env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User") # Verify you can now access the 'bicep' command. if (TestBicep) { $bicep = InstalledBicepVersion Write-Host "Bicep version $bicep successfully installed!" } else { Write-Error "Error installing Bicep CLI" } # Remove the downloaded installer. Remove-Item $TargetFileName -ErrorAction SilentlyContinue -Force } else { $versionCheck = CompareBicepVersion if ($versionCheck) { Write-Host "The latest Bicep CLI Version is already installed." } else { Write-Host "Bicep CLI is already installed, but there is a newer release available. Use Update-BicepCLI or Install-BicepCLI -Force to updated to the latest release" } } } #EndRegion './Public/Install-BicepCLI.ps1' 87 #Region './Public/New-BicepMarkdownDocumentation.ps1' -1 function New-BicepMarkdownDocumentation { [CmdletBinding(DefaultParameterSetName = 'FromFile')] param ( [Parameter(ParameterSetName = 'FromFile', Position = 0)] [string]$File, [Parameter(ParameterSetName = 'FromFolder', Position = 0)] [string]$Path, [Parameter(ParameterSetName = 'FromFolder')] [switch]$Recurse, [Parameter(ParameterSetName = 'FromFile')] [Parameter(ParameterSetName = 'FromFolder')] [switch]$AsString, [Parameter(ParameterSetName = 'FromFile')] [Parameter(ParameterSetName = 'FromFolder')] [switch]$Force ) switch ($PSCmdLet.ParameterSetName) { 'FromFile' { [System.IO.FileInfo[]]$FileCollection = Get-Item $File } 'FromFolder' { [System.IO.FileInfo[]]$FileCollection = Get-ChildItem $Path *.bicep -Recurse:$Recurse } } Write-Verbose -Verbose "Files to process:`n$($FileCollection.Name)" $MDHeader = @' # {{SourceFile}} [[_TOC_]] '@ foreach ($SourceFile in $FileCollection) { $FileDocumentationResult = $MDHeader.Replace('{{SourceFile}}', $SourceFile.Name) #region build Bicep PS object try { $BuildObject = (Build-BicepFile -Path $SourceFile.FullName -ErrorAction Stop) | ConvertFrom-Json -Depth 100 # The language version of the ARM template defines the schema # 2.0 changes the resources array to a dictionary of key values $LanguageVersion = $BuildObject.languageVersion ?? '1.0' } catch { Write-Error -Message "Failed to build $($SourceFile.Name) - $($_.Exception.Message)" switch ($ErrorActionPreference) { 'Stop' { throw } default { continue } } } #endregion #region Get used modules in the bicep file $UsedModules = Get-BicepUsedModules -Path $SourceFile.FullName -ErrorAction Stop #endregion #region Add Metadata to MD output $MDMetadata = NewMDMetadata -Metadata $BuildObject.metadata $FileDocumentationResult += @" ## Metadata $MDMetadata "@ #endregion #region Add providers to MD output $MDProviders = NewMDProviders -Resources $BuildObject.resources -LanguageVersion $LanguageVersion $FileDocumentationResult += @" ## Providers $MDProviders "@ #endregion #region Add Resources to MD output $MDResources = NewMDResources -Resources $BuildObject.resources -LanguageVersion $LanguageVersion $FileDocumentationResult += @" ## Resources $MDResources "@ #endregion #region Add Parameters to MD output $MDParameters = NewMDParameters -Parameters $BuildObject.parameters $FileDocumentationResult += @" ## Parameters $MDParameters "@ #endregion #region Add Variables to MD output $MDVariables = NewMDVariables -Variables $BuildObject.variables $FileDocumentationResult += @" ## Variables $MDVariables "@ #endregion #region Add Outputs to MD output $MDOutputs = NewMDOutputs -Outputs $BuildObject.outputs $FileDocumentationResult += @" ## Outputs $MDOutputs "@ #endregion #region Add Modules to MD output $MDModules = NewMDModules -Modules $UsedModules $FileDocumentationResult += @" ## Modules $MDModules "@ #endregion #region output if ($AsString) { $FileDocumentationResult } else { $OutFileName = $SourceFile.FullName -replace '\.bicep$', '.md' $FileDocumentationResult | Out-File $OutFileName } #endregion } } #EndRegion './Public/New-BicepMarkdownDocumentation.ps1' 162 #Region './Public/New-BicepParameterFile.ps1' -1 function New-BicepParameterFile { [CmdletBinding(SupportsShouldProcess)] param ( [Parameter(Mandatory, Position = 1)] [string]$Path, [Parameter(Position = 2)] [ValidateNotNullOrEmpty()] [ValidateSet("All", "Required")] [string]$Parameters, [Parameter(Position = 3)] [ValidateNotNullOrEmpty()] [string]$OutputDirectory ) begin { if ($PSBoundParameters.ContainsKey('OutputDirectory') -and (-not (Test-Path $OutputDirectory))) { $null = New-Item $OutputDirectory -Force -ItemType Directory -WhatIf:$WhatIfPreference } } process { $File = Get-Item -Path $Path $validateBicepFile = Test-BicepFile -Path $File.FullName -AcceptDiagnosticLevel Warning -IgnoreDiagnosticOutput if (-not $validateBicepFile) { Write-Error -Message "$($File.FullName) have build errors, make sure that the Bicep template builds successfully and try again." Write-Host "`nYou can use either 'Test-BicepFile' or 'Build-Bicep' to verify that the template builds successfully.`n" break } if ($File) { $ARMTemplate = Build-BicepFile -Path $file.FullName if($PSBoundParameters.ContainsKey('OutputDirectory')) { $OutputFilePath = Join-Path -Path $OutputDirectory -ChildPath ('{0}.parameters.json' -f $File.BaseName) } else { $OutputFilePath = $File.FullName -replace '\.bicep','.parameters.json' } if (-not $PSBoundParameters.ContainsKey('Parameters')){ $Parameters='Required' } GenerateParameterFile -Content $ARMTemplate -Parameters $Parameters -DestinationPath $OutputFilePath -WhatIf:$WhatIfPreference } else { Write-Error "No bicep file named $Path was found!" } } } #EndRegion './Public/New-BicepParameterFile.ps1' 50 #Region './Public/Publish-Bicep.ps1' -1 function Publish-Bicep { [CmdletBinding()] param ( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [ValidateScript({Test-Path $_})] [string]$Path, [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [ValidatePattern('^(?<Prefix>[bBrR]{2})(?<ACROrAlias>(:[\w\-_]+\.azurecr.io|\/[\w\-\._]+:))(?<path>[\w\/\-\._]+)(?<tag>:[\w\/\-\._]+)$', ErrorMessage = 'Target does not match pattern for registry. Specify a path to a registry using "br:", or "br/" if using an alias.')] [string]$Target, [Parameter()] [ValidateNotNullOrEmpty()] [string]$DocumentationUri, [Parameter()] [switch]$PublishSource, [Parameter()] [switch]$Force ) process { $BicepFile = Get-Childitem -Path $Path -File try { $validBicep = Test-BicepFile -Path $BicepFile.FullName -IgnoreDiagnosticOutput -AcceptDiagnosticLevel Warning -Verbose:$false if (-not ($validBicep)) { throw "The provided bicep is not valid. Make sure that your bicep file builds successfully before publishing." } else { Write-Verbose "[$($BicepFile.Name)] is valid" } } catch { $_.CategoryInfo.Activity = 'Publish-Bicep' Throw $_ } $bicepConfig= Get-BicepConfig -Path $BicepFile if ($VerbosePreference -eq [System.Management.Automation.ActionPreference]::Continue) { Write-Verbose -Message "Using Bicep configuration: $($bicepConfig.Path)" } AssertAzureConnection -TokenSplat $script:TokenSplat -BicepConfig $bicepConfig -ErrorAction 'Stop' # Publish module $PublishParams = @{ Path = $BicepFile.FullName Target = $Target PublishSource = $PublishSource.IsPresent Token = $script:Token.Token Force = $Force.IsPresent } if($PSBoundParameters.ContainsKey('DocumentationUri')) { $PublishParams.Add('DocumentationUri', $DocumentationUri) } try { Publish-BicepFile @PublishParams -ErrorAction Stop Write-Verbose -Message "[$($BicepFile.Name)] published to: [$Target]" } catch { $_.CategoryInfo.Activity = 'Publish-Bicep' throw $_ } } } #EndRegion './Public/Publish-Bicep.ps1' 70 #Region './Public/Restore-Bicep.ps1' -1 function Restore-Bicep { [CmdletBinding()] param ( [Parameter(Mandatory, Position = 1)] [ValidateNotNullOrEmpty()] [string]$Path ) begin { } process { $BicepToken = @{} $BicepFile = Get-Childitem -Path $Path -File $Config = Get-BicepConfig -Path $BicepFile try { AssertAzureConnection -TokenSplat $script:TokenSplat -BicepConfig $Config -ErrorAction 'Stop' $BicepToken['Token'] = $script:Token.Token } catch { # We don't care about errors here, let bicep throw if authentication is needed } if ($VerbosePreference -eq [System.Management.Automation.ActionPreference]::Continue) { Write-Verbose -Message "Using Bicep configuration: $($bicepConfig.Path)" } # Restore modules try { Restore-BicepFile -Path $BicepFile.FullName @BicepToken -ErrorAction Stop Write-Verbose -Message "Successfully restored all modules" } catch { Throw $_ } } } #EndRegion './Public/Restore-Bicep.ps1' 40 #Region './Public/Test-BicepFile.ps1' -1 # TODO This won't work with Bicep, has to be rewritten. function Test-BicepFile { [CmdletBinding()] param ( # Specifies a path to one or more locations. [Parameter( Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true )] [Alias("PSPath")] [ValidateNotNullOrEmpty()] [string] $Path, # Set output type [Parameter()] [ValidateSet('Simple', 'Json')] [String] $OutputType = 'Simple', # Level of diagnostic that will fail the test [Parameter()] [BicepDiagnosticLevel] $AcceptDiagnosticLevel = [BicepDiagnosticLevel]::Info, # Write diagnostic output to screen [Parameter()] [switch] $IgnoreDiagnosticOutput ) begin { if ($AcceptDiagnosticLevel -eq [BicepDiagnosticLevel]::Error) { throw 'Accepting diagnostic level Error results in test never failing.' } } process { $file = Get-Item -Path $Path try { $ParseParams = @{ Path = $file.FullName InformationVariable = 'DiagnosticOutput' ErrorAction = 'Stop' } if ($VerbosePreference -eq [System.Management.Automation.ActionPreference]::Continue) { $bicepConfig= Get-BicepConfig -Path $file Write-Verbose -Message "Using Bicep configuration: $($bicepConfig.Path)" } $BuildResult = Build-BicepFile -Path $file.FullName if (-not $IgnoreDiagnosticOutput) { $BuildResult.Diagnostic | WriteBicepDiagnostic -InformationAction 'Continue' } } catch { # We don't care about errors here. } $DiagnosticGroups = $BuildResult.Diagnostic | Group-Object -Property { $_.Level } $HighestDiagLevel = $null foreach ($DiagGroup in $DiagnosticGroups) { $Level = [int][BicepDiagnosticLevel]$DiagGroup.Name if ($Level -gt $HighestDiagLevel) { $HighestDiagLevel = $Level } } switch ($OutputType) { 'Simple' { if ([int]$AcceptDiagnosticLevel -ge $HighestDiagLevel) { return $true } else { return $false } } 'Json' { $Result = @{} foreach ($Group in $DiagnosticGroups) { $List = foreach ($Entry in $Group.Group) { @{ Path = $Entry.LocalPath Line = [int]$Entry.Position[0] + 1 Character = [int]$Entry.Position[1] + 1 Level = $Entry.Level.ToString() Code = $Entry.Code Message = $Entry.Message } } $Result[$Group.Name] = @($List) } return $Result | ConvertTo-Json -Depth 5 } default { # this should never happen but just to make sure throw "$_ has not been implemented yet." } } } } #EndRegion './Public/Test-BicepFile.ps1' 109 #Region './Public/Uninstall-BicepCLI.ps1' -1 function Uninstall-BicepCLI { [CmdLetBinding()] param ( [switch]$Force ) if (-not $IsWindows) { Write-Error -Message "This cmdlet is only supported for Windows systems. ` To uninstall Bicep on your system see instructions on https://github.com/Azure/bicep" Write-Host "`nList the available module cmdlets by running 'Get-Help -Name Bicep'`n" break } if (TestBicep) { # Test if we are running as administrators. $currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()) $IsAdmin = $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) if (-not $IsAdmin -and -not $Force) { Write-Error 'Some Bicep parts might not be properly uninstalled unless you run elevated. Use the -Force switch to try anyway. Any Bicep version installed by Azure CLI to %USERPROFILE%\.Azure\bin will not be uninstalled.' } if (-not $IsAdmin -and $Force) { Write-Host 'You are not running elevated. We may not be able to remove all parts.' } if ($IsAdmin -or $Force) { $UninstallerFileName = 'unins000.exe' $BicepExeName = 'bicep.exe' $BicepInstalls = $env:Path -split ';' | Where-Object { $_ -like "*\.bicep" -or $_ -like "*\Bicep CLI" } foreach ($Install in $BicepInstalls) { $FileContents = Get-ChildItem $Install if (($UninstallerFileName -in $FileContents.Name) -and ($BicepExeName -in $FileContents.Name)) { # Bicep is installed using installer. Try using it to uninstall $UninstallerPath = ($FileContents | Where-Object -Property Name -eq $UninstallerFileName).FullName & $UninstallerPath /VERYSILENT do { $UninstallProcess = Get-Process -Name $UninstallerFileName.Replace('.exe', '') -ErrorAction SilentlyContinue Start-Sleep -Seconds 1 } until ($null -eq $UninstallProcess) } else { # Bicep is running in standalone exe mode. Remove manualy $ExePath = Join-path -Path $Install -ChildPath $BicepExeName if (Test-Path $ExePath) { Remove-Item $ExePath } } } # verify that no bicep install is still reachable $Removed = TestBicep if ($Removed) { Throw "Unknown version of Bicep is still installed." } else { Write-Host "Successfully removed bicep." } } } else { Write-Error "Bicep CLI is not installed on this device, nothing to uninstall." } } #EndRegion './Public/Uninstall-BicepCLI.ps1' 65 #Region './Public/Update-BicepCLI.ps1' -1 function Update-BicepCLI { [CmdletBinding()] param ( ) if (-not $IsWindows) { Write-Error -Message "This cmdlet is only supported for Windows systems. ` To update Bicep on your system see instructions on https://github.com/Azure/bicep" Write-Host "`nCompare your Bicep version with latest version by running Get-BicepVersion`n" break } $versionCheck = CompareBicepVersion if ($versionCheck) { Write-Host "You are already running the latest version of Bicep CLI." } else { Uninstall-BicepCLI -Force -ErrorAction SilentlyContinue Install-BicepCLI -Force } } #EndRegion './Public/Update-BicepCLI.ps1' 23 #Region './Public/Update-BicepParameterFile.ps1' -1 function Update-BicepParameterFile { [CmdletBinding()] param ( [ValidateNotNullOrEmpty()] [ValidatePattern('\.json$', ErrorMessage = 'Path must be a parameters file with a .json extension.')] [Parameter(Mandatory, Position = 1)] [string]$Path, [ValidateNotNullOrEmpty()] [ValidatePattern('\.bicep$', ErrorMessage = 'BicepFile must have a .bicep extension.')] [Parameter(Position = 2)] [string]$BicepFile, [ValidateNotNullOrEmpty()] [Parameter(Position = 3)] [ValidateSet('All', 'Required')] [string]$Parameters = 'All' ) begin { $tempPath = [System.Io.Path]::GetTempPath() } process { try { $ParamFile = Get-Item -Path $Path -ErrorAction Stop } catch { Write-Error "Cant find ParameterFile at specified Path $Path." Break } $FileName = $ParamFile.BaseName.Replace('.parameters', '') # Try to find a matching Bicep template for the provided parameter file based on its file name if (-not $PSBoundParameters.ContainsKey('BicepFile')) { $BicepFilePath = (Get-ChildItem $ParamFile.DirectoryName -Filter *.bicep | Where-Object { $_.BaseName -eq $FileName }).FullName if (-not $BicepFilePath) { Write-Error "Cant find BicepFile Named $FileName in directory: $($ParamFile.DirectoryName)" Break } } else { if (-not (Test-Path $BicepFile)) { Write-Error "Cant find BicepFile at specified Path $BicepFile." Break } $BicepFilePath = $BicepFile } $validateBicepFile = Test-BicepFile -Path $BicepFilePath -AcceptDiagnosticLevel Warning -IgnoreDiagnosticOutput if (-not $validateBicepFile) { Write-Error -Message "$BicepFilePath have build errors, make sure that the Bicep template builds successfully and try again." Write-Host "`nYou can use either 'Test-BicepFile' or 'Build-Bicep' to verify that the template builds successfully.`n" break } # Import the old parameter file and convert it to an ordered hashtable $oldParametersFile = Get-Content -Path $Path | ConvertFrom-Json -Depth 100 | ConvertToHashtable -Ordered # Generate a temporary Bicep Parameter File with all parameters $BicepFileName = (Get-Item -Path $BicepFilePath).BaseName New-BicepParameterFile -Path $BicepFilePath -OutputDirectory $tempPath -Parameters All $allParametersFilePath = $tempPath + "$($BicepFileName).parameters.json" # Convert the all parameters file to an ordered hashtable try { $allParametersFile = Get-Content -Path $allParametersFilePath -ErrorAction Stop | ConvertFrom-Json -Depth 100 | ConvertToHashtable -Ordered } catch { Write-Error "Failed to create Bicep ParameterObject." Break } # Remove any deleted parameters from old parameter file $oldParameterArray = @() $oldParametersFile.parameters.Keys.ForEach( { $oldParameterArray += $_ }) foreach ($item in $oldParameterArray) { if (-not $allParametersFile.parameters.Contains($item)) { $oldParametersFile.parameters.Remove($item) } } # Generate a new temporary Bicep Parameter File if -Parameters parameter is set to Required if ($Parameters -eq 'Required') { $BicepFileName = (Get-Item -Path $BicepFilePath).BaseName New-BicepParameterFile -Path $BicepFilePath -OutputDirectory $tempPath -Parameters $Parameters } $NewParametersFilePath = $tempPath + "$BicepFileName.parameters.json" # Convert the new parameter file to an ordered hashtable try { $NewParametersFile = Get-Content -Path $NewParametersFilePath -ErrorAction Stop | ConvertFrom-Json -Depth 100 | ConvertToHashtable -Ordered } catch { Write-Error "Failed to create Bicep ParameterObject." Break } $ParameterArray = @() $NewParametersFile.parameters.Keys.ForEach( { $ParameterArray += $_ }) # Iterate over the new parameters and add any missing to the old parameters array foreach ($item in $ParameterArray) { if (-not $oldParametersFile.parameters.Contains($item)) { $oldParametersFile.parameters[$item] = @{ value = $NewParametersFile.parameters.$item.value } } } $oldParametersFile | ConvertTo-Json -Depth 100 | Out-File -Path $Path -Force } end { Remove-Item $NewParametersFilePath -Force } } #EndRegion './Public/Update-BicepParameterFile.ps1' 124 #Region './Public/Update-BicepTypes.ps1' -1 function Update-BicepTypes { [CmdletBinding(SupportsShouldProcess)] param () $ModulePath = (Get-Module Bicep).Path $ModuleFolder = Split-Path -Path $ModulePath # Where the file is stored $BicepTypesPath = Join-Path -Path $ModuleFolder -ChildPath 'Assets\BicepTypes.json' # Url to fetch new types from $BicepTypesUrl = 'https://raw.githubusercontent.com/Azure/bicep-types-az/main/generated/index.json' Write-Verbose "Fetching types from GitHub: $BicepTypesUrl" try { $BicepTypes = Invoke-RestMethod -Uri $BicepTypesUrl -Verbose:$false } catch { Throw "Unable to get new Bicep types from GitHub. $_" } Write-Verbose "Filtering content" # If the Resources property does not exist we want to throw an error if ($BicepTypes.psobject.Properties.name -notcontains 'Resources') { Throw "Resources not found in index file." } try { $TypesOnly = ConvertTo-Json -InputObject $BicepTypes.Resources.psobject.Properties.name -Compress } catch { Throw "Unable to filter content. Index file might have changed. $_" } Write-Verbose "Saving to disk at '$BicepTypesPath'" try { Out-File -FilePath $BicepTypesPath -InputObject $TypesOnly -WhatIf:$WhatIfPreference } catch { Throw "Failed to save new Bicep types. $_" } if (-not $WhatIfPreference) { Write-Host "Updated Bicep types." } # To avoid having to re-import the module, update the module variable $null = GetBicepTypes -Path $BicepTypesPath } #EndRegion './Public/Update-BicepTypes.ps1' 53 #Region './suffix.ps1' -1 $script:BicepTypesPath = Join-Path -Path $PSScriptRoot -ChildPath 'Assets/BicepTypes.json' $script:ModuleManifestPath = Join-Path -Path $PSScriptRoot -ChildPath 'Bicep.psd1' $script:TokenSplat = @{} Write-Verbose "Preloading Bicep types from: '$BicepTypesPath'" $null = GetBicepTypes -Path "$BicepTypesPath" TestModuleVersion #EndRegion './suffix.ps1' 7 |