Files

134 lines
5.7 KiB
PowerShell
Raw Permalink Normal View History

<#
Manual OCI layout pusher for registries whose token realm is misconfigured.
This script is an emergency fallback. It reads Docker's OCI layout output,
obtains a Bearer token over HTTPS from the Git credential store, and uploads
the blobs and image manifest directly through the Registry HTTP API.
默认面向 GitHub Container Registry(ghcr.io)。如果要用回自建 Gitea registry:
-Registry jayhub.top -Repository mrlee/havline -CredentialHost jayhub.top `
-TokenPath /v2/token -TokenService container_registry
#>
param(
[Parameter(Mandatory = $true)][string]$LayoutDir,
[Parameter(Mandatory = $true)][string]$Platform,
[Parameter(Mandatory = $true)][string]$Tag,
[string]$Registry = "ghcr.io",
[string]$Repository = "mrlee233/havline",
[string]$CredentialHost = "ghcr.io",
# GHCR: /token?service=ghcr.io;Gitea: /v2/token?service=container_registry
[string]$TokenPath = "/token",
[string]$TokenService = "ghcr.io"
)
$ErrorActionPreference = "Stop"
$script:Registry = $Registry
$script:Repository = $Repository
function Get-RegistryBasicAuth {
$cred = "protocol=https`nhost=$CredentialHost`n`n" | git credential fill 2>$null
$values = @{}
$cred -split "`n" | ForEach-Object {
if ($_ -match '^(.*?)=(.*)$') { $values[$matches[1]] = $matches[2] }
}
if (-not $values.username -or -not $values.password) {
throw "No Git credential found for $CredentialHost"
}
$raw = $values.username + ":" + $values.password
return "Basic " + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($raw))
}
function Get-BearerToken([string]$Scope) {
$basic = Get-RegistryBasicAuth
$url = "https://$Registry$TokenPath?service=$TokenService&scope=$Scope"
$response = Invoke-RestMethod -Uri $url -Headers @{ Authorization = $basic }
if (-not $response.token) { throw "Registry did not return a token" }
return $response.token
}
function Get-BlobPath([string]$LayoutRoot, [string]$Digest) {
return Join-Path $LayoutRoot ("blobs\sha256\" + ($Digest -replace '^sha256:', ''))
}
function Read-JsonBlob([string]$LayoutRoot, [string]$Digest) {
$path = Get-BlobPath $LayoutRoot $Digest
return Get-Content $path -Raw | ConvertFrom-Json
}
function Find-PlatformManifest($Index, [string]$Arch, [string]$Os) {
foreach ($entry in $Index.manifests) {
if ($entry.platform.architecture -eq $Arch -and $entry.platform.os -eq $Os) {
return $entry
}
}
return $null
}
function Resolve-ImageManifest([string]$LayoutRoot, [string]$Arch, [string]$Os) {
$index = Get-Content (Join-Path $LayoutRoot "index.json") -Raw | ConvertFrom-Json
foreach ($entry in $index.manifests) {
$document = Read-JsonBlob $LayoutRoot $entry.digest
if ($document.mediaType -eq "application/vnd.oci.image.manifest.v1+json") {
return @{ Digest = $entry.digest; Document = $document }
}
$match = Find-PlatformManifest $document $Arch $Os
if ($match) {
return @{ Digest = $match.digest; Document = (Read-JsonBlob $LayoutRoot $match.digest) }
}
}
throw "No image manifest found for $Os/$Arch"
}
function Test-BlobExists([string]$Digest, [string]$Token) {
$url = "https://$Registry/v2/$Repository/blobs/$Digest"
$response = Invoke-WebRequest -Uri $url -Method Head -Headers @{ Authorization = "Bearer $Token" } -SkipHttpErrorCheck
return $response.StatusCode -eq 200
}
function Push-Blob([string]$LayoutRoot, [string]$Digest, [string]$Token) {
if (Test-BlobExists $Digest $Token) {
Write-Output "Blob already exists: $Digest"
return
}
$postUrl = "https://$Registry/v2/$Repository/blobs/uploads/"
$post = Invoke-WebRequest -Uri $postUrl -Method Post -Headers @{ Authorization = "Bearer $Token" } -SkipHttpErrorCheck
if ($post.StatusCode -ne 202) { throw "Create upload session failed: HTTP $($post.StatusCode)" }
$location = $post.Headers["Location"]
if ($location -is [array]) { $location = $location[0] }
if ($location -notmatch '^https?://') { $location = "https://$Registry$location" }
$separator = "?"
if ($location -match '\?') { $separator = "&" }
$putUrl = $location + $separator + "digest=" + $Digest
$path = Get-BlobPath $LayoutRoot $Digest
$curlArgs = @(
"-sS", "-o", "NUL", "-w", "%{http_code}", "-X", "PUT",
"-H", "Authorization: Bearer $Token",
"-H", "Content-Type: application/octet-stream",
"--data-binary", ("@" + $path), $putUrl
)
$status = (& curl.exe @curlArgs).Trim()
if ($status -ne "201") { throw "Upload blob failed: $Digest -> HTTP $status" }
Write-Output "Uploaded blob: $Digest"
}
function Push-Manifest([string]$TagValue, [string]$MediaType, [string]$Json, [string]$Token) {
$url = "https://$Registry/v2/$Repository/manifests/$TagValue"
$response = Invoke-WebRequest -Uri $url -Method Put -Headers @{ Authorization = "Bearer $Token" } -ContentType $MediaType -Body ([Text.Encoding]::UTF8.GetBytes($Json)) -SkipHttpErrorCheck
if ($response.StatusCode -ne 201) {
throw "Push manifest failed: HTTP $($response.StatusCode) $($response.Content)"
}
return $response.Headers["Docker-Content-Digest"]
}
$parts = $Platform -split "/"
if ($parts.Count -ne 2) { throw "Platform must look like linux/amd64" }
$layoutRoot = (Resolve-Path $LayoutDir).Path
$manifest = Resolve-ImageManifest $layoutRoot $parts[1] $parts[0]
$allBlobs = @($manifest.Document.config.digest) + @($manifest.Document.layers | ForEach-Object { $_.digest })
$token = Get-BearerToken "repository:$Repository`:push,pull"
foreach ($blob in $allBlobs) { Push-Blob $layoutRoot $blob $token }
$manifestJson = Get-Content (Get-BlobPath $layoutRoot $manifest.Digest) -Raw
$digest = Push-Manifest $Tag $manifest.Document.mediaType $manifestJson $token
Write-Output "Manifest: $digest"
Write-Output ("Tag: {0}/{1}:{2}" -f $Registry, $Repository, $Tag)