|
| 1 | +#requires -Version 7.0 |
| 2 | + |
| 3 | +[CmdletBinding()] |
| 4 | +param( |
| 5 | + [Parameter(Mandatory)] |
| 6 | + [string]$SourceResourceGroup, |
| 7 | + |
| 8 | + [Parameter(Mandatory)] |
| 9 | + [string]$TargetResourceGroup, |
| 10 | + |
| 11 | + [ValidateSet('Developer', 'Premium', 'StandardV2', 'PremiumV2')] |
| 12 | + [string]$SkuName = 'StandardV2', |
| 13 | + |
| 14 | + [string]$Location = 'eastus2', |
| 15 | + |
| 16 | + [ValidateSet('Info', 'Verbose', 'Debug')] |
| 17 | + [string]$LogLevel = 'Verbose', |
| 18 | + |
| 19 | + [Parameter(Mandatory)] |
| 20 | + [string]$PublisherEmail, |
| 21 | + |
| 22 | + [Parameter(Mandatory)] |
| 23 | + [string]$StateFile |
| 24 | +) |
| 25 | + |
| 26 | +$ErrorActionPreference = 'Stop' |
| 27 | +$VerbosePreference = if ($LogLevel -in @('Verbose', 'Debug')) { 'Continue' } else { 'SilentlyContinue' } |
| 28 | +$DebugPreference = if ($LogLevel -eq 'Debug') { 'Continue' } else { 'SilentlyContinue' } |
| 29 | + |
| 30 | +$maskingModule = Join-Path $PSScriptRoot 'MaskingHelpers.psm1' |
| 31 | +Import-Module $maskingModule -Force |
| 32 | + |
| 33 | +$account = az account show --output json 2>$null | ConvertFrom-Json |
| 34 | +if (-not $account) { |
| 35 | + Write-Error "Not logged in to Azure CLI. Run 'az login' first." |
| 36 | + exit 2 |
| 37 | +} |
| 38 | + |
| 39 | +$subscriptionId = $account.id |
| 40 | +Write-Host "🔐 Azure CLI authenticated: $($account.name) ($(Protect-SubscriptionId -Value $subscriptionId))" |
| 41 | + |
| 42 | +$deploySourceScript = Join-Path $PSScriptRoot 'Deploy-SourceApim.ps1' |
| 43 | +$deployTargetScript = Join-Path $PSScriptRoot 'Deploy-TargetApim.ps1' |
| 44 | + |
| 45 | +foreach ($requiredFile in @($maskingModule, $deploySourceScript, $deployTargetScript)) { |
| 46 | + if (-not (Test-Path $requiredFile)) { |
| 47 | + Write-Error "Required file not found: $requiredFile" |
| 48 | + exit 2 |
| 49 | + } |
| 50 | +} |
| 51 | + |
| 52 | +$logsDir = Join-Path $PSScriptRoot 'logs' |
| 53 | +if (-not (Test-Path $logsDir)) { |
| 54 | + New-Item -ItemType Directory -Path $logsDir -Force | Out-Null |
| 55 | +} |
| 56 | +$timestamp = Get-Date -Format 'yyyyMMdd-HHmmss' |
| 57 | +$sourceLogFile = Join-Path $logsDir "source-deploy-$timestamp.log" |
| 58 | +$targetLogFile = Join-Path $logsDir "target-deploy-$timestamp.log" |
| 59 | + |
| 60 | +Write-Host "🚀 PHASE 1 — Deploy source and target APIM instances (parallel)" |
| 61 | + |
| 62 | +$sourceJob = Start-Job -Name 'DeploySource' -ScriptBlock { |
| 63 | + param($script, $rg, $sku, $loc, $email, $transcriptFile, $logLevel) |
| 64 | + $ErrorActionPreference = 'Stop' |
| 65 | + Start-Transcript -Path $transcriptFile -Force -UseMinimalHeader | Out-Null |
| 66 | + try { |
| 67 | + $scriptArgs = @{ |
| 68 | + ResourceGroupName = $rg |
| 69 | + SkuName = $sku |
| 70 | + Location = $loc |
| 71 | + PublisherEmail = $email |
| 72 | + LogLevel = $logLevel |
| 73 | + } |
| 74 | + $result = & $script @scriptArgs |
| 75 | + if (-not $result -or -not $result.apimServiceName) { |
| 76 | + throw "Source deployment returned no outputs" |
| 77 | + } |
| 78 | + return $result |
| 79 | + } finally { |
| 80 | + Stop-Transcript | Out-Null |
| 81 | + } |
| 82 | +} -ArgumentList $deploySourceScript, $SourceResourceGroup, $SkuName, $Location, $PublisherEmail, $sourceLogFile, $LogLevel |
| 83 | + |
| 84 | +$targetJob = Start-Job -Name 'DeployTarget' -ScriptBlock { |
| 85 | + param($script, $rg, $sku, $loc, $email, $transcriptFile, $logLevel) |
| 86 | + $ErrorActionPreference = 'Stop' |
| 87 | + Start-Transcript -Path $transcriptFile -Force -UseMinimalHeader | Out-Null |
| 88 | + try { |
| 89 | + $scriptArgs = @{ |
| 90 | + ResourceGroupName = $rg |
| 91 | + SkuName = $sku |
| 92 | + Location = $loc |
| 93 | + PublisherEmail = $email |
| 94 | + LogLevel = $logLevel |
| 95 | + } |
| 96 | + $result = & $script @scriptArgs |
| 97 | + if (-not $result -or -not $result.apimServiceName -or -not $result.apimServiceName.value) { |
| 98 | + throw "Target deployment returned no outputs" |
| 99 | + } |
| 100 | + return $result |
| 101 | + } finally { |
| 102 | + Stop-Transcript | Out-Null |
| 103 | + } |
| 104 | +} -ArgumentList $deployTargetScript, $TargetResourceGroup, $SkuName, $Location, $PublisherEmail, $targetLogFile, $LogLevel |
| 105 | + |
| 106 | +$jobs = @($sourceJob, $targetJob) |
| 107 | +$sourceLastPos = 0 |
| 108 | +$targetLastPos = 0 |
| 109 | + |
| 110 | +while (($jobs | Where-Object { $_.State -eq 'Running' }).Count -gt 0) { |
| 111 | + if (Test-Path $sourceLogFile) { |
| 112 | + $content = Get-Content $sourceLogFile -Raw -ErrorAction SilentlyContinue |
| 113 | + if ($content -and $content.Length -gt $sourceLastPos) { |
| 114 | + $newContent = $content.Substring($sourceLastPos) |
| 115 | + $newContent -split "`n" | Where-Object { $_.Trim() } | ForEach-Object { Write-Host " [SRC] $_" } |
| 116 | + $sourceLastPos = $content.Length |
| 117 | + } |
| 118 | + } |
| 119 | + |
| 120 | + if (Test-Path $targetLogFile) { |
| 121 | + $content = Get-Content $targetLogFile -Raw -ErrorAction SilentlyContinue |
| 122 | + if ($content -and $content.Length -gt $targetLastPos) { |
| 123 | + $newContent = $content.Substring($targetLastPos) |
| 124 | + $newContent -split "`n" | Where-Object { $_.Trim() } | ForEach-Object { Write-Host " [TGT] $_" } |
| 125 | + $targetLastPos = $content.Length |
| 126 | + } |
| 127 | + } |
| 128 | + |
| 129 | + Start-Sleep -Seconds 5 |
| 130 | +} |
| 131 | + |
| 132 | +$sourceOutputs = $null |
| 133 | +$targetOutputs = $null |
| 134 | +$exitCode = 0 |
| 135 | + |
| 136 | +if ($sourceJob.State -eq 'Failed') { |
| 137 | + Write-Host "❌ Source deployment failed:" |
| 138 | + $jobOutput = Receive-Job $sourceJob -ErrorAction SilentlyContinue 2>&1 |
| 139 | + if ($jobOutput) { $jobOutput | ForEach-Object { Write-Host " $_" } } |
| 140 | + if ($sourceJob.ChildJobs[0].Error) { |
| 141 | + $sourceJob.ChildJobs[0].Error | ForEach-Object { Write-Host " $_" } |
| 142 | + } |
| 143 | + $exitCode = 2 |
| 144 | +} else { |
| 145 | + $sourceOutputs = Receive-Job $sourceJob |
| 146 | + Write-Host " ✅ Source deployed: $(Protect-ApimName -Value $sourceOutputs.apimServiceName)" |
| 147 | +} |
| 148 | +Remove-Job $sourceJob |
| 149 | + |
| 150 | +if ($targetJob.State -eq 'Failed') { |
| 151 | + Write-Host "❌ Target deployment failed:" |
| 152 | + $jobOutput = Receive-Job $targetJob -ErrorAction SilentlyContinue 2>&1 |
| 153 | + if ($jobOutput) { $jobOutput | ForEach-Object { Write-Host " $_" } } |
| 154 | + if ($targetJob.ChildJobs[0].Error) { |
| 155 | + $targetJob.ChildJobs[0].Error | ForEach-Object { Write-Host " $_" } |
| 156 | + } |
| 157 | + $exitCode = 2 |
| 158 | +} else { |
| 159 | + $targetOutputs = Receive-Job $targetJob |
| 160 | + Write-Host " ✅ Target deployed: $(Protect-ApimName -Value $targetOutputs.apimServiceName.value)" |
| 161 | +} |
| 162 | +Remove-Job $targetJob |
| 163 | + |
| 164 | +if ($exitCode -ne 0) { |
| 165 | + exit $exitCode |
| 166 | +} |
| 167 | + |
| 168 | +$sourceSubId = if ($sourceOutputs -and $sourceOutputs.subscriptionId) { $sourceOutputs.subscriptionId } else { $subscriptionId } |
| 169 | +$sourceRg = if ($sourceOutputs -and $sourceOutputs.resourceGroup) { $sourceOutputs.resourceGroup } else { $SourceResourceGroup } |
| 170 | +$sourceName = if ($sourceOutputs -and $sourceOutputs.apimServiceName) { $sourceOutputs.apimServiceName } else { |
| 171 | + $apimName = az apim list --resource-group $SourceResourceGroup --query "[0].name" -o tsv 2>$null |
| 172 | + if (-not $apimName) { |
| 173 | + Write-Host "❌ No APIM instance found in resource group $(Protect-ResourceGroupName -Value $SourceResourceGroup)" |
| 174 | + exit 2 |
| 175 | + } |
| 176 | + $apimName |
| 177 | +} |
| 178 | + |
| 179 | +$targetSubId = if ($targetOutputs -and $targetOutputs.subscriptionId.value) { $targetOutputs.subscriptionId.value } else { $subscriptionId } |
| 180 | +$targetRg = if ($targetOutputs -and $targetOutputs.resourceGroupName.value) { $targetOutputs.resourceGroupName.value } else { $TargetResourceGroup } |
| 181 | +$targetName = if ($targetOutputs -and $targetOutputs.apimServiceName.value) { $targetOutputs.apimServiceName.value } else { |
| 182 | + $apimName = az apim list --resource-group $TargetResourceGroup --query "[0].name" -o tsv 2>$null |
| 183 | + if (-not $apimName) { |
| 184 | + Write-Host "❌ No APIM instance found in resource group $(Protect-ResourceGroupName -Value $TargetResourceGroup)" |
| 185 | + exit 2 |
| 186 | + } |
| 187 | + $apimName |
| 188 | +} |
| 189 | + |
| 190 | +if ($SkuName -in @('Developer', 'Premium')) { |
| 191 | + $postActivationState = az deployment group list ` |
| 192 | + --resource-group $sourceRg ` |
| 193 | + --query "sort_by([?starts_with(name, 'source-apim-post-activation-')], &properties.timestamp)[-1].properties.provisioningState" ` |
| 194 | + -o tsv 2>$null |
| 195 | + if (-not $postActivationState) { |
| 196 | + Write-Host "❌ Could not find source-apim-post-activation deployment in $(Protect-ResourceGroupName -Value $sourceRg)" |
| 197 | + exit 2 |
| 198 | + } |
| 199 | + if ($postActivationState -ne 'Succeeded') { |
| 200 | + Write-Host "❌ source-apim-post-activation deployment state is '$postActivationState' (expected 'Succeeded')" |
| 201 | + exit 2 |
| 202 | + } |
| 203 | + Write-Host " ✅ source-apim-post-activation deployment confirmed" |
| 204 | +} |
| 205 | + |
| 206 | +$state = [ordered]@{ |
| 207 | + sourceSubscriptionId = $sourceSubId |
| 208 | + sourceResourceGroup = $sourceRg |
| 209 | + sourceApimName = $sourceName |
| 210 | + targetSubscriptionId = $targetSubId |
| 211 | + targetResourceGroup = $targetRg |
| 212 | + targetApimName = $targetName |
| 213 | + skuName = $SkuName |
| 214 | + location = $Location |
| 215 | +} |
| 216 | + |
| 217 | +$stateDir = Split-Path -Parent $StateFile |
| 218 | +if (-not [string]::IsNullOrWhiteSpace($stateDir) -and -not (Test-Path $stateDir)) { |
| 219 | + New-Item -ItemType Directory -Path $stateDir -Force | Out-Null |
| 220 | +} |
| 221 | + |
| 222 | +$state | ConvertTo-Json -Depth 5 | Set-Content -Path $StateFile -Encoding utf8 |
| 223 | +Write-Host "✅ PHASE 1 complete — state saved to $StateFile" |
| 224 | +exit 0 |
0 commit comments