docs: project docs, scripts, deployment configs, and evidence
This commit is contained in:
249
scripts/ops/drill-config-isolation.ps1
Normal file
249
scripts/ops/drill-config-isolation.ps1
Normal file
@@ -0,0 +1,249 @@
|
||||
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
|
||||
Reference in New Issue
Block a user