Modules/businessdev.ALbuild.Containers/Public/Invoke-BcContainerCommand.ps1
|
function Invoke-BcContainerCommand { <# .SYNOPSIS Runs a PowerShell script block inside a running Business Central (Windows) container. .DESCRIPTION Executes the script block inside the container via 'docker exec ... powershell -EncodedCommand', returning the captured stdout. Simple variables can be passed with -Variables (marshalled as JSON across the process boundary). This is the single seam through which higher-level container cmdlets run Business Central management cmdlets. .PARAMETER ContainerName The target container. .PARAMETER ScriptBlock The PowerShell to run inside the container. .PARAMETER Variables Optional hashtable of simple values made available as variables inside the container. .PARAMETER DockerExecutable The Docker executable to use (default 'docker'). .EXAMPLE Invoke-BcContainerCommand -ContainerName bld -ScriptBlock { Get-NavServerInstance } .OUTPUTS System.String (the command's stdout). #> [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory, Position = 0)] [ValidateNotNullOrEmpty()] [string] $ContainerName, [Parameter(Mandatory, Position = 1)] [ValidateNotNull()] [scriptblock] $ScriptBlock, [hashtable] $Variables = @{}, # Echo the in-container output live (so a long operation shows progress) instead of returning # it only after the command completes. The full output is still captured and returned. [switch] $StreamOutput, [string] $DockerExecutable = 'docker' ) $scriptText = ConvertTo-BcEncodedCommand -ScriptBlock $ScriptBlock -Variables $Variables -AsText $encoded = [System.Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($scriptText)) # 'docker exec ... powershell -EncodedCommand <base64>' is a single command line, capped at # ~32 KB on Windows. Large in-container scripts (dependency install, publish, toolkit) exceed it, # so stage them as a .ps1 in the container's shared folder and run with -File instead. $cmdFileHost = $null if ($encoded.Length -lt 24000) { $arguments = @('exec', $ContainerName, 'powershell', '-NoLogo', '-NoProfile', '-EncodedCommand', $encoded) } else { $hostShare = Get-BcContainerHostShare -Name $ContainerName if (-not (Test-Path -LiteralPath $hostShare)) { New-Item -ItemType Directory -Force -Path $hostShare | Out-Null } $leaf = ".albuild-cmd-$([guid]::NewGuid().ToString('N')).ps1" $cmdFileHost = Join-Path $hostShare $leaf # UTF-8 with BOM so the container's Windows PowerShell reads non-ASCII correctly. [System.IO.File]::WriteAllText($cmdFileHost, $scriptText, [System.Text.UTF8Encoding]::new($true)) $arguments = @('exec', $ContainerName, 'powershell', '-NoLogo', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', "C:\run\my\$leaf") } $dockerArgs = @{ DockerExecutable = $DockerExecutable PassThru = $true Quiet = $true Arguments = $arguments } if ($StreamOutput) { $dockerArgs['StreamOutput'] = $true } try { $result = Invoke-BcDocker @dockerArgs } finally { if ($cmdFileHost) { Remove-Item -LiteralPath $cmdFileHost -Force -ErrorAction SilentlyContinue } } if (-not $result.Success) { # Prefer an explicit, already-clean failure summary the in-container script emits as # 'ALBUILD-ERROR:' lines and exits on - this avoids PowerShell's error-record scaffolding # (source line, '~~~~', 'At line:', '+ CategoryInfo'/'+ FullyQualifiedErrorId'), which is # fragmented unreliably by CLIXML serialisation. Fall back to the captured stderr/stdout, # decoded from CLIXML and reduced to its core message. $markerLines = @($result.StdOut -split "`r?`n" | Where-Object { $_ -match '^ALBUILD-ERROR:' } | ForEach-Object { ($_ -replace '^ALBUILD-ERROR:', '').TrimEnd() }) if ($markerLines.Count -gt 0) { $detail = ($markerLines -join [Environment]::NewLine).Trim() } else { $raw = if ([string]::IsNullOrWhiteSpace($result.StdErr)) { $result.StdOut } else { $result.StdErr } $detail = Format-BcErrorMessage -Text (ConvertFrom-BcCliXml -Text $raw) } throw "Command in container '$ContainerName' failed (exit $($result.ExitCode)).$(if ([string]::IsNullOrWhiteSpace($detail)) { '' } else { [Environment]::NewLine + $detail.Trim() })" } return $result.StdOut } |