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 } if ($context.Request.HttpMethod -ne 'POST' -or $context.Request.Url.AbsolutePath -ne '/repository/execute') { 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 } $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} } }