Files
Havline/scripts/push-oci-manual.ps1

134 lines
5.7 KiB
PowerShell
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<#
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)