Modules/businessdev.ALbuild.Containers/Public/Get-BcDockerDataRoot.ps1

function Get-BcDockerDataRoot {
    <#
    .SYNOPSIS
        Where Docker stores its images and container layers, and whether that place is a good one.
 
    .DESCRIPTION
        On a build server this one directory decides how much can be cached and how many containers can
        run. A BC container copies the service tier, the web client and the apps into its writable layer
        and restores the demo database there - many GB each - so a Docker root on the system drive is the
        usual reason a container dies with a cryptic "Failed to start service" after a long wait.
 
        This reports the current root, what daemon.json actually says (which is not the same question -
        an absent setting means the built-in default), the service that owns it, the volume's properties,
        and how much would have to be pulled again after a move. Read-only: it changes nothing.
 
        Use Set-BcDockerDataRoot to move it.
 
    .PARAMETER DockerExecutable
        The Docker executable to query. Default 'docker'.
 
    .PARAMETER ConfigPath
        The daemon configuration file. Defaults to the standard Windows location.
 
    .EXAMPLE
        Get-BcDockerDataRoot | Format-List
 
    .EXAMPLE
        (Get-BcDockerDataRoot).Warnings
 
        Just the reasons this host's Docker root is a poor place to keep an image cache.
 
    .OUTPUTS
        PSCustomObject with Path, ConfiguredPath, IsDefault, ConfigPath, ServiceName, ServiceStatus,
        IsDockerDesktop, Drive, FileSystem, DriveType, FreeGb, TotalGb, DeduplicationEnabled,
        ContainerCount, ImageCount, Warnings.
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [string] $DockerExecutable = 'docker',
        [string] $ConfigPath = (Join-Path $env:ProgramData 'Docker\config\daemon.json')
    )

    $warnings = [System.Collections.Generic.List[string]]::new()

    # --- what the daemon is actually using -------------------------------------------------------
    $path = $null
    $docker = Test-BcDocker -DockerExecutable $DockerExecutable
    if (-not $docker.Installed) {
        $warnings.Add("The Docker CLI ('$DockerExecutable') is not on PATH, so nothing about the daemon could be read.")
    }
    elseif (-not $docker.DaemonRunning) {
        $warnings.Add('The Docker daemon is not reachable, so the root in use could not be read; only daemon.json was inspected.')
    }
    else {
        $info = Invoke-BcDocker -DockerExecutable $DockerExecutable -Quiet -PassThru -Arguments @('info', '--format', '{{.DockerRootDir}}')
        if ($info.Success) { $path = "$($info.StdOut)".Trim() }
    }

    # --- what the configuration says ------------------------------------------------------------
    # A missing 'data-root' is not a missing answer: it means the daemon is on its built-in default,
    # which is exactly the case a move is meant to fix.
    $configured = $null
    $configExists = Test-Path -LiteralPath $ConfigPath
    if ($configExists) {
        try {
            $json = Get-Content -LiteralPath $ConfigPath -Raw -Encoding UTF8 | ConvertFrom-Json
            if ($json.PSObject.Properties['data-root']) { $configured = "$($json.'data-root')" }
            # 'graph' is the deprecated spelling; still honoured by the engine, so still worth reporting.
            elseif ($json.PSObject.Properties['graph']) {
                $configured = "$($json.graph)"
                $warnings.Add("daemon.json uses the deprecated 'graph' key instead of 'data-root'. Set-BcDockerDataRoot writes 'data-root'.")
            }
        }
        catch {
            $warnings.Add("daemon.json at '$ConfigPath' could not be parsed: $($_.Exception.Message). Fix it before moving the data root, or the daemon may refuse to start.")
        }
    }

    # --- who owns the service -------------------------------------------------------------------
    # 'docker' is the Docker Engine service; 'com.docker.service' belongs to Docker Desktop, which
    # manages its own settings and overwrites daemon.json - worth knowing before editing it.
    $serviceName = $null
    $serviceStatus = $null
    $isDesktop = $false
    foreach ($candidate in @('docker', 'com.docker.service')) {
        $svc = Get-Service -Name $candidate -ErrorAction SilentlyContinue
        if (-not $svc) { continue }
        if (-not $serviceName) {
            $serviceName = $svc.Name
            $serviceStatus = "$($svc.Status)"
        }
        if ($candidate -eq 'com.docker.service') { $isDesktop = $true }
    }
    if (-not $serviceName) {
        $warnings.Add('No Docker service was found. On this host the daemon is started some other way, and Set-BcDockerDataRoot cannot restart it - use -ServiceName to name it.')
    }
    if ($isDesktop -and $serviceName -eq 'com.docker.service') {
        $warnings.Add('This looks like Docker Desktop, which owns its own configuration and can overwrite daemon.json. Change the disk image location in Docker Desktop settings instead.')
    }

    # --- the volume ------------------------------------------------------------------------------
    $volume = if ($path) { Get-BcHostVolumeInfo -Path $path } else { $null }
    if ($volume) {
        if ($volume.FileSystem -and $volume.FileSystem -ne 'NTFS') {
            $warnings.Add("The data root is on $($volume.FileSystem); the Windows storage driver needs NTFS.")
        }
        if ($volume.DeduplicationEnabled) {
            $warnings.Add('Data Deduplication is enabled on this volume. It rewrites layer files underneath the storage driver and corrupts images - exclude the Docker root or move it.')
        }
        if ($null -ne $volume.FreeGb -and $volume.FreeGb -lt 200) {
            $warnings.Add("Only $($volume.FreeGb) GB free on $($volume.Drive). A BC container needs tens of GB, and a version image cache needs about 6.4 GB per platform version.")
        }
    }

    # --- what a move would cost ------------------------------------------------------------------
    # The new root starts empty, so this is the inventory that would have to be pulled again.
    $containerCount = $null
    $imageCount = $null
    if ($docker.DaemonRunning) {
        $c = Invoke-BcDocker -DockerExecutable $DockerExecutable -Quiet -PassThru -Arguments @('ps', '-aq')
        if ($c.Success) { $containerCount = @("$($c.StdOut)".Trim() -split '\r?\n' | Where-Object { $_ }).Count }
        $i = Invoke-BcDocker -DockerExecutable $DockerExecutable -Quiet -PassThru -Arguments @('images', '-q')
        if ($i.Success) { $imageCount = @("$($i.StdOut)".Trim() -split '\r?\n' | Where-Object { $_ } | Sort-Object -Unique).Count }
    }

    return [PSCustomObject]@{
        Path                 = $path
        ConfiguredPath       = $configured
        IsDefault            = [bool](-not $configured)
        ConfigPath           = $ConfigPath
        ConfigExists         = $configExists
        ServiceName          = $serviceName
        ServiceStatus        = $serviceStatus
        IsDockerDesktop      = $isDesktop
        Drive                = if ($volume) { $volume.Drive } else { $null }
        FileSystem           = if ($volume) { $volume.FileSystem } else { $null }
        DriveType            = if ($volume) { $volume.DriveType } else { $null }
        FreeGb               = if ($volume) { $volume.FreeGb } else { $null }
        TotalGb              = if ($volume) { $volume.TotalGb } else { $null }
        DeduplicationEnabled = if ($volume) { $volume.DeduplicationEnabled } else { $null }
        ContainerCount       = $containerCount
        ImageCount           = $imageCount
        Warnings             = @($warnings)
    }
}