Modules/businessdev.ALbuild.Containers/Public/New-BcContainer.ps1

function New-BcContainer {
    <#
    .SYNOPSIS
        Creates a Business Central Docker container from an artifact.
 
    .DESCRIPTION
        Pulls the Business Central generic image (if needed) and runs a container configured for
        the given artifact URL using the generic image's environment contract (accept_eula,
        artifactUrl, Auth, username, password, licenseFile). Requires a Windows host with a running
        Docker engine; on other platforms it fails with a clear message. After starting, it waits
        for the container to report ready unless -NoWait is specified.
 
    .PARAMETER Name
        Container name.
 
    .PARAMETER ArtifactUrl
        The artifact URL (see Find-BcArtifactUrl). Required unless -Type/-Country/-Version are used.
 
    .PARAMETER Credential
        Credential for the container's admin user (used with UserPassword auth).
 
    .PARAMETER Auth
        Authentication model: UserPassword (default), NavUserPassword, Windows or AAD.
 
    .PARAMETER ImageName
        The generic image to base the container on.
 
    .PARAMETER MemoryLimit
        Optional memory limit (e.g. '8G'). Defaults to '8G' when not specified: Business Central's
        start script requires at least 3 GB, and under hyperv isolation (the Docker Desktop default
        on Windows client hosts) this value sizes the container VM, so leaving it unset makes the
        container exit immediately with "At least 3Gb memory needs to be available to the Container".
 
    .PARAMETER Isolation
        Container isolation: process or hyperv (default: let Docker decide).
 
    .PARAMETER LicenseFile
        Optional license file (path or URL) passed to the image.
 
    .PARAMETER Labels
        Additional Docker labels (hashtable).
 
    .PARAMETER PublishPorts
        Ports to publish (docker --publish values).
 
    .PARAMETER Http
        Serve the Web Client / Web Services over plain HTTP instead of the generic image's default
        self-signed HTTPS (sets the image env 'useSSL=N'). NavUserPassword works over HTTP. Intended
        for Sandbox/Dev containers on a trusted LAN only - credentials travel in clear text over HTTP.
 
    .PARAMETER Transparent
        Attach the container to a transparent Docker network so it gets its own LAN IP via DHCP and is
        reachable from other hosts on the LAN (no port publishing / firewall rule needed). The
        transparent network is created if the host does not already have one (see
        Initialize-BcTransparentNetwork). Dev-only; on a Hyper-V VM host the vNIC needs
        MACAddressSpoofing enabled.
 
    .PARAMETER EnvironmentVariables
        Additional environment variables (hashtable) merged into the image contract.
 
    .PARAMETER AdditionalArguments
        Extra raw 'docker run' arguments.
 
    .PARAMETER NoWait
        Do not wait for the container to become ready.
 
    .PARAMETER DockerExecutable
        The Docker executable to use (default 'docker').
 
    .EXAMPLE
        $cred = Get-Credential
        New-BcContainer -Name bld -ArtifactUrl (Find-BcArtifactUrl -Country w1 -Select Latest) -Credential $cred
 
    .OUTPUTS
        PSCustomObject describing the container.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $Name,
        [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string] $ArtifactUrl,
        [pscredential] $Credential,
        [ValidateSet('UserPassword', 'NavUserPassword', 'Windows', 'AAD')] [string] $Auth = 'UserPassword',
        [string] $ImageName = 'mcr.microsoft.com/businesscentral:ltsc2022',
        [string] $MemoryLimit,
        [ValidateSet('', 'process', 'hyperv')] [string] $Isolation = '',
        [string] $LicenseFile,
        [hashtable] $Labels = @{},
        [string[]] $PublishPorts = @(),
        [switch] $Http,
        [switch] $Transparent,
        [hashtable] $EnvironmentVariables = @{},
        [string[]] $AdditionalArguments = @(),
        [switch] $NoWait,
        # Persistent host folder shared into the container at C:\dl so the artifact is downloaded once
        # per host and reused by later containers. Default: ALBUILD_BCARTIFACT_CACHE env var, else the
        # BcArtifactCacheFolder config setting (C:\bcartifacts.cache).
        [string] $ArtifactCacheFolder,
        [switch] $NoArtifactCache,
        [string] $DockerExecutable = 'docker'
    )

    Write-ALbuildLog "Creating Business Central container '$Name' from artifact '$ArtifactUrl' with image '$ImageName'..."

    Test-BcPlatform -Require | Out-Null
    Test-BcDocker -DockerExecutable $DockerExecutable -Require | Out-Null

    # BC's start script enforces a >= 3 GB minimum; default to 8 GB so the container starts under
    # any isolation mode (under hyperv this value sizes the VM, not just a cap). Overridable.
    if ([string]::IsNullOrWhiteSpace($MemoryLimit)) { $MemoryLimit = '8G' }

    $containerEnv = @{
        'accept_eula' = 'Y'
        'artifactUrl' = $ArtifactUrl
        'Auth'        = $Auth
    }
    if ($Credential) {
        $containerEnv['username'] = $Credential.UserName
        $containerEnv['password'] = $Credential.GetNetworkCredential().Password
    }
    # HTTP-only: the BC generic image serves the Web Client over self-signed HTTPS by default; setting
    # useSSL=N makes it plain HTTP (works with NavUserPassword). Default (no -Http) leaves the image
    # default untouched. See Get-BcContainerWebClientUrl / docs for the Dev-only, LAN-only caveat.
    if ($Http) { $containerEnv['useSSL'] = 'N' }
    foreach ($key in $EnvironmentVariables.Keys) { $containerEnv[$key] = $EnvironmentVariables[$key] }

    # Stamp the resulting protocol + reachability as labels so later steps (inspect, remove, the URL
    # helper) can read the container's state without re-deriving it.
    $protocol = if ($Http) { 'http' } else { 'https' }
    $reachability = if ($Transparent) { 'transparent' } else { 'none' }
    $Labels['albuild.protocol'] = $protocol
    $Labels['albuild.reachability'] = $reachability

    if (-not $PSCmdlet.ShouldProcess($Name, "Create Business Central container from $ArtifactUrl")) { return }

    # Remove any pre-existing container of the same name so a re-run does not fail with a name
    # conflict ('The container name "/X" is already in use'). 'docker container inspect' exits 0 when
    # the container exists and 1 when it does not.
    $existing = Invoke-BcDocker -DockerExecutable $DockerExecutable -Quiet -PassThru -SuccessExitCodes @(0, 1) -Arguments @('container', 'inspect', $Name)
    if ($existing.ExitCode -eq 0) {
        Write-ALbuildLog "Container '$Name' already exists; removing it before recreating."
        Invoke-BcDocker -DockerExecutable $DockerExecutable -Quiet -Arguments @('rm', '--force', $Name) | Out-Null
    }

    # Bind-mount a host folder to C:\run\my so files (apps, packages) can be shared into the
    # container by writing to the host side - 'docker cp' is not supported against a running
    # hyperv-isolated container (the Docker Desktop default on Windows client hosts).
    $hostShare = Get-BcContainerHostShare -Name $Name
    if (Test-Path -LiteralPath $hostShare) { Remove-Item -LiteralPath $hostShare -Recurse -Force -ErrorAction SilentlyContinue }
    New-Item -ItemType Directory -Force -Path $hostShare | Out-Null
    $volumes = @("$($hostShare):C:\run\my")

    # Share a persistent host artifact cache into the container at C:\dl - the folder the BC image
    # downloads its artifact into and checks before downloading. The first container on a host fills it;
    # every later container finds the artifact already there and skips the multi-GB download. This is the
    # BcContainerHelper cache model (same layout, so an existing c:\bcartifacts.cache is reused too), and
    # it matters most on self-hosted agents with persistent storage. Disable with -NoArtifactCache.
    if (-not $NoArtifactCache) {
        $artifactCache = if ($ArtifactCacheFolder) { $ArtifactCacheFolder }
        elseif (-not [string]::IsNullOrWhiteSpace($env:ALBUILD_BCARTIFACT_CACHE)) { $env:ALBUILD_BCARTIFACT_CACHE }
        else { Get-ALbuildConfig -Name 'BcArtifactCacheFolder' }
        if (-not [string]::IsNullOrWhiteSpace($artifactCache)) {
            New-Item -ItemType Directory -Force -Path $artifactCache | Out-Null
            $volumes += "$($artifactCache):C:\dl"
            Write-ALbuildLog "Sharing artifact cache '$artifactCache' into the container at C:\dl (download once per host, reuse thereafter)."
            # Bulletproofing: an earlier interrupted download can leave an INCOMPLETE artifact in the
            # cache (folder present, no manifest.json). The image would then find the folder, skip the
            # re-download and exit with "Cannot find ...\manifest.json". Validate the cached artifact and
            # throw away any incomplete copy so this container re-downloads a clean one.
            Repair-BcArtifactCache -CacheFolder $artifactCache -ArtifactUrl $ArtifactUrl | Out-Null
        }
    }

    # A local license file must be made available *inside* the container - the host path (e.g. a
    # OneDrive folder) does not exist in the container, so the start script reports 'License File not
    # found'. Stage it into the bind-mounted share and reference the in-container path (URLs pass
    # through unchanged for the container to download).
    if ($LicenseFile) {
        $containerEnv['licenseFile'] = Resolve-BcContainerLicense -LicenseFile $LicenseFile -HostShare $hostShare
    }

    Write-ALbuildLog "Pulling image $ImageName ..."
    Invoke-BcDocker -DockerExecutable $DockerExecutable -RetryCount 5 -RetryDelaySeconds 15 -Arguments @('pull', $ImageName) | Out-Null

    # Transparent reachability: attach to a transparent Docker network (created on demand) so the
    # container gets its own LAN IP and is reachable from other hosts. Default = Docker's NAT (empty).
    $network = ''
    if ($Transparent) { $network = Initialize-BcTransparentNetwork -DockerExecutable $DockerExecutable }

    $runArgs = Get-BcContainerRunArguments -ImageName $ImageName -Name $Name -EnvironmentVariables $containerEnv `
        -MemoryLimit $MemoryLimit -Isolation $Isolation -Network $network -PublishPorts $PublishPorts -Labels $Labels `
        -Volumes $volumes -AdditionalArguments $AdditionalArguments -Detach $true

    Write-ALbuildLog "Creating container $Name ..."
    Invoke-BcDocker -DockerExecutable $DockerExecutable -Arguments $runArgs | Out-Null

    if (-not $NoWait) {
        Write-ALbuildLog "Waiting for container $Name to become ready (first run downloads the artifact; this can take several minutes)..."
        Wait-BcContainerReady -Name $Name -DockerExecutable $DockerExecutable | Out-Null
    }

    $container = Get-BcContainer -Name $Name -DockerExecutable $DockerExecutable

    # Enrich the returned object with the reachable Web Client URL + protocol/reachability so callers
    # (MCP ensure-container, the CLI, the browser agent) get a usable address. Best-effort: a container
    # started with -NoWait may not have an IP yet, in which case the URL fields are $null.
    $web = $null
    try { $web = Get-BcContainerWebClientUrl -Name $Name -Protocol $protocol -DockerExecutable $DockerExecutable }
    catch { Write-ALbuildLog -Level Warning "Could not resolve the Web Client URL for '$Name': $($_.Exception.Message)" }
    $container | Add-Member -NotePropertyMembers @{
        Protocol     = $protocol
        Reachability = $reachability
        WebClientUrl = if ($web) { $web.Url } else { $null }
        IpAddress    = if ($web) { $web.Ip } else { $null }
        Port         = if ($web) { $web.Port } else { $null }
    } -Force
    return $container
}