LibreDevOpsHelpers.Terraform.AzureImport/LibreDevOpsHelpers.Terraform.AzureImport.psm1
if (-not $script:SubscriptionIdCache) { $script:SubscriptionIdCache = $null } function Get-AzureCliTerraformImportResourceId { [CmdletBinding()] param( [Parameter(Mandatory)][string] $TfType, [Parameter(Mandatory)][psobject]$After, [string]$SubscriptionId ) try { _LogMessage INFO "Resolving ARM ID for $TfType/$( $After.name )" ` -InvocationName $MyInvocation.MyCommand.Name # ── 1. subscription id (cached) ────────────────────────────────────── if (-not $SubscriptionId) { if (-not $script:SubscriptionIdCache) { _LogMessage INFO 'az account show --query id -o tsv' ` -InvocationName $MyInvocation.MyCommand.Name $script:SubscriptionIdCache = & az account show --query id -o tsv } $SubscriptionId = $script:SubscriptionIdCache } # ── 2. fast-path map ───────────────────────────────────────────────── $TypeMap = @{ azurerm_resource_group = { "/subscriptions/$SubscriptionId/resourceGroups/$( $After.name )" } azurerm_storage_account = { "/subscriptions/$SubscriptionId/resourceGroups/$( $After.resource_group_name )/providers/Microsoft.Storage/storageAccounts/$( $After.name )" } azurerm_storage_container = { "/subscriptions/$SubscriptionId/resourceGroups/$( $After.resource_group_name )/providers/Microsoft.Storage/storageAccounts/$( $After.storage_account_name )/blobServices/default/containers/$( $After.name )" } azurerm_virtual_network = { "/subscriptions/$SubscriptionId/resourceGroups/$( $After.resource_group_name )/providers/Microsoft.Network/virtualNetworks/$( $After.name )" } azurerm_subnet = { "/subscriptions/$SubscriptionId/resourceGroups/$( $After.resource_group_name )/providers/Microsoft.Network/virtualNetworks/$( $After.virtual_network_name )/subnets/$( $After.name )" } azurerm_user_assigned_identity = { "/subscriptions/$SubscriptionId/resourceGroups/$( $After.resource_group_name )/providers/Microsoft.ManagedIdentity/userAssignedIdentities/$( $After.name )" } azurerm_network_security_group = { "/subscriptions/$SubscriptionId/resourceGroups/$( $After.resource_group_name )/providers/Microsoft.Network/networkSecurityGroups/$( $After.name )" } azurerm_network_security_rule = { "/subscriptions/$SubscriptionId/resourceGroups/$( $After.resource_group_name )/providers/Microsoft.Network/networkSecurityGroups/$( $After.network_security_group_name )/securityRules/$( $After.name )" } azurerm_subnet_network_security_group_association = { $After.subnet_id } azurerm_key_vault = { "/subscriptions/$SubscriptionId/resourceGroups/$( $After.resource_group_name )/providers/Microsoft.KeyVault/vaults/$( $After.name )" } azurerm_private_endpoint = { "/subscriptions/$SubscriptionId/resourceGroups/$( $After.resource_group_name )/providers/Microsoft.Network/privateEndpoints/$( $After.name )" } azurerm_databricks_workspace = { "/subscriptions/$SubscriptionId/resourceGroups/$( $After.resource_group_name )/providers/Microsoft.Databricks/workspaces/$( $After.name )" } azurerm_windows_function_app = { "/subscriptions/$SubscriptionId/resourceGroups/$( $After.resource_group_name )/providers/Microsoft.Web/sites/$( $After.name )" } azurerm_service_plan = { "/subscriptions/$SubscriptionId/resourceGroups/$( $After.resource_group_name )/providers/Microsoft.Web/serverfarms/$( $After.name )" } } if ( $TypeMap.ContainsKey($TfType)) { return (& $TypeMap[$TfType]) } # ── 3. generic az resource list ────────────────────────────────────── $azType = if ($TfType -eq 'azurerm_subnet_network_security_group_association') { 'Microsoft.Network/virtualNetworks/subnets' } else { $TfType -replace '^azurerm_', '' -replace '_', '/' } $listCmd = "az resource list --name `"$( $After.name )`" --resource-type `"$azType`" --query `[0].id` -o tsv" _LogMessage INFO $listCmd -InvocationName $MyInvocation.MyCommand.Name $id = & az resource list --name $After.name --resource-type $azType --query '[0].id' -o tsv if ($id) { return $id } # ── 4. Azure Resource Graph fallback ───────────────────────────────── $kusto = "Resources | where name == '$( $After.name )' | take 1 | project id" _LogMessage INFO "az graph query -q `"$kusto`" --first 1 --output tsv" ` -InvocationName $MyInvocation.MyCommand.Name return & az graph query -q $kusto --first 1 --output tsv } catch { _LogMessage ERROR "ARM-ID lookup failed for ${TfType}: $_" ` -InvocationName $MyInvocation.MyCommand.Name return $null } } # ──────────────────────────────────────────────────────────────────────────────── # Main entry point # ──────────────────────────────────────────────────────────────────────────────── function Invoke-TerraformImportFromPlan { [CmdletBinding()] param( [Parameter(Mandatory)][string]$PlanJson, [string]$CodePath = ".", [switch]$WhatIf, [string]$Manifest = "./import-map.csv" ) $ErrorActionPreference = 'Stop' try { _LogMessage INFO "Reading plan file $PlanJson" -InvocationName $MyInvocation.MyCommand.Name $plan = Get-Content $PlanJson -Raw | ConvertFrom-Json } catch { _LogMessage ERROR "Cannot read or parse plan: $_" -InvocationName $MyInvocation.MyCommand.Name throw } $imports = @() foreach ($chg in $plan.resource_changes) { if ($chg.mode -ne 'managed') { continue } if ($chg.change.actions -notcontains 'create') { continue } $addr = $chg.address $after = $chg.change.after $type = $chg.type # ── fill required props for subnet-NSG assoc. ─────────────────────── if ($type -eq 'azurerm_subnet_network_security_group_association') { if (-not $after.name -and ($addr -match '\["([^"]+)"\]')) { $after | Add-Member name $matches[1] } $sub = $plan.resource_changes | Where-Object { $_.type -eq 'azurerm_subnet' -and $_.change.after.name -eq $after.name } | Select-Object -First 1 if ($sub) { foreach ($p in 'resource_group_name', 'virtual_network_name') { if (-not $after.$p) { $after | Add-Member $p $sub.change.after.$p } } if (-not $after.subnet_id) { $sid = Get-AzureCliTerraformImportResourceId ` 'azurerm_subnet' $sub.change.after if ($sid) { $after | Add-Member subnet_id $sid } } } } try { $id = Get-AzureCliTerraformImportResourceId -TfType $type -After $after if (-not $id) { _LogMessage WARN "⤫ No ARM ID for ${addr} (${type}) — skipping" ` -InvocationName $MyInvocation.MyCommand.Name continue } _LogMessage INFO "✓ Mapped ${addr} → ${id}" -InvocationName $MyInvocation.MyCommand.Name if ($type -ne 'azurerm_subnet_network_security_group_association') { $showCmd = "az resource show --ids `"$id`" --query id -o tsv" _LogMessage INFO $showCmd -InvocationName $MyInvocation.MyCommand.Name if (-not (az resource show --ids $id --query id -o tsv 2> $null)) { _LogMessage WARN "⤫ Azure says not found — skipping ${addr}" ` -InvocationName $MyInvocation.MyCommand.Name continue } _LogMessage INFO "✓ Confirmed existing resource for ${addr}" ` -InvocationName $MyInvocation.MyCommand.Name } $imports += [pscustomobject]@{ Address = $addr; Id = $id } } catch { _LogMessage ERROR "Lookup/import prep failed for ${addr}: $_" ` -InvocationName $MyInvocation.MyCommand.Name } } if (-not $imports) { _LogMessage INFO "Nothing to import – plan has no unmanaged Azure resources." ` -InvocationName $MyInvocation.MyCommand.Name return } # ── NEW: sort parent → child (fewer “/” first) ────────────────────────── $imports = $imports | Sort-Object { (($_.Id -split '/').Count) } try { $imports | Export-Csv $Manifest -NoTypeInformation _LogMessage INFO "Wrote manifest to $Manifest" -InvocationName $MyInvocation.MyCommand.Name } catch { _LogMessage ERROR "Failed to write manifest: $_" -InvocationName $MyInvocation.MyCommand.Name } foreach ($i in $imports) { $cmdArgs = @('--%', $i.Address, $i.Id) if ($WhatIf) { _LogMessage INFO "[DRY-RUN] terraform import $( $cmdArgs -join ' ' )" ` -InvocationName $MyInvocation.MyCommand.Name continue } try { Push-Location $CodePath _LogMessage INFO "Importing $( $i.Address )" -InvocationName $MyInvocation.MyCommand.Name & terraform import @cmdArgs 2>&1 _LogMessage INFO "Imported $( $i.Address )" -InvocationName $MyInvocation.MyCommand.Name } catch { _LogMessage ERROR "terraform import failed for $( $i.Address ): $_" ` -InvocationName $MyInvocation.MyCommand.Name } finally { Pop-Location } } _LogMessage INFO "Completed: $( $imports.Count ) resource(s) processed." ` -InvocationName $MyInvocation.MyCommand.Name } Export-ModuleMember -Function ` Get-AzureCliTerraformImportResourceId, ` Invoke-TerraformImportFromPlan |