chore: establish local platform baseline
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
$ErrorActionPreference='Stop'
|
||||
$PSNativeCommandUseErrorActionPreference=$true
|
||||
$root=Split-Path -Parent $PSScriptRoot
|
||||
Set-Location -LiteralPath $root
|
||||
$api='http://localhost:58080'
|
||||
$tokenUrl='http://localhost:58180/realms/voice-local/protocol/openid-connect/token'
|
||||
$tenantPublicId='10000000-0000-0000-0000-000000000010'
|
||||
$routeId=$null;$silenceId=$null;$incidentIds=@();$outboxIds=@()
|
||||
|
||||
function Token([string]$name){(Invoke-RestMethod -Method Post -Uri $tokenUrl -ContentType 'application/x-www-form-urlencoded' -Body @{grant_type='password';client_id='voice-platform-web';username=$name;password='LocalOnly123!';scope='openid'}).access_token}
|
||||
function Expected([scriptblock]$call,[int]$status){try{&$call|Out-Null;throw "Expected HTTP $status"}catch{$actual=[int]$_.Exception.Response.StatusCode;if($actual-ne$status){throw}}}
|
||||
function Scalar([string]$sql){(docker compose exec -T postgres psql -U voice -d voice_platform -q -t -A -c $sql|Out-String).Trim()}
|
||||
function InsertDeadOutbox(){
|
||||
$id=Scalar "INSERT INTO outbox_event(tenant_id,aggregate_type,aggregate_id,event_type,payload,status,attempt_count,last_error_code,dead_at,payload_hash) SELECT id,'alert_smoke',gen_random_uuid(),'alert.smoke.dead','{}'::jsonb,'dead',12,'SMOKE_DEAD',now(),repeat('0',64) FROM tenant WHERE public_id='$tenantPublicId'::uuid RETURNING id::text"
|
||||
if($id-notmatch'^[0-9a-fA-F-]{36}$'){throw "Unable to create dead outbox signal: $id"}
|
||||
$script:outboxIds+=$id
|
||||
return $id
|
||||
}
|
||||
function WaitAlert([string]$token,[string]$expectedStatus,[int]$seconds=20){
|
||||
for($i=0;$i-lt$seconds;$i++){
|
||||
$result=Invoke-RestMethod -Uri "$api/api/v1/ops/alerts" -Headers @{Authorization="Bearer $token";'X-Tenant-Id'=$tenantPublicId}
|
||||
$item=@($result.data.items|Where-Object{$_.fingerprint-eq'outbox-dead' -and (!$expectedStatus-or$_.status-eq$expectedStatus) -and $_.receiver_group-eq'platform-oncall'})|Select-Object -First 1
|
||||
if($item){return $item}
|
||||
Start-Sleep -Seconds 1
|
||||
}
|
||||
throw "Alert outbox-dead did not reach status $expectedStatus"
|
||||
}
|
||||
function Detail([string]$token,[string]$id){(Invoke-RestMethod -Uri "$api/api/v1/ops/alerts/$id" -Headers @{Authorization="Bearer $token";'X-Tenant-Id'=$tenantPublicId}).data}
|
||||
function Transition([string]$token,[object]$item,[string]$action,[string]$note){
|
||||
$body=@{note=$note}|ConvertTo-Json -Compress
|
||||
return (Invoke-RestMethod -Method Post -Uri "$api/api/v1/ops/alerts/$($item.id)/$action" -Headers @{Authorization="Bearer $token";'X-Tenant-Id'=$tenantPublicId;'If-Match'="`"v$($item.row_version)`""} -ContentType 'application/json' -Body $body).data
|
||||
}
|
||||
|
||||
try{
|
||||
$owner=Token 'owner';$outsider=Token 'outsider'
|
||||
Expected {Invoke-WebRequest -UseBasicParsing -Uri "$api/api/v1/ops/alerts" -Headers @{Authorization="Bearer $outsider";'X-Tenant-Id'=$tenantPublicId}} 403
|
||||
|
||||
$routeKey='alert-route-'+[Guid]::NewGuid().ToString('N')
|
||||
$routeBody=@{category='TECHNICAL';severity='HIGH';receiver_group='platform-oncall';response_minutes=1;escalation_after_minutes=1;escalation_receiver_group='incident-command';runbook_ref='runbook://platform/outbox-dead-letter';change_summary='告警闭环冒烟路由'}|ConvertTo-Json -Compress
|
||||
$created=Invoke-WebRequest -UseBasicParsing -Method Post -Uri "$api/api/v1/ops/alert-routing-policies" -Headers @{Authorization="Bearer $owner";'X-Tenant-Id'=$tenantPublicId;'Idempotency-Key'=$routeKey} -ContentType 'application/json' -Body $routeBody
|
||||
$route=($created.Content|ConvertFrom-Json).data;$routeId=[string]$route.id
|
||||
if($route.status-ne'ACTIVE'-or$route.receiver_group-ne'platform-oncall'-or$route.runbook_ref-ne'runbook://platform/outbox-dead-letter'){throw 'Active routing policy was not created'}
|
||||
$replay=Invoke-WebRequest -UseBasicParsing -Method Post -Uri "$api/api/v1/ops/alert-routing-policies" -Headers @{Authorization="Bearer $owner";'X-Tenant-Id'=$tenantPublicId;'Idempotency-Key'=$routeKey} -ContentType 'application/json' -Body $routeBody
|
||||
if($replay.StatusCode-ne200-or[string]$replay.Headers.'Idempotency-Replayed'-ne'true'-or($replay.Content|ConvertFrom-Json).data.id-ne$routeId){throw 'Alert route idempotency replay failed'}
|
||||
|
||||
$firstOutbox=InsertDeadOutbox
|
||||
$incident=WaitAlert $owner 'OPEN'
|
||||
$incidentIds+=[string]$incident.id
|
||||
if($incident.receiver_group-ne'platform-oncall'-or$incident.runbook_ref-ne'runbook://platform/outbox-dead-letter'){throw "Detected incident did not freeze active route context: $($incident|ConvertTo-Json -Compress -Depth 6)"}
|
||||
$detectedReceiver=Scalar "SELECT payload->>'receiver_group' FROM outbox_event WHERE event_type='alert.webhook_requested' AND aggregate_id='$($incident.id)'::uuid AND payload->>'action'='DETECTED'"
|
||||
if($detectedReceiver-ne'platform-oncall'){throw "Detected notification did not target the active receiver group: $detectedReceiver"}
|
||||
Scalar "DELETE FROM outbox_event WHERE id='$firstOutbox'::uuid"|Out-Null
|
||||
Scalar "UPDATE alert_incident SET first_detected_at=now()-interval '2 minutes' WHERE public_id='$($incident.id)'::uuid RETURNING row_version"|Out-Null
|
||||
for($i=0;$i-lt20;$i++){Start-Sleep -Seconds 1;$incident=Detail $owner $incident.id;if($incident.escalated_at){break}}
|
||||
if(-not$incident.escalated_at-or-not(@($incident.actions.action_type)-contains'ESCALATED')){throw 'Incident escalation action was not recorded'}
|
||||
$escalatedReceiver=Scalar "SELECT payload->>'receiver_group' FROM outbox_event WHERE event_type='alert.webhook_requested' AND aggregate_id='$($incident.id)'::uuid AND payload->>'action'='ESCALATED'"
|
||||
if($escalatedReceiver-ne'incident-command'){throw "Escalated notification did not target the escalation receiver group: $escalatedReceiver"}
|
||||
$beforeAcknowledge=$incident.row_version
|
||||
$incident=Transition $owner $incident 'acknowledge' '值班人员已确认并接手死信处置'
|
||||
if($incident.status-ne'ACKNOWLEDGED'-or-not$incident.current_owner){throw 'Incident acknowledgement did not assign an owner'}
|
||||
$staleBody=@{note='使用过期版本重复确认'}|ConvertTo-Json -Compress
|
||||
Expected {Invoke-WebRequest -UseBasicParsing -Method Post -Uri "$api/api/v1/ops/alerts/$($incident.id)/mitigate" -Headers @{Authorization="Bearer $owner";'X-Tenant-Id'=$tenantPublicId;'If-Match'="`"v$beforeAcknowledge`""} -ContentType 'application/json' -Body $staleBody} 412
|
||||
$incident=Transition $owner $incident 'mitigate' '死信根因已修复且待办已受控重放'
|
||||
if($incident.status-ne'MITIGATED'){throw 'Incident mitigation failed'}
|
||||
$incident=Transition $owner $incident 'resolve' '监测指标恢复,复核无残留死信后关闭'
|
||||
if($incident.status-ne'RESOLVED'){throw 'Incident resolution failed'}
|
||||
$actionTypes=@($incident.actions.action_type)
|
||||
foreach($expectedAction in @('DETECTED','ESCALATED','ACKNOWLEDGED','MITIGATED','RESOLVED')){if($actionTypes-notcontains$expectedAction){throw "Missing action $expectedAction in incident timeline"}}
|
||||
|
||||
$silenceKey='alert-silence-'+[Guid]::NewGuid().ToString('N')
|
||||
$now=[DateTime]::UtcNow
|
||||
$silenceBody=@{rule_code='OUTBOX_DEAD_LETTER';matcher=@{};reason='计划内死信重放演练';starts_at=$now.AddMinutes(-1).ToString('o');ends_at=$now.AddHours(1).ToString('o')}|ConvertTo-Json -Compress
|
||||
$silenceResponse=Invoke-WebRequest -UseBasicParsing -Method Post -Uri "$api/api/v1/ops/alert-silences" -Headers @{Authorization="Bearer $owner";'X-Tenant-Id'=$tenantPublicId;'Idempotency-Key'=$silenceKey} -ContentType 'application/json' -Body $silenceBody
|
||||
$silence=($silenceResponse.Content|ConvertFrom-Json).data;$silenceId=[string]$silence.id
|
||||
$secondOutbox=InsertDeadOutbox
|
||||
$silencedIncident=WaitAlert $owner 'SILENCED'
|
||||
$incidentIds+=[string]$silencedIncident.id
|
||||
$silencedIncident=Detail $owner $silencedIncident.id
|
||||
if(-not(@($silencedIncident.actions.action_type)-contains'SILENCED')){throw 'Silenced detection did not preserve an explicit silence action'}
|
||||
$revokeBody=@{reason='演练结束,立即恢复处置队列'}|ConvertTo-Json -Compress
|
||||
$revoked=(Invoke-RestMethod -Method Post -Uri "$api/api/v1/ops/alert-silences/$silenceId/revoke" -Headers @{Authorization="Bearer $owner";'X-Tenant-Id'=$tenantPublicId;'If-Match'="`"v$($silence.row_version)`""} -ContentType 'application/json' -Body $revokeBody).data
|
||||
if(-not$revoked.revoked_at){throw 'Silence revocation was not recorded'}
|
||||
$reopened=WaitAlert $owner 'OPEN'
|
||||
$reopened=Detail $owner $reopened.id
|
||||
if(-not(@($reopened.actions.action_type)-contains'REOPENED')){throw 'Incident did not reopen after silence revocation'}
|
||||
Scalar "DELETE FROM outbox_event WHERE id='$secondOutbox'::uuid"|Out-Null
|
||||
$reopened=Transition $owner $reopened 'resolve' '静默撤销后复核并关闭测试事件'
|
||||
|
||||
Write-Host "Alert smoke passed: tenant isolation, versioned route, automatic detection/deduplication, escalation, ETag conflict, audited acknowledge-mitigate-resolve, silence and reopen: $($incident.id), $($reopened.id)"
|
||||
}finally{
|
||||
foreach($id in $outboxIds){if($id-match'^[0-9a-fA-F-]{36}$'){Scalar "DELETE FROM outbox_event WHERE id='$id'::uuid"|Out-Null}}
|
||||
$incidentIds=@($incidentIds|Where-Object{$_-match'^[0-9a-fA-F-]{36}$'}|Select-Object -Unique)
|
||||
if($incidentIds.Count){$ids=($incidentIds|ForEach-Object{"'$_'::uuid"})-join',';$sql="BEGIN; DELETE FROM outbox_event WHERE event_type='alert.webhook_requested' AND aggregate_id IN($ids); DELETE FROM audit_event WHERE resource_id IN(SELECT id FROM alert_incident WHERE public_id IN($ids)); DELETE FROM alert_incident WHERE public_id IN($ids); COMMIT;";$sql|docker compose exec -T postgres psql -U voice -d voice_platform -v ON_ERROR_STOP=1 -q|Out-Null}
|
||||
if($silenceId-match'^[0-9a-fA-F-]{36}$'){$sql="BEGIN; DELETE FROM audit_event WHERE resource_id IN(SELECT id FROM alert_silence WHERE public_id='$silenceId'::uuid); DELETE FROM idempotency_record WHERE resource_id IN(SELECT id FROM alert_silence WHERE public_id='$silenceId'::uuid); DELETE FROM alert_silence WHERE public_id='$silenceId'::uuid; COMMIT;";$sql|docker compose exec -T postgres psql -U voice -d voice_platform -v ON_ERROR_STOP=1 -q|Out-Null}
|
||||
if($routeId-match'^[0-9a-fA-F-]{36}$'){$sql="BEGIN; DELETE FROM audit_event WHERE resource_id IN(SELECT id FROM alert_routing_policy WHERE public_id='$routeId'::uuid); DELETE FROM idempotency_record WHERE resource_id IN(SELECT id FROM alert_routing_policy WHERE public_id='$routeId'::uuid); DELETE FROM alert_routing_policy WHERE public_id='$routeId'::uuid; COMMIT;";$sql|docker compose exec -T postgres psql -U voice -d voice_platform -v ON_ERROR_STOP=1 -q|Out-Null}
|
||||
}
|
||||
Reference in New Issue
Block a user