Private/ConvertTo-AACTerraformFile.ps1
|
function ConvertTo-AACTerraformFile { <# .SYNOPSIS Lays out what the Azure Export for Terraform API returned - one export, or several (batches, and the azapi pass) - as one Terraform folder you can run: the files and their text, and every exported resource with its Terraform address and Azure ID. .DESCRIPTION -Source takes each export's result, in order: @{ Result (its configuration and import); Provider (azurerm or azapi); File (main.tf or azapi.tf) }. Each configuration carries its own terraform and provider blocks; a module may declare its providers only once, so they are merged: terraform.tf one terraform block, required_providers of all providers.tf the provider blocks - subscription_id added where missing, so the folder runs without ARM_SUBSCRIPTION_ID (azurerm 4 requires one) main.tf the azurerm resources azapi.tf the azapi resources import.tf the import blocks of all A resource two exports both returned (a child exported with its parent's batch and again on its own) is kept once - the first - and references to the other copy are pointed at it. -Primary and -Fallback are the two-export shorthand: the first export, and the azapi export of what it skipped. Returns @{ Files (ordered: name -> text); Imports (@{ Address; Id; Provider; File }); Duplicates (how many were dropped) }. #> [CmdletBinding(DefaultParameterSetName = 'Source')] [OutputType([hashtable])] param( [Parameter(Mandatory, ParameterSetName = 'Source')] [AllowEmptyCollection()] [object[]] $Source, [Parameter(Mandatory, ParameterSetName = 'Pair')] [System.Collections.IDictionary] $Primary, [Parameter(ParameterSetName = 'Pair')] [System.Collections.IDictionary] $Fallback, [Parameter(Mandatory, ParameterSetName = 'Pair')] [string] $PrimaryProvider, [string] $SubscriptionId ) if ($PSCmdlet.ParameterSetName -eq 'Pair') { $Source = @(@{ Result = $Primary; Provider = $PrimaryProvider; File = $(if ($PrimaryProvider -eq 'azapi') { 'azapi.tf' } else { 'main.tf' }) }) if ($Fallback) { $Source += @{ Result = $Fallback; Provider = 'azapi'; File = 'azapi.tf' } } } $requiredProviders = [ordered]@{} $providers = [ordered]@{} $imports = [System.Collections.Generic.List[hashtable]]::new() $files = [ordered]@{} $resourceText = [ordered]@{ 'main.tf' = [System.Collections.Generic.List[hashtable]]::new(); 'azapi.tf' = [System.Collections.Generic.List[hashtable]]::new() } $importText = [System.Collections.Generic.List[string]]::new() $kept = @{} $renamed = @{} $duplicates = 0 # The text between the braces of the first "<name> {" in a block, or ''. $body = { param([string] $Block, [string] $Name) $open = [regex]::Match($Block, "(?m)\b$([regex]::Escape($Name))\s*\{") if (-not $open.Success) { return '' } $parts = @(Split-AACHclBlock -Text "x $($Block.Substring($open.Index + $open.Length - 1))") if (-not $parts.Count) { return '' } $t = [string]$parts[0].Text $t.Substring($t.IndexOf('{') + 1, $t.LastIndexOf('}') - $t.IndexOf('{') - 1) } foreach ($item in $Source) { $result = if ($item.Result -is [System.Collections.IDictionary]) { $item.Result } else { @{} } $file = [string]$item.File if (-not $resourceText.Contains($file)) { $resourceText[$file] = [System.Collections.Generic.List[hashtable]]::new() } # Its imports first: an Azure ID already exported means this copy of the resource goes. $dropped = @{} foreach ($block in @(Split-AACHclBlock -Text ([string]$result['import']) | Where-Object { $_.Type -eq 'import' })) { $id = [regex]::Match($block.Text, '\bid\s*=\s*"([^"]+)"') $to = [regex]::Match($block.Text, '\bto\s*=\s*([^\s}]+)') if (-not ($id.Success -and $to.Success)) { continue } $address = $to.Groups[1].Value $same = ($id.Groups[1].Value -split '\?')[0].ToLowerInvariant() if ($kept.ContainsKey($same)) { $duplicates++; if ($address -ne $kept[$same]) { $renamed[$address] = $kept[$same] }; $dropped[$address] = $true; continue } $kept[$same] = $address $imports.Add(@{ Address = $address; Id = $id.Groups[1].Value; Provider = [string]$item.Provider; File = $file }) $importText.Add([string]$block.Text) } foreach ($block in @(Split-AACHclBlock -Text ([string]$result['configuration']))) { switch ($block.Type) { 'terraform' { # Each entry of required_providers: "azurerm = { source = ..., version = ... }". $inner = & $body $block.Text 'required_providers' foreach ($entry in [regex]::Matches($inner, '(?m)^\s*([A-Za-z_][A-Za-z0-9_-]*)\s*=\s*\{')) { $name = $entry.Groups[1].Value if ($requiredProviders.Contains($name)) { continue } # Its attributes, one per line - laid out again with the same indentation whatever the source used. $value = @(Split-AACHclBlock -Text "x $($inner.Substring($entry.Index + $entry.Length - 1))") if ($value.Count) { $t = [string]$value[0].Text $requiredProviders[$name] = @($t.Substring($t.IndexOf('{') + 1, $t.LastIndexOf('}') - $t.IndexOf('{') - 1) -split "`r?`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ }) } } } 'provider' { $name = if ($block.Labels.Count) { [string]$block.Labels[0] } else { '' } if ($name -and -not $providers.Contains($name)) { $text = [string]$block.Text if ($SubscriptionId -and $name -in 'azurerm', 'azapi' -and $text -notmatch '\bsubscription_id\s*=') { $brace = $text.IndexOf('{') $text = $text.Substring(0, $brace + 1) + "`n subscription_id = `"$SubscriptionId`"" + $text.Substring($brace + 1) } $providers[$name] = $text } } default { $address = if ($block.Type -eq 'resource' -and $block.Labels.Count -ge 2) { "$($block.Labels[0]).$($block.Labels[1])" } else { '' } if ($address -and $dropped.ContainsKey($address)) { continue } $resourceText[$file].Add(@{ Text = [string]$block.Text }) } } } } # References to a dropped copy point at the one kept. $repoint = { param([string] $Text) foreach ($from in $renamed.Keys) { $Text = [regex]::Replace($Text, "(?<![\w.-])$([regex]::Escape($from))(?![\w-])", [string]$renamed[$from]) } $Text } $nl = "`n" if ($requiredProviders.Count) { $entries = foreach ($name in $requiredProviders.Keys) { " $name = {$nl$(@($requiredProviders[$name] | ForEach-Object { " $_" }) -join $nl)$nl }" } $files['terraform.tf'] = "terraform {$nl required_providers {$nl$($entries -join $nl)$nl }$nl}$nl" } if ($providers.Count) { $files['providers.tf'] = (@($providers.Values) -join "$nl$nl") + $nl } foreach ($file in $resourceText.Keys) { if ($resourceText[$file].Count) { $files[$file] = (@($resourceText[$file] | ForEach-Object { & $repoint $_.Text }) -join "$nl$nl") + $nl } } if ($importText.Count) { $files['import.tf'] = (@($importText) -join "$nl$nl") + $nl } @{ Files = $files; Imports = $imports.ToArray(); Duplicates = $duplicates } } |