Wsl-Image/Wsl-Image.Docker.ps1
New-Variable -Name DockerHubAuthDomain -Value "auth.docker.io" -Option Constant -Force New-Variable -Name DockerHubRegistryDomain -Value "registry-1.docker.io" -Option Constant -Force New-Variable -Name DockerHubService -Value "registry.docker.io" -Option Constant -Force # Internal function to get authentication token function Get-DockerAuthToken { param( [string]$Registry, [string]$Repository ) $Service = $Registry $AuthDomain = $Registry if ($Registry -eq "docker.io") { $Service = $DockerHubService $AuthDomain = $DockerHubAuthDomain if ($Repository -notmatch "/") { $Repository = "library/$Repository" } } try { Write-Verbose "Getting docker authentication token for registry $Registry and repository $Repository..." $tokenUrl = "https://$AuthDomain/token?service=$Service&scope=repository:$Repository`:pull" $Headers = @{ "User-Agent" = (Get-UserAgent) } $tokenContent = Invoke-FetchUrl -Uri $tokenUrl -Headers $Headers $tokenData = $tokenContent | ConvertFrom-Json return $tokenData.token } catch { throw [WslImageDownloadException]::new("Failed to get authentication token: $($_.Exception.Message)", $_.Exception) } } function Get-DockerImageManifest { [CmdletBinding()] param ( [Parameter(Mandatory = $false)] [string]$AuthToken, [Parameter(Mandatory = $true)] [string]$ImageName, [Parameter(Mandatory = $true)] [string]$Tag, [Parameter(Mandatory = $false)] [string]$Registry = "ghcr.io" ) Progress "Retrieving docker image manifest for $ImageName`:$Tag from registry $Registry..." $RegistryDomain = $Registry if ($Registry -eq "docker.io") { $RegistryDomain = $DockerHubRegistryDomain if ($ImageName -notmatch "/") { $ImageName = "library/$ImageName" } } if (-not $AuthToken) { $AuthToken = Get-DockerAuthToken -Registry $Registry -Repository $ImageName } $Headers = @{ "User-Agent" = (Get-UserAgent) Accept = "application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.docker.distribution.manifest.v2+json" Authorization = "Bearer $AuthToken" } # Step 1: Get the image manifest $manifestUrl = "https://$RegistryDomain/v2/$ImageName/manifests/$Tag" # Mutualize Exception Handling to ease coverage $ExceptionBlock = { if ($_.Exception.Response.StatusCode -eq 401) { throw [WslImageDownloadException]::new("Access denied to registry. The image may not exist or authentication failed.", $_.Exception) } elseif ($_.Exception.Response.StatusCode -eq 404) { throw [WslImageDownloadException]::new("Image not found: $fullImageName`:$Tag", $_.Exception) } else { throw [WslImageDownloadException]::new("Failed to get manifest: $($_.Exception.Message)", $_.Exception) } } try { Write-Verbose "Getting docker image manifest $($manifestUrl)..." $manifestJson = Invoke-FetchUrl -Uri $manifestUrl -Headers $Headers $manifest = $manifestJson | ConvertFrom-Json } catch [System.Net.WebException] { . $ExceptionBlock } # Step 2: Extract the amd manifest information if (-not $manifest.manifests -or $manifest.manifests.Count -eq 0) { throw [WslImageDownloadException]::new("No manifests found in the image manifest") } $amdManifest = $manifest.manifests | Where-Object { $_.platform.architecture -eq 'amd64' } if (-not $amdManifest) { throw [WslImageDownloadException]::new("No amd64 manifest found in the image manifest") } # replace the Accept header $Headers.Accept = $amdManifest.mediaType $manifestUrl = "https://$RegistryDomain/v2/$ImageName/manifests/$($amdManifest.digest)" try { Write-Verbose "Getting docker image amd64 manifest $($manifestUrl)..." $manifestJson = Invoke-FetchUrl -Uri $manifestUrl -Headers $Headers $manifest = $manifestJson | ConvertFrom-Json | Convert-PSObjectToHashtable } catch [System.Net.WebException] { . $ExceptionBlock } if (-not $manifest.layers) { throw [WslImageDownloadException]::new("The image layers are missing") } $layer = $manifest.layers # if $layer is an Array, test that is has only one element and get it if ($layer -is [Array]) { if ($layer.Count -ne 1) { throw [WslImageDownloadException]::new("The image should have exactly one layer") } $layer = $layer[0] } $config = $manifest.config $configDigest = $config.digest $Headers.Accept = $config.mediaType $configUrl = "https://$RegistryDomain/v2/$ImageName/blobs/$configDigest" try { Write-Verbose "Getting docker image config $($configUrl)..." $configJson = Invoke-FetchUrl -Uri $configUrl -Headers $Headers $config = $configJson | ConvertFrom-Json | Select-Object -Property * -ExcludeProperty history, rootfs | Convert-PSObjectToHashtable } catch [System.Net.WebException] { . $ExceptionBlock } $config.mediaType = $layer.mediaType $config.size = $layer.size $config.digest = $layer.digest return $config } <# .SYNOPSIS Downloads a Docker image from GitHub Container Registry (ghcr.io) as a tar.gz file. .DESCRIPTION This function downloads a Docker image from GitHub Container Registry by making HTTP requests to: 1. Get the image manifest 2. Ensure the image contains only one layer 3. Download the layer blob 4. Save it as a tar.gz file locally This is specifically designed to work with images built by the build-Image-oci.yaml workflow, which creates images with a single layer containing the root filesystem. .PARAMETER ImageName The name of the Docker image (e.g., "antoinemartin/powershell-wsl-manager/miniwsl-alpine") .PARAMETER Tag The tag of the image (e.g., "latest", "3.19.1", "2025.08.01") .PARAMETER DestinationFile The path where the downloaded layer should be saved as a tar.gz file .PARAMETER Registry The container registry URL. Defaults to "ghcr.io" .EXAMPLE Get-DockerImage -ImageName "antoinemartin/powershell-wsl-manager/miniwsl-alpine" -Tag "latest" -DestinationFile "alpine.rootfs.tar.gz" Downloads the latest alpine miniwsl image layer to alpine.rootfs.tar.gz .EXAMPLE Get-DockerImage -ImageName "antoinemartin/powershell-wsl-manager/miniwsl-arch" -Tag "2025.08.01" -DestinationFile "arch.rootfs.tar.gz" Downloads the arch miniwsl image with specific version tag .NOTES This function requires network access to the GitHub Container Registry. The function assumes the Docker image has only one layer (typical for FROM scratch images with ADD). #> function Get-DockerImage { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string]$ImageName, [Parameter(Mandatory = $true)] [string]$Tag, [Parameter(Mandatory = $true)] [string]$DestinationFile, [Parameter(Mandatory = $false)] [string]$Registry = "ghcr.io" ) try { $RegistryDomain = $Registry if ($Registry -eq "docker.io") { $RegistryDomain = $DockerHubRegistryDomain if ($ImageName -notmatch "/") { $ImageName = "library/$ImageName" } } $fullImageName = "$Registry/$ImageName" Progress "Downloading Docker image layer from $fullImageName`:$Tag..." # Get authentication token $authToken = Get-DockerAuthToken -Registry $Registry -Repository $ImageName if (-not $authToken) { throw [WslImageDownloadException]::new("Failed to retrieve authentication token for registry $Registry and repository $ImageName") } $layer = Get-DockerImageManifest -Registry $Registry -ImageName $ImageName -Tag $Tag -AuthToken $authToken $layerDigest = $layer.digest $layerSize = $layer.size Information "Root filesystem size: $(Format-FileSize $layerSize). Digest $layerDigest. Downloading..." # Step 3: Download the layer blob $blobUrl = "https://$RegistryDomain/v2/$ImageName/blobs/$layerDigest" # Prepare destination file $destinationFileInfo = [System.IO.FileInfo]::new($DestinationFile) # Ensure destination directory exists if (-not $destinationFileInfo.Directory.Exists) { $destinationFileInfo.Directory.Create() } Start-Download $blobUrl $destinationFileInfo.FullName @{ Authorization = "Bearer $authToken" } # Verify the file was created and has content if ($destinationFileInfo.Exists) { $destinationFileInfo.Refresh() Success "Successfully downloaded Docker image layer to $($destinationFileInfo.FullName). File size: $(Format-FileSize $destinationFileInfo.Length)" # Check file integrity (e.g., hash) $expectedHash = $layer.digest -split ":" | Select-Object -Last 1 # $actualHash = Get-FileHash -Path $destinationFileInfo.FullName -Algorithm SHA256 | Select-Object -ExpandProperty Hash # if ($expectedHash -ne $actualHash) { # throw [WslImageDownloadException]::new("Downloaded file hash does not match expected hash. Expected: $expectedHash, Actual: $actualHash") # } return $expectedHash } else { throw [WslImageDownloadException]::new("Failed to create destination file: $DestinationFile") } } catch { Write-Error "Failed to download Docker image layer: $($_.Exception.Message)" throw } finally { if ($webClient) { $webClient.Dispose() } } } |