Modules/businessdev.ALbuild.Containers/Public/Invoke-BcDocker.ps1

function Invoke-BcDocker {
    <#
    .SYNOPSIS
        Runs a Docker CLI command for ALbuild (the central Docker entry point).
 
    .DESCRIPTION
        Every Docker interaction in ALbuild goes through this function so behaviour is consistent:
        the Docker CLI is located (and a clear error raised if it is missing), the command is run
        via Invoke-ALbuildProcess (reliable output capture, optional retry/back-off), registry
        authentication failures on 'pull' are turned into actionable errors, and output is logged
        through Write-ALbuildLog. This is ALbuild's equivalent of BcContainerHelper's DockerDo, written
        from scratch on top of the shared process helper.
 
    .PARAMETER Arguments
        The Docker command and its arguments, e.g. @('ps','--all','--format','{{.Names}}').
 
    .PARAMETER DockerExecutable
        The Docker executable to use. Default 'docker'. Allows full paths or alternatives
        (e.g. 'docker.exe', 'podman') and makes the function testable against a stand-in.
 
    .PARAMETER WorkingDirectory
        Working directory for the Docker process.
 
    .PARAMETER SuccessExitCodes
        Exit codes treated as success. Default: 0.
 
    .PARAMETER RetryCount
        Additional attempts on failure (use for transient operations such as 'pull'). Default 0.
 
    .PARAMETER RetryDelaySeconds
        Delay between attempts. Default 5.
 
    .PARAMETER PassThru
        Return the result object even on failure instead of throwing.
 
    .PARAMETER Quiet
        Do not echo Docker stdout through Write-ALbuildLog.
 
    .EXAMPLE
        Invoke-BcDocker -Arguments @('version','--format','{{.Server.Version}}')
 
    .EXAMPLE
        Invoke-BcDocker -Arguments @('pull', $imageName) -RetryCount 5 -RetryDelaySeconds 15
 
    .OUTPUTS
        PSCustomObject with ExitCode, StdOut, StdErr, Success, Attempts.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, Position = 0)]
        [ValidateNotNullOrEmpty()]
        [string[]] $Arguments,

        [string] $DockerExecutable = 'docker',

        [string] $WorkingDirectory,

        [int[]] $SuccessExitCodes = @(0),

        [ValidateRange(0, [int]::MaxValue)]
        [int] $RetryCount = 0,

        [ValidateRange(0, [int]::MaxValue)]
        [int] $RetryDelaySeconds = 5,

        [switch] $PassThru,

        [switch] $Quiet,

        # Echo the process output live (chunk-streamed) instead of only after it completes - used for
        # long-running container operations so progress is visible.
        [switch] $StreamOutput
    )

    $command = Get-Command -Name $DockerExecutable -CommandType Application -ErrorAction SilentlyContinue |
        Select-Object -First 1
    if (-not $command) {
        throw "The Docker CLI ('$DockerExecutable') was not found on PATH. Business Central containers require Docker on a Windows host. Install Docker, or run containerless operations instead."
    }

    $procArgs = @{
        FilePath          = $command.Source
        Arguments         = $Arguments
        SuccessExitCodes  = $SuccessExitCodes
        RetryCount        = $RetryCount
        RetryDelaySeconds = $RetryDelaySeconds
        PassThru          = $true
    }
    if ($WorkingDirectory) { $procArgs['WorkingDirectory'] = $WorkingDirectory }
    if ($StreamOutput) { $procArgs['StreamOutput'] = $true }

    $result = Invoke-ALbuildProcess @procArgs

    if (-not $Quiet -and -not [string]::IsNullOrWhiteSpace($result.StdOut)) {
        Write-ALbuildLog $result.StdOut.TrimEnd()
    }

    if (-not $result.Success) {
        $combined = "$($result.StdOut)`n$($result.StdErr)"
        if ($combined -match '(?im)please login|unauthorized|denied|authentication required') {
            $registry = ($Arguments | Where-Object { $_ -match '/' } | Select-Object -First 1)
            if ($registry) { $registry = $registry.Split('/')[0] }
            throw "Docker registry authentication is required$(if ($registry) { " for '$registry'" }). Log in with 'docker login' (for Microsoft insider/private images, obtain credentials via the Microsoft ReadyToGo / Collaborate program) and retry."
        }

        if (-not $PassThru) {
            $detail = if ([string]::IsNullOrWhiteSpace($result.StdErr)) { $result.StdOut } else { $result.StdErr }
            throw "Docker command 'docker $($Arguments -join ' ')' failed with exit code $($result.ExitCode).$(if ([string]::IsNullOrWhiteSpace($detail)) { '' } else { [Environment]::NewLine + $detail.Trim() })"
        }
    }

    return $result
}