166 lines
9.6 KiB
PowerShell
166 lines
9.6 KiB
PowerShell
param(
|
|
[string]$ConfigPath = $env:ONEC_REPOSITORY_RUNNER_BASES_JSON_FILE,
|
|
[string]$ListenPrefix = $(if ($env:ONEC_REPOSITORY_RUNNER_PREFIX) { $env:ONEC_REPOSITORY_RUNNER_PREFIX } else { 'http://+:8121/' })
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
if (-not $ConfigPath -or -not (Test-Path -LiteralPath $ConfigPath -PathType Leaf)) { throw 'Repository runner config file is required.' }
|
|
|
|
function Send-Json($Context, [int]$Status, $Value) {
|
|
$bytes = [Text.Encoding]::UTF8.GetBytes(($Value | ConvertTo-Json -Depth 12 -Compress))
|
|
$Context.Response.StatusCode = $Status
|
|
$Context.Response.ContentType = 'application/json; charset=utf-8'
|
|
$Context.Response.ContentLength64 = $bytes.Length
|
|
$Context.Response.OutputStream.Write($bytes, 0, $bytes.Length)
|
|
$Context.Response.Close()
|
|
}
|
|
|
|
function Quote-Argument([string]$Value) {
|
|
if ($Value -notmatch '[\s"]') { return $Value }
|
|
return '"' + ($Value -replace '(\\*)"', '$1$1\"' -replace '(\\+)$', '$1$1') + '"'
|
|
}
|
|
|
|
function Get-Secret($Config, [string]$Name) {
|
|
$envName = [string]$Config.("${Name}_env")
|
|
if (-not $envName) { return '' }
|
|
$value = [Environment]::GetEnvironmentVariable($envName, 'Process')
|
|
if (-not $value) { $value = [Environment]::GetEnvironmentVariable($envName, 'Machine') }
|
|
if ($value) { return $value }
|
|
return ''
|
|
}
|
|
|
|
function Invoke-RepositoryAction($Config, $Payload) {
|
|
$temp = Join-Path $env:TEMP ('onec-repository-' + [guid]::NewGuid().ToString('N'))
|
|
New-Item -ItemType Directory -Path $temp | Out-Null
|
|
try {
|
|
$log = Join-Path $temp 'designer.log'
|
|
$args = @('DESIGNER')
|
|
if ($Config.infobase.file) { $args += @('/F', [string]$Config.infobase.file) }
|
|
elseif ($Config.infobase.server) { $args += @('/S', [string]$Config.infobase.server) }
|
|
elseif ($Config.infobase.name) { $args += @('/IBName', [string]$Config.infobase.name) }
|
|
else { throw 'Runner infobase selector is missing.' }
|
|
if ($Config.infobase_user) {
|
|
$args += @('/N', [string]$Config.infobase_user)
|
|
$password = Get-Secret $Config 'infobase_password'
|
|
if ($password) { $args += @('/P', $password) }
|
|
}
|
|
$args += @('/DisableStartupMessages', '/DisableStartupDialogs', '/Out', $log, '/ConfigurationRepositoryF', [string]$Config.endpoint)
|
|
if ($Config.repository_user) {
|
|
$args += @('/ConfigurationRepositoryN', [string]$Config.repository_user)
|
|
$password = Get-Secret $Config 'repository_password'
|
|
if ($password) { $args += @('/ConfigurationRepositoryP', $password) }
|
|
}
|
|
if ($Config.extension) { $args += @('-Extension', [string]$Config.extension) }
|
|
$action = [string]$Payload.action
|
|
if ($action -eq 'report') {
|
|
$args += @('/ConfigurationRepositoryReport', (Join-Path $temp 'report.txt'), '-NBegin', '-1', '-ReportFormat', 'txt')
|
|
} else {
|
|
$objects = Join-Path $temp 'objects.txt'
|
|
[IO.File]::WriteAllLines($objects, @($Payload.objects), [Text.UTF8Encoding]::new($false))
|
|
if ($action -eq 'lock') { $args += @('/ConfigurationRepositoryLock', '-Objects', $objects) }
|
|
elseif ($action -eq 'unlock') { $args += @('/ConfigurationRepositoryUnlock', '-Objects', $objects) }
|
|
elseif ($action -eq 'commit') {
|
|
$args += @('/ConfigurationRepositoryCommit', '-Objects', $objects, '-Comment', [string]$Payload.comment)
|
|
if ($Payload.keep_locked -eq $true) { $args += '-KeepLocked' }
|
|
} else { throw 'Unsupported repository action.' }
|
|
}
|
|
$process = Start-Process -FilePath ([string]$Config.designer_path) -ArgumentList (($args | ForEach-Object { Quote-Argument ([string]$_) }) -join ' ') -PassThru -WindowStyle Hidden
|
|
if (-not $process.WaitForExit(180000)) { $process.Kill(); return @{ status='timeout'; exit_code=$null } }
|
|
$text = if (Test-Path -LiteralPath $log) { Get-Content -LiteralPath $log -Raw -ErrorAction SilentlyContinue } else { '' }
|
|
foreach ($secretName in @('infobase_password', 'repository_password')) {
|
|
$secret = Get-Secret $Config $secretName
|
|
if ($secret) { $text = $text.Replace($secret, '[REDACTED]') }
|
|
}
|
|
$text = [string]$text
|
|
$excerpt = if ($text.Length -gt 4000) { $text.Substring($text.Length - 4000) } else { $text }
|
|
return @{ status=$(if ($process.ExitCode -eq 0) {'ok'} else {'failed'}); exit_code=$process.ExitCode; output=$excerpt }
|
|
} finally { Remove-Item -LiteralPath $temp -Recurse -Force -ErrorAction SilentlyContinue }
|
|
}
|
|
|
|
$listener = [Net.HttpListener]::new()
|
|
$listener.Prefixes.Add($ListenPrefix)
|
|
$listener.Start()
|
|
while ($listener.IsListening) {
|
|
$context = $listener.GetContext()
|
|
try {
|
|
if ($context.Request.HttpMethod -eq 'GET' -and $context.Request.Url.AbsolutePath -eq '/healthz') { Send-Json $context 200 @{status='ok';service='onec-repository-runner'}; continue }
|
|
$requestPath = $context.Request.Url.AbsolutePath
|
|
if ($context.Request.HttpMethod -ne 'POST' -or $requestPath -notin @('/repository/execute', '/configuration/activation/debug')) { Send-Json $context 404 @{status='not_found'}; continue }
|
|
$expected = [Environment]::GetEnvironmentVariable('ONEC_REPOSITORY_RUNNER_TOKEN', 'Process')
|
|
if (-not $expected) { $expected = [Environment]::GetEnvironmentVariable('ONEC_REPOSITORY_RUNNER_TOKEN', 'Machine') }
|
|
if (-not $expected -or $context.Request.Headers['Authorization'] -ne "Bearer $expected") { Send-Json $context 401 @{status='unauthorized'}; continue }
|
|
$reader = [IO.StreamReader]::new($context.Request.InputStream, [Text.Encoding]::UTF8)
|
|
$payload = $reader.ReadToEnd() | ConvertFrom-Json
|
|
$all = Get-Content -LiteralPath $ConfigPath -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
$base = $all.([string]$payload.base_id)
|
|
if (-not $base -or -not $base.repository) { Send-Json $context 400 @{status='not_configured'}; continue }
|
|
if ($requestPath -eq '/configuration/activation/debug') {
|
|
$layer = if ($payload.layer) { [string]$payload.layer } else { 'all' }
|
|
$mode = if ($payload.mode) { ([string]$payload.mode).ToLowerInvariant() } else { 'debug' }
|
|
$requestId = [string]$payload.request_id
|
|
$fingerprint = ([string]$payload.fingerprint).ToLowerInvariant()
|
|
if ($layer -notin @('all', 'base_saved_state', 'extension_saved_state') -or $mode -ne 'debug') {
|
|
Send-Json $context 400 @{status='invalid_request';message='a supported layer and mode=debug are required'}
|
|
continue
|
|
}
|
|
if ([bool]$requestId -ne [bool]$fingerprint -or ($requestId -and ($requestId -notmatch '^actreq-[0-9a-fA-F]{32}$' -or $fingerprint -notmatch '^[0-9a-f]{64}$'))) {
|
|
Send-Json $context 400 @{status='invalid_request';message='request_id and a 64-hex fingerprint must be supplied together'}
|
|
continue
|
|
}
|
|
$config = $base.repository
|
|
$selectorCount = 0
|
|
foreach ($selectorKey in @('file', 'server', 'name')) {
|
|
if ([string]$config.infobase.$selectorKey) { $selectorCount++ }
|
|
}
|
|
$designerConfigured = [bool]([string]$config.designer_path)
|
|
$designerAvailable = $designerConfigured -and (Test-Path -LiteralPath ([string]$config.designer_path) -PathType Leaf)
|
|
$selectorConfigured = $selectorCount -eq 1
|
|
$ready = $designerAvailable -and $selectorConfigured
|
|
$debugAcceptance = $null
|
|
if ($requestId) {
|
|
$receiptText = "base_id=$([string]$payload.base_id)`nlayer=$layer`nrequest_id=$requestId`nfingerprint=$fingerprint`nmode=debug"
|
|
$receiptBytes = [Text.Encoding]::UTF8.GetBytes($receiptText)
|
|
$receiptHash = [Security.Cryptography.SHA256]::Create()
|
|
try {
|
|
$receipt = if ($ready) { -join ($receiptHash.ComputeHash($receiptBytes) | ForEach-Object { $_.ToString('x2') }) } else { $null }
|
|
} finally { $receiptHash.Dispose() }
|
|
$debugAcceptance = @{
|
|
accepted=$ready
|
|
request_id=$requestId
|
|
fingerprint=$fingerprint
|
|
receipt=$receipt
|
|
}
|
|
}
|
|
$result = @{
|
|
schema='onec_configuration_activation_runner_probe.v1'
|
|
status=$(if ($ready) {'ready'} else {'not_ready'})
|
|
base_id=[string]$payload.base_id
|
|
layer=$layer
|
|
runner=@{
|
|
kind='local'
|
|
reachable=$true
|
|
designer_path_configured=$designerConfigured
|
|
designer_available=$designerAvailable
|
|
infobase_selector_configured=$selectorConfigured
|
|
}
|
|
operation=@{
|
|
kind=$(if ($layer -eq 'base_saved_state') {'/UpdateDBCfg'} else {$null})
|
|
execution_supported=$false
|
|
extension_manual_only=$layer -in @('all', 'extension_saved_state')
|
|
}
|
|
debug_acceptance=$debugAcceptance
|
|
execution=@{
|
|
mode='debug'
|
|
performed=$false
|
|
designer_started=$false
|
|
active_configuration_changed=$false
|
|
}
|
|
}
|
|
Send-Json $context 200 $result
|
|
continue
|
|
}
|
|
$result = Invoke-RepositoryAction $base.repository $payload
|
|
Send-Json $context $(if ($result.status -eq 'ok') {200} else {409}) $result
|
|
} catch { Send-Json $context 500 @{status='runner_error';message=$_.Exception.Message} }
|
|
}
|