param( [string]$SourceDb = '', [int]$ConfigPort = 18085, [int]$EnvPort = 18086, [string]$EvidenceDate = (Get-Date -Format 'yyyy-MM-dd') ) $ErrorActionPreference = 'Stop' $projectRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path if ([string]::IsNullOrWhiteSpace($SourceDb)) { $SourceDb = Join-Path $projectRoot 'data\user_management.db' } $evidenceRoot = Join-Path $projectRoot "docs\evidence\ops\$EvidenceDate\config-isolation" $goCacheRoot = Join-Path $projectRoot '.cache' $goBuildCache = Join-Path $goCacheRoot 'go-build' $goModCache = Join-Path $goCacheRoot 'gomod' $goPath = Join-Path $goCacheRoot 'gopath' $timestamp = Get-Date -Format 'yyyyMMdd-HHmmss' $drillRoot = Join-Path $evidenceRoot $timestamp $isolatedDb = Join-Path $drillRoot 'user_management.isolated.db' $isolatedConfig = Join-Path $drillRoot 'config.isolated.yaml' $serverExe = Join-Path $drillRoot 'server-config-isolation.exe' $configOnlyStdOut = Join-Path $drillRoot 'config-only.stdout.log' $configOnlyStdErr = Join-Path $drillRoot 'config-only.stderr.log' $envOverrideStdOut = Join-Path $drillRoot 'env-override.stdout.log' $envOverrideStdErr = Join-Path $drillRoot 'env-override.stderr.log' $capabilitiesConfigOnlyPath = Join-Path $drillRoot 'capabilities.config-only.json' $capabilitiesEnvOverridePath = Join-Path $drillRoot 'capabilities.env-override.json' $reportPath = Join-Path $drillRoot 'CONFIG_ENV_ISOLATION_DRILL.md' New-Item -ItemType Directory -Force $evidenceRoot, $drillRoot, $goBuildCache, $goModCache, $goPath | Out-Null function Test-UrlReady { param( [Parameter(Mandatory = $true)][string]$Url ) try { $response = Invoke-WebRequest $Url -UseBasicParsing -TimeoutSec 2 return $response.StatusCode -ge 200 -and $response.StatusCode -lt 500 } catch { return $false } } function Wait-UrlReady { param( [Parameter(Mandatory = $true)][string]$Url, [Parameter(Mandatory = $true)][string]$Label, [int]$RetryCount = 120, [int]$DelayMs = 500 ) for ($i = 0; $i -lt $RetryCount; $i++) { if (Test-UrlReady -Url $Url) { return } Start-Sleep -Milliseconds $DelayMs } throw "$Label did not become ready: $Url" } function Stop-TreeProcess { param( [Parameter(Mandatory = $false)]$Process ) if (-not $Process) { return } if (-not $Process.HasExited) { try { taskkill /PID $Process.Id /T /F *> $null } catch { Stop-Process -Id $Process.Id -Force -ErrorAction SilentlyContinue } } } function Build-IsolatedConfig { param( [Parameter(Mandatory = $true)][string]$TemplatePath, [Parameter(Mandatory = $true)][string]$OutputPath, [Parameter(Mandatory = $true)][string]$DbPath, [Parameter(Mandatory = $true)][int]$Port ) $content = Get-Content $TemplatePath -Raw $dbPathForYaml = ($DbPath -replace '\\', '/') $content = $content -replace '(?m)^ port: \d+$', " port: $Port" $content = [regex]::Replace( $content, '(?ms)(sqlite:\s*\r?\n\s*path:\s*).+?(\r?\n)', "`$1`"$dbPathForYaml`"`$2" ) Set-Content -Path $OutputPath -Value $content -Encoding UTF8 } if (-not (Test-Path $SourceDb)) { throw "source db not found: $SourceDb" } Copy-Item $SourceDb $isolatedDb -Force Build-IsolatedConfig ` -TemplatePath (Join-Path $projectRoot 'configs\config.yaml') ` -OutputPath $isolatedConfig ` -DbPath $isolatedDb ` -Port $ConfigPort Push-Location $projectRoot try { $env:GOCACHE = $goBuildCache $env:GOMODCACHE = $goModCache $env:GOPATH = $goPath & go build -o $serverExe .\cmd\server if ($LASTEXITCODE -ne 0) { throw 'build config isolation server failed' } } finally { Pop-Location Remove-Item Env:GOCACHE, Env:GOMODCACHE, Env:GOPATH -ErrorAction SilentlyContinue } $previousConfigPath = $env:UMS_CONFIG_PATH $previousServerPort = $env:UMS_SERVER_PORT $previousAllowedOrigins = $env:UMS_CORS_ALLOWED_ORIGINS $configOnlyProcess = $null $envOverrideProcess = $null try { $env:UMS_CONFIG_PATH = $isolatedConfig Remove-Item Env:UMS_SERVER_PORT -ErrorAction SilentlyContinue Remove-Item Env:UMS_CORS_ALLOWED_ORIGINS -ErrorAction SilentlyContinue Remove-Item $configOnlyStdOut, $configOnlyStdErr -Force -ErrorAction SilentlyContinue $configOnlyProcess = Start-Process ` -FilePath $serverExe ` -WorkingDirectory $projectRoot ` -PassThru ` -WindowStyle Hidden ` -RedirectStandardOutput $configOnlyStdOut ` -RedirectStandardError $configOnlyStdErr Wait-UrlReady -Url "http://127.0.0.1:$ConfigPort/health" -Label 'config-only health endpoint' Wait-UrlReady -Url "http://127.0.0.1:$ConfigPort/health/ready" -Label 'config-only readiness endpoint' $configOnlyCapabilities = Invoke-RestMethod "http://127.0.0.1:$ConfigPort/api/v1/auth/capabilities" -TimeoutSec 5 Set-Content -Path $capabilitiesConfigOnlyPath -Value (($configOnlyCapabilities.data | ConvertTo-Json -Depth 6) + [Environment]::NewLine) -Encoding UTF8 } finally { Stop-TreeProcess $configOnlyProcess } try { $env:UMS_CONFIG_PATH = $isolatedConfig $env:UMS_SERVER_PORT = "$EnvPort" $env:UMS_CORS_ALLOWED_ORIGINS = 'https://admin.example.com' Remove-Item $envOverrideStdOut, $envOverrideStdErr -Force -ErrorAction SilentlyContinue $envOverrideProcess = Start-Process ` -FilePath $serverExe ` -WorkingDirectory $projectRoot ` -PassThru ` -WindowStyle Hidden ` -RedirectStandardOutput $envOverrideStdOut ` -RedirectStandardError $envOverrideStdErr Wait-UrlReady -Url "http://127.0.0.1:$EnvPort/health" -Label 'env-override health endpoint' Wait-UrlReady -Url "http://127.0.0.1:$EnvPort/health/ready" -Label 'env-override readiness endpoint' $envOverrideCapabilities = Invoke-RestMethod "http://127.0.0.1:$EnvPort/api/v1/auth/capabilities" -TimeoutSec 5 Set-Content -Path $capabilitiesEnvOverridePath -Value (($envOverrideCapabilities.data | ConvertTo-Json -Depth 6) + [Environment]::NewLine) -Encoding UTF8 $corsAllowed = Invoke-WebRequest ` -Uri "http://127.0.0.1:$EnvPort/health" ` -Headers @{ Origin = 'https://admin.example.com' } ` -UseBasicParsing ` -TimeoutSec 5 $corsRejected = Invoke-WebRequest ` -Uri "http://127.0.0.1:$EnvPort/health" ` -Headers @{ Origin = 'http://localhost:3000' } ` -UseBasicParsing ` -TimeoutSec 5 } finally { Stop-TreeProcess $envOverrideProcess if ([string]::IsNullOrWhiteSpace($previousConfigPath)) { Remove-Item Env:UMS_CONFIG_PATH -ErrorAction SilentlyContinue } else { $env:UMS_CONFIG_PATH = $previousConfigPath } if ([string]::IsNullOrWhiteSpace($previousServerPort)) { Remove-Item Env:UMS_SERVER_PORT -ErrorAction SilentlyContinue } else { $env:UMS_SERVER_PORT = $previousServerPort } if ([string]::IsNullOrWhiteSpace($previousAllowedOrigins)) { Remove-Item Env:UMS_CORS_ALLOWED_ORIGINS -ErrorAction SilentlyContinue } else { $env:UMS_CORS_ALLOWED_ORIGINS = $previousAllowedOrigins } } $corsAllowedOrigin = $corsAllowed.Headers['Access-Control-Allow-Origin'] $corsRejectedOrigin = $corsRejected.Headers['Access-Control-Allow-Origin'] if ($corsAllowedOrigin -ne 'https://admin.example.com') { throw "expected env override CORS allow origin to be https://admin.example.com, got: $corsAllowedOrigin" } if (-not [string]::IsNullOrWhiteSpace($corsRejectedOrigin)) { throw "expected localhost origin to be excluded by env override, got: $corsRejectedOrigin" } $reportLines = @( '# Config And Env Isolation Drill', '', "- Generated at: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss zzz')", "- Source DB: $SourceDb", "- Isolated DB: $isolatedDb", "- Isolated config: $isolatedConfig", '', '## Verification Results', '', "- Base config default port: 8080", "- UMS_CONFIG_PATH isolated port: $ConfigPort", "- UMS_SERVER_PORT override port: $EnvPort", "- UMS_CORS_ALLOWED_ORIGINS override accepted origin: $corsAllowedOrigin", "- UMS_CORS_ALLOWED_ORIGINS override excluded origin: $(if ([string]::IsNullOrWhiteSpace($corsRejectedOrigin)) { 'none' } else { $corsRejectedOrigin })", "- auth capabilities with config-only override: $(($configOnlyCapabilities.data | ConvertTo-Json -Compress))", "- auth capabilities with env override: $(($envOverrideCapabilities.data | ConvertTo-Json -Compress))", '', '## Evidence Files', '', "- $(Split-Path $configOnlyStdOut -Leaf)", "- $(Split-Path $configOnlyStdErr -Leaf)", "- $(Split-Path $envOverrideStdOut -Leaf)", "- $(Split-Path $envOverrideStdErr -Leaf)", "- $(Split-Path $capabilitiesConfigOnlyPath -Leaf)", "- $(Split-Path $capabilitiesEnvOverridePath -Leaf)", "- $(Split-Path $isolatedConfig -Leaf)", '' ) Set-Content -Path $reportPath -Value ($reportLines -join [Environment]::NewLine) -Encoding UTF8 Get-Content $reportPath