Skip to content

Commit d914ff4

Browse files
authored
Split phase 2 into three standalone scripts (extract, publish, compare)
1 parent f5ee5d6 commit d914ff4

5 files changed

Lines changed: 456 additions & 190 deletions

File tree

β€Žtests/integration/all-resource-types/README.mdβ€Ž

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -109,12 +109,46 @@ Requires an `integration-test` environment with secrets:
109109
- `AZURE_CLIENT_ID`, `AZURE_TENANT_ID`, `AZURE_SUBSCRIPTION_ID` (OIDC)
110110
- `APIM_PUBLISHER_EMAIL`
111111

112+
### Phase 2 Sub-Scripts
113+
114+
Phase 2 is split into three independent scripts that can be called separately for targeted re-runs, without re-executing the full round-trip:
115+
116+
#### Phase 2a β€” Extract
117+
118+
Extracts artifacts from the source APIM and validates the extracted structure.
119+
120+
```powershell
121+
.\run-roundtrip-phase2a-extract.ps1 -StateFile ./roundtrip-state.json
122+
```
123+
124+
#### Phase 2b β€” Publish
125+
126+
Generates target environment overrides (Key Vault, App Insights, Event Hub) and publishes the extracted artifacts to the target APIM.
127+
128+
```powershell
129+
.\run-roundtrip-phase2b-publish.ps1 -StateFile ./roundtrip-state.json
130+
```
131+
132+
Requires `ExtractOutputDir` (default: `./extracted-artifacts`) to already be populated by the extract step.
133+
134+
#### Phase 2c β€” Compare
135+
136+
Compares source and target APIM instances via ARM REST API with deep property normalization.
137+
138+
```powershell
139+
.\run-roundtrip-phase2c-compare.ps1 -StateFile ./roundtrip-state.json
140+
```
141+
142+
Exit codes: `0` = match, `1` = differences found, `2` = error.
143+
144+
All three scripts accept `-StateFile` (mandatory), `-LogLevel` (Info/Verbose/Debug), and `-ExtractOutputDir` (2a/2b only).
145+
112146
### Comparison Script
113147

114-
`compare-apim-instances.ps1` can also be run standalone to diff any two APIM instances:
148+
`Compare-ApimInstance.ps1` can also be run standalone to diff any two APIM instances directly without a state file:
115149

116150
```powershell
117-
.\compare-apim-instances.ps1 `
151+
.\Compare-ApimInstance.ps1 `
118152
-SourceSubscriptionId "..." -SourceResourceGroup rg-src -SourceApimName apim-src `
119153
-TargetSubscriptionId "..." -TargetResourceGroup rg-tgt -TargetApimName apim-tgt
120154
```
@@ -127,10 +161,15 @@ Exit codes: `0` = match, `1` = differences found, `2` = error.
127161
|------|---------|
128162
| `source-apim.bicep` | Source APIM with all 33 resource types |
129163
| `target-apim.bicep` | Blank target APIM + supporting infra |
130-
| `deploy-source.ps1` | Deploy/destroy the source instance |
131164
| `run-roundtrip-test.ps1` | Master orchestrator for the full test |
132-
| `compare-apim-instances.ps1` | ARM REST comparison script |
133-
| `validate-extracted-artifacts.ps1` | Validate extracted artifact structure |
165+
| `run-roundtrip-phase1-deploy.ps1` | Phase 1: deploy source + target APIM instances |
166+
| `run-roundtrip-phase2-roundtrip.ps1` | Phase 2 orchestrator: calls 2a β†’ 2b β†’ 2c |
167+
| `run-roundtrip-phase2a-extract.ps1` | Phase 2a: extract artifacts from source APIM |
168+
| `run-roundtrip-phase2b-publish.ps1` | Phase 2b: generate overrides + publish to target |
169+
| `run-roundtrip-phase2c-compare.ps1` | Phase 2c: compare source vs target via ARM |
170+
| `run-roundtrip-phase3-teardown.ps1` | Phase 3: tear down resource groups |
171+
| `Compare-ApimInstance.ps1` | ARM REST comparison script (standalone) |
172+
| `Test-ExtractedArtifact.ps1` | Validate extracted artifact structure |
134173
| `expected-structure.json` | Manifest of expected extracted files |
135174

136175
## Cost
Lines changed: 44 additions & 185 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,22 @@
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()]
422
param(
@@ -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
18662
if ($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

21776
exit $exitCode

0 commit comments

Comments
Β (0)