11# requires -Version 7.0
2+ <#
3+ . SYNOPSIS
4+ Phase 2 orchestrator β Extract β Publish β Compare round-trip.
5+
6+ . DESCRIPTION
7+ Delegates to the three sub-scripts in sequence:
8+ run-roundtrip-phase2a-extract.ps1 β extract artifacts from source APIM
9+ run-roundtrip-phase2b-publish.ps1 β generate overrides and publish to target APIM
10+ run-roundtrip-phase2c-compare.ps1 β compare source vs target via ARM REST API
11+
12+ Each sub-script can also be invoked independently for targeted re-runs.
13+
14+ . EXAMPLE
15+ .\run-roundtrip-phase2-roundtrip.ps1 -StateFile ./roundtrip-state.json
16+
17+ . EXAMPLE
18+ .\run-roundtrip-phase2-roundtrip.ps1 -StateFile ./roundtrip-state.json -LogLevel Debug -ExtractOutputDir ./my-artifacts
19+ #>
220
321[CmdletBinding ()]
422param (
@@ -12,206 +30,47 @@ param(
1230)
1331
1432$ErrorActionPreference = ' Stop'
15- $VerbosePreference = if ($LogLevel -in @ (' Verbose' , ' Debug' )) { ' Continue' } else { ' SilentlyContinue' }
16- $DebugPreference = if ($LogLevel -eq ' Debug' ) { ' Continue' } else { ' SilentlyContinue' }
17-
18- $maskingModule = Join-Path $PSScriptRoot ' MaskingHelpers.psm1'
19- Import-Module $maskingModule - Force
2033
21- $compareScript = Join-Path $PSScriptRoot ' Compare-ApimInstance .ps1'
22- $validateScript = Join-Path $PSScriptRoot ' Test-ExtractedArtifact .ps1'
23- $manifestFile = Join-Path $PSScriptRoot ' expected-structure.json '
34+ $extractScript = Join-Path $PSScriptRoot ' run-roundtrip-phase2a-extract .ps1'
35+ $publishScript = Join-Path $PSScriptRoot ' run-roundtrip-phase2b-publish .ps1'
36+ $compareScript = Join-Path $PSScriptRoot ' run-roundtrip-phase2c-compare.ps1 '
2437
25- foreach ($requiredFile in @ ($maskingModule , $compareScript , $validateScript , $manifestFile , $StateFile )) {
38+ foreach ($requiredFile in @ ($extractScript , $publishScript , $compareScript , $StateFile )) {
2639 if (-not (Test-Path $requiredFile )) {
2740 Write-Error " Required file not found: $requiredFile "
2841 exit 2
2942 }
3043}
3144
32- function Get-ApiopsLogLevel ([string ]$ScriptLogLevel ) {
33- switch ($ScriptLogLevel ) {
34- ' Info' { return ' info' }
35- ' Verbose' { return ' warn' }
36- ' Debug' { return ' debug' }
37- default { return ' info' }
38- }
39- }
40-
41- function Get-ApiopsAuthArgs {
42- # In CI, we explicitly pass client/tenant to apiops so DefaultAzureCredential
43- # can use the intended federated identity after long-running deploy phases.
44- # If env vars are unset (local runs), apiops falls back to default credential chain.
45- $authArgs = @ ()
46-
47- if (-not [string ]::IsNullOrWhiteSpace($env: AZURE_CLIENT_ID )) {
48- $authArgs += @ (' --client-id' , $env: AZURE_CLIENT_ID )
49- }
50-
51- if (-not [string ]::IsNullOrWhiteSpace($env: AZURE_TENANT_ID )) {
52- $authArgs += @ (' --tenant-id' , $env: AZURE_TENANT_ID )
53- }
54-
55- return $authArgs
56- }
57-
58- $state = Get-Content - Path $StateFile - Raw | ConvertFrom-Json
59- $sourceSubId = $state.sourceSubscriptionId
60- $sourceRg = $state.sourceResourceGroup
61- $sourceName = $state.sourceApimName
62- $targetSubId = $state.targetSubscriptionId
63- $targetRg = $state.targetResourceGroup
64- $targetName = $state.targetApimName
65- $skuName = $state.skuName
66-
6745$exitCode = 0
68- $apiopsLogLevel = Get-ApiopsLogLevel - ScriptLogLevel $LogLevel
69- $apiopsAuthArgs = Get-ApiopsAuthArgs
70-
71- Write-Host " π₯ PHASE 2 β Extract from source APIM"
72- if (Test-Path $ExtractOutputDir ) {
73- Remove-Item - Path $ExtractOutputDir - Recurse - Force
74- Write-Host " Cleaned previous extract output"
75- }
76-
77- $extractArgs = @ (
78- ' extract' ,
79- ' --subscription-id' , $sourceSubId ,
80- ' --resource-group' , $sourceRg ,
81- ' --service-name' , $sourceName ,
82- ' --output' , $ExtractOutputDir ,
83- ' --log-level' , $apiopsLogLevel
84- ) + $apiopsAuthArgs
85-
86- $extractExitCode = Invoke-MaskedApiopsCommand - Replacements @ {
87- $sourceSubId = Protect-SubscriptionId - Value $sourceSubId
88- $sourceRg = Protect-ResourceGroupName - Value $sourceRg
89- $sourceName = Protect-ApimName - Value $sourceName
90- } - Arguments $extractArgs
91-
92- if ($extractExitCode -ne 0 ) {
93- Write-Host " β Extract failed (exit code $extractExitCode )"
94- exit 2
95- }
96-
97- $extractedFiles = Get-ChildItem - Path $ExtractOutputDir - Recurse - File - ErrorAction SilentlyContinue
98- if (-not $extractedFiles -or $extractedFiles.Count -eq 0 ) {
99- Write-Host " β Extract produced no files in $ExtractOutputDir "
100- exit 2
101- }
102-
103- Write-Host " οΏ½οΏ½ PHASE 2.1 β Validate extracted artifact structure"
104- $validateArgs = @ {
105- ExtractedDir = $ExtractOutputDir
106- ManifestFile = $manifestFile
107- SkuName = $skuName
108- }
109- switch ($LogLevel ) {
110- ' Verbose' { $validateArgs.Verbose = $true }
111- ' Debug' { $validateArgs.Debug = $true }
112- }
113- & $validateScript @validateArgs
114- $validateExitCode = $LASTEXITCODE
115- if ($validateExitCode -ne 0 ) {
116- Write-Host " β Artifact validation failed (exit code $validateExitCode )"
117- $exitCode = if ($validateExitCode -eq 2 ) { 2 } else { 1 }
118- Write-Host " β οΈ Continuing with round-trip despite validation failures..."
119- }
120-
121- Write-Host " π§ PHASE 2.5 β Generate override config for target environment"
122- $targetKvUri = az keyvault list -- resource- group $targetRg -- query " [0].properties.vaultUri" - o tsv
123- $targetAiResourceId = az monitor app- insights component list -- resource- group $targetRg -- query " [0].id" - o tsv
124- $targetAiKey = az monitor app- insights component list -- resource- group $targetRg -- query " [0].instrumentationKey" - o tsv
125- $targetEhNs = az eventhubs namespace list -- resource- group $targetRg -- query " [0].name" - o tsv
126-
127- if (-not $targetKvUri ) {
128- Write-Host " β Could not resolve target Key Vault URI in $ ( Protect-ResourceGroupName - Value $targetRg ) "
129- exit 2
130- }
131- if (-not $targetAiResourceId -or -not $targetAiKey ) {
132- Write-Host " β Could not resolve target Application Insights details in $ ( Protect-ResourceGroupName - Value $targetRg ) "
133- exit 2
134- }
135- if (-not $targetEhNs ) {
136- Write-Host " β Could not resolve target Event Hub namespace in $ ( Protect-ResourceGroupName - Value $targetRg ) "
137- exit 2
138- }
139-
140- $targetEhConnStr = az eventhubs namespace authorization- rule keys list `
141- -- resource- group $targetRg `
142- -- namespace- name $targetEhNs `
143- -- name ' tgt-eh-send' `
144- -- query ' primaryConnectionString' - o tsv
145-
146- if (-not $targetEhConnStr ) {
147- Write-Host " β οΈ Could not get Event Hub connection string β EH logger override will be empty"
148- }
149-
150- $targetEhName = ' tgt-eh-logs'
151- $overrideFile = [System.IO.Path ]::GetFullPath((Join-Path $ExtractOutputDir ' .overrides.yaml' ))
152- $overrideYaml = @"
153- namedValues:
154- src-nv-keyvault:
155- keyVault:
156- secretIdentifier: "${targetKvUri} secrets/tgt-secret-value"
157-
158- loggers:
159- src-logger-appinsights:
160- resourceId: "$targetAiResourceId "
161- credentials:
162- instrumentationKey: "$targetAiKey "
163- src-logger-eventhub:
164- credentials:
165- name: "$targetEhName "
166- connectionString: "$targetEhConnStr "
167- "@
168-
169- $overrideYaml | Set-Content - Path $overrideFile - Encoding utf8
170-
171- Write-Host " π€ PHASE 3 β Publish to target APIM"
172- $publishExitCode = Invoke-MaskedApiopsCommand - Replacements @ {
173- $targetSubId = Protect-SubscriptionId - Value $targetSubId
174- $targetRg = Protect-ResourceGroupName - Value $targetRg
175- $targetName = Protect-ApimName - Value $targetName
176- } - Arguments @ (
177- ' publish' ,
178- ' --subscription-id' , $targetSubId ,
179- ' --resource-group' , $targetRg ,
180- ' --service-name' , $targetName ,
181- ' --source' , $ExtractOutputDir ,
182- ' --overrides' , $overrideFile ,
183- ' --log-level' , $apiopsLogLevel
184- ) + $apiopsAuthArgs
18546
47+ # ββ Phase 2a: Extract ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
48+ Write-Host " π₯ PHASE 2a β Extract artifacts from source APIM"
49+ & $extractScript - StateFile $StateFile - LogLevel $LogLevel - ExtractOutputDir $ExtractOutputDir
50+ $extractExitCode = $LASTEXITCODE
51+ if ($extractExitCode -ge 2 ) {
52+ exit $extractExitCode
53+ } elseif ($extractExitCode -ne 0 ) {
54+ $exitCode = $extractExitCode
55+ Write-Host " β οΈ Continuing with round-trip despite extract/validation failures..."
56+ }
57+
58+ # ββ Phase 2b: Publish ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
59+ Write-Host " π€ PHASE 2b β Publish artifacts to target APIM"
60+ & $publishScript - StateFile $StateFile - LogLevel $LogLevel - ExtractOutputDir $ExtractOutputDir
61+ $publishExitCode = $LASTEXITCODE
18662if ($publishExitCode -ne 0 ) {
187- Write-Host " β Publish failed (exit code $publishExitCode )"
188- exit 2
189- }
190-
191- Write-Host " π PHASE 4 β Compare source and target APIM instances"
192- $compareArgs = @ {
193- SourceSubscriptionId = $sourceSubId
194- SourceResourceGroup = $sourceRg
195- SourceApimName = $sourceName
196- TargetSubscriptionId = $targetSubId
197- TargetResourceGroup = $targetRg
198- TargetApimName = $targetName
199- }
200- switch ($LogLevel ) {
201- ' Verbose' { $compareArgs.Verbose = $true }
202- ' Debug' { $compareArgs.Debug = $true }
63+ exit $publishExitCode
20364}
204- & $compareScript @compareArgs
205- $verifyExitCode = $LASTEXITCODE
20665
207- if ($verifyExitCode -eq 1 ) {
208- Write-Host " β Verification found differences"
66+ # ββ Phase 2c: Compare ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
67+ Write-Host " π PHASE 2c β Compare source and target APIM instances"
68+ & $compareScript - StateFile $StateFile - LogLevel $LogLevel
69+ $compareExitCode = $LASTEXITCODE
70+ if ($compareExitCode -eq 1 ) {
20971 if ($exitCode -eq 0 ) { $exitCode = 1 }
210- } elseif ($verifyExitCode -ge 2 ) {
211- Write-Host " β Verification encountered an error (exit code $verifyExitCode )"
72+ } elseif ($compareExitCode -ge 2 ) {
21273 $exitCode = 2
213- } else {
214- Write-Host " β
Verification complete β instances match"
21574}
21675
21776exit $exitCode
0 commit comments