Day 7 把 Nexus 裝起來了,但到目前為止沒有任何 image 真的經過它。今天把這條路走通:
docker-proxy 從 Docker Hub 拉到 imagedocker-hosted,刪掉本地副本後再拉回ci namespace 的 Pod 安裝 npm 套件時,流量走 npm-group
擋在中間的是 Day 7 自己設的一個全域 outbound HTTP proxy(假設斷網環境的出入口)。那台 proxy 主機不存在,docker-proxy 一建起來就被 Nexus 打成 AUTO_BLOCKED_UNAVAILABLE,而錯誤訊息不會指向這裡。
Invoke-NexusApi 的寫法,/service/extdirect 的呼叫形態openshift-pipelines 的共用 Task/v2/ ping、manifest、blob| 先前的說法 | 本次實測 |
|---|---|
Day 7 記錄的 nonProxyHosts 是 ["*.cluster.local","localhost"] |
現況三筆,多了 registry.npmjs.org。建 npm-proxy 時被改過,Day 7 記的是當初值 |
Sonatype 有 HTTP Configuration API(/service/rest/v1/http) |
該端點 3.71.0 加入但標示 Pro 專屬。CE 上回 404,swagger 255 個端點裡無任何 */http* |
proxy repo 被 AUTO_BLOCKED_UNAVAILABLE 之後要等封鎖窗口到期 |
不必等。改動全域 HTTP 設定會重建 HTTP client,狀態立刻轉回 AVAILABLE |
| Nexus docker repo 要另開埠(port connector) | 3.83.0 起有 path-based routing,官方標為首選,port connector 標為 legacy。不動 Service、不開新 Route |
| CRC 的 ingress CA 就那一份 | 兩份,用途不能互換。1119 bytes(root only)給 git 與 CRI-O,2342 bytes(leaf+root)給 curl 與 podman |
| 要在 Windows 上裝 podman 才能驗 | CRC 節點本身有 podman 5.4.0,oc debug node 進去用 |
CRC 2.61.0+6eb443
OpenShift 4.21.14 / Kubernetes v1.34.6(單節點 crc)
Nexus Repository 3.93.0-06(COMMUNITY,nexus.loadAsOSS=true)
Chart stevehipwell/nexus3 5.23.0
節點 podman 5.4.0
用戶端:Windows 11 Pro + PowerShell 5.1(主機無 podman、無 docker)
下面所有結論只對這組版本成立。path-based routing 是 3.83.0 才加入的功能;coreui_HttpSettings 是未公開文件化的內部介面,換版本要重新核對欄位。
$OC = (Get-ChildItem "$env:USERPROFILE\.crc\cache" -Recurse -Filter "oc.exe" | Select-Object -First 1).FullName
$kc = "$env:USERPROFILE\.crc\machines\crc\kubeconfig"
$ns = "nexus-proxy"
$workDir = Join-Path "$env:USERPROFILE\nexus-run" (Get-Date -Format "yyyyMMdd-HHmmss")
New-Item -ItemType Directory -Path $workDir -Force | Out-Null
$NEXUS_HOST = & $OC --kubeconfig $kc get route nexus -n $ns -o jsonpath='{.spec.host}'
$nexusUrl = "https://$NEXUS_HOST"
$nexusPass = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String(
(& $OC --kubeconfig $kc get secret nexus-admin -n $ns -o jsonpath='{.data.password}')))
$b64cred = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes("admin:$nexusPass"))
# ExtDirect 的 read payload。第 2 節與收工檢查清單都會用到,放這裡才能單獨重跑
$readBody = [ordered]@{ action="coreui_HttpSettings"; method="read"; data=$null; type="rpc"; tid=1 } |
ConvertTo-Json -Depth 10 -Compress
Invoke-NexusApi沿用 Day 7。認證走 -K 設定檔不進指令列,body 走 --data-binary "@檔案",暫存檔無 BOM 且在 finally 刪除。
if (-not ('TrustAllCertsPolicy' -as [type])) {
Add-Type @"
using System.Net;
using System.Security.Cryptography.X509Certificates;
public class TrustAllCertsPolicy : ICertificatePolicy {
public bool CheckValidationResult(
ServicePoint sp, X509Certificate cert, WebRequest req, int problem) { return true; }
}
"@
}
[Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy
function Invoke-NexusApi {
param([string]$Url, [string]$Method = "GET", [string]$JsonBody = $null)
$cfg = Join-Path $env:TEMP ("nx-{0}.cfg" -f [guid]::NewGuid()); $tmp = $null
try {
[IO.File]::WriteAllText($cfg, "header = `"Authorization: Basic $b64cred`"",
(New-Object Text.UTF8Encoding $false))
$a = @("-K",$cfg,"-k","-s","--fail-with-body","-X",$Method,
"-H","Content-Type: application/json",$Url)
if ($JsonBody) {
$tmp = Join-Path $env:TEMP ("nx-{0}.json" -f [guid]::NewGuid())
[IO.File]::WriteAllText($tmp,$JsonBody,(New-Object Text.UTF8Encoding $false))
$a += @("--data-binary","@$tmp")
}
$raw = & curl.exe @a; $script:lastExit = $LASTEXITCODE; return ($raw -join "`n")
} finally {
if (Test-Path $cfg) { Remove-Item $cfg -Force }
if ($tmp -and (Test-Path $tmp)) { Remove-Item $tmp -Force }
}
}
Nexus 側的每一次呼叫都走 Route,不需要 oc exec。
後面每一步都會碰到,先列出來。
一、ConvertTo-Json 的 -Depth 預設是 2。 超過深度的結構被替換成型別名稱字串,不發警告,產出的 JSON 語法合法,ConvertFrom-Json 也不報錯。整條鏈上沒有任何環節會告訴你資料壞了。一律顯式指定 -Depth 10,並在送出前自檢:
"depth truncation = $($updBody -match 'OrderedDictionary|Hashtable')" # 必須是 False
二、傳給原生 exe 的參數,未逸出的雙引號會消失。 引號其實有進到原始命令列,是接收行程的 argv 解析把它們當成分隔符吃掉的。所以 --% 不能解決這件事,\" 逸出可以,而最穩的是寫檔後用 --data-binary "@file"。症狀是 curl: no URL specified!,或接收端回報 invalid character 'i' in literal true。
三、雙引號 here-string @"..."@ 會展開 $。 反斜線不是跳脫字元(跳脫是反引號),\$H 擋不住展開,變數被吃成空字串,送進 pod 的腳本變成 podman push /docker-hosted/smoke:v1。要注入值就用單引號 here-string 加佔位符:
$tpl = @'
printf '%s' '__PASS__' | podman login --username '__USER__' --password-stdin __HOST__
'@
$script = $tpl.Replace('__PASS__',$p).Replace('__USER__',$u).Replace('__HOST__',$NEXUS_HOST)
四、oc 的多行輸出是字串陣列。 [Text.Encoding]::UTF8.GetBytes($x) 會用空白接行——PEM 換行全沒了,而檔案大小看起來還是對的;$x.Length 回的是行數不是字元數;@($x).Count 在 ConvertFrom-Json 回空結果時會誤報成 1。拿 oc 輸出做長度或計數判斷之前,先確認型別。
要把腳本送進 pod 或節點執行,可行的形態是把整段 base64 化:
$sb64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($script))
& $OC --kubeconfig $kc debug node/crc -q -- chroot /host sh -c "echo $sb64 | base64 -d | sh"
base64 只有 A-Za-z0-9+/=,字串裡沒有雙引號,PowerShell 會因為有空白而替整串加上外層引號,argv 收到完整一段。不要用 oc cp 送檔——C:\... 的磁碟機冒號會被當成 pod:path 的分隔符,回 error: one of src or dest must be a local file specification。
Sonatype 文件記載的唯一途徑是 UI 的 Settings → System → HTTP。要用指令做,四條候選裡只有一條可用:
| 途徑 | 狀態 |
|---|---|
/service/rest/v1/http |
404。3.71.0 加入,release note 標示 Pro 專屬。這台 swagger 的 255 個端點裡沒有任何 */http* |
Script API POST /v1/script |
410 Gone。Groovy 引擎自 3.21.2 起預設停用,啟用需改 nexus.properties 並重啟 |
nexus.properties / JVM 參數 |
皆無 proxy 設定,組態只存在 config datastore |
/service/extdirect |
可用。UI 存檔時打的就是這支 |
回 404 而不是 403,代表端點未註冊而非權限不足。
coreui_HttpSettings 沒有相容性承諾,換版本要重驗欄位。
$beforeRaw = Invoke-NexusApi -Url "$nexusUrl/service/extdirect" -Method POST -JsonBody $readBody
[IO.File]::WriteAllText((Join-Path $workDir "before.json"), $beforeRaw,
(New-Object Text.UTF8Encoding $false))
($beforeRaw | ConvertFrom-Json).result.data | Format-List
userAgentSuffix : null
timeout : null
retries : null
httpEnabled : True
httpHost : proxy.internal.local
httpPort : 3128
httpsEnabled : True
httpsHost : proxy.internal.local
httpsPort : 3128
nonProxyHosts : {registry.npmjs.org, localhost, *.cluster.local}
這一份是唯一的回滾備份——CE 沒有其他方式匯出這組設定。
三件事一次看清楚:
nonProxyHosts 含 registry.npmjs.org。 這解釋了同一台上 npm-proxy 一直正常、maven-central 與 docker-proxy 全部 502 的原因:npm 走直連,另外兩個走一個解析不到的 proxy。
userAgentSuffix / timeout / retries 皆 null,所以 2.4 的 payload 不必帶它們。
Day 7 記的是兩筆,現況是三筆。 這組設定在建 npm-proxy 時被改過。
這是整段最容易靜默失敗的地方:
開關送
false時,其管轄的欄位一併回存null;開關未帶入時,管轄欄位不會寫入,而回應仍是success: true。
| 開關 | 管轄 |
|---|---|
httpEnabled |
httpHost / httpPort |
httpsEnabled |
httpsHost / httpsPort |
httpAuthEnabled |
httpAuthUsername / NtlmHost / NtlmDomain |
httpsAuthEnabled |
同上三個 |
| (無開關) | nonProxyHosts |
四個開關全部要帶,不能只帶兩個,也不能靠「不帶就等於關」。
read 回傳 20 個欄位,沒帶進 payload 的一律變 null,所以 2.2 讀到的非 null 欄位必須原樣帶回來。
$clearData = [ordered]@{
httpEnabled = $false; httpAuthEnabled = $false
httpsEnabled = $false; httpsAuthEnabled = $false
}
$updBody = [ordered]@{
action="coreui_HttpSettings"; method="update"; data=@($clearData); type="rpc"; tid=1
} | ConvertTo-Json -Depth 10 -Compress
"depth truncation = $($updBody -match 'OrderedDictionary|Hashtable')"
$updRaw = Invoke-NexusApi -Url "$nexusUrl/service/extdirect" -Method POST -JsonBody $updBody
$r = $updRaw | ConvertFrom-Json
"type = $($r.type) ; success = $($r.result.success)"
depth truncation = False
type = rpc ; success = True
data 必須用 @(...) 包成陣列,這是 ExtDirect 的形態。
判斷回應要看 type:型別錯誤時 HTTP 仍是 200,頂層沒有 success,type 是 exception,--fail-with-body 攔不到。
update 的回顯與寫入是同一次呼叫,不能拿來驗證自己。另發一次 read:
$afterRaw = Invoke-NexusApi -Url "$nexusUrl/service/extdirect" -Method POST -JsonBody $readBody
[IO.File]::WriteAllText((Join-Path $workDir "after.json"), $afterRaw,
(New-Object Text.UTF8Encoding $false))
$stored = ($afterRaw | ConvertFrom-Json).result.data
$bad = @('httpEnabled','httpHost','httpPort','httpsEnabled','httpsHost','httpsPort') |
Where-Object { $v = $stored.$_; -not (($null -eq $v) -or ($v -is [bool] -and -not $v)) }
if ($bad) { throw "未清除:$($bad -join ', ')" }
"全域 proxy 已關閉"
nonProxyHosts 沒帶入,回存為 null。proxy 關掉之後例外清單沒有意義,原值留在 before.json。
maven-central 當探針驗證這一步不必動到 docker。maven-central 是 Nexus 內建的 proxy repo,此刻已經存在,先前也一樣是 502(日誌同樣是 UnknownHostException: proxy.internal.local),但它沒有 realm、CA、帳號那一串前置:
Invoke-NexusApi -Url "$nexusUrl/repository/maven-central/org/slf4j/slf4j-api/2.0.13/slf4j-api-2.0.13.pom" | Out-Null
"maven-central exit=$lastExit" # 0
它通了就代表 proxy 解決了,docker 之後再出問題就是 docker 自己的事。這個切分讓後面每一節都能單獨判斷成敗。
給先建 repo 才撞到這件事的人。 本文把清 proxy 排在建 repo 之前,照這個順序做根本不會看到封鎖狀態。若你是先建了 docker-proxy、拿到 502 才找到這裡,它此刻是 AUTO_BLOCKED_UNAVAILABLE,日誌那行帶 until <timestamp>,直覺會以為要等窗口過去——實測不用:
HttpClientFacetImpl - Repository status for maven-central changed from READY to AVAILABLE
HttpClientFacetImpl - Repository status for docker-proxy changed from READY to AVAILABLE
改動全域 HTTP 設定本身就重建了 HTTP client 並清掉封鎖狀態。
docker 客戶端(含 podman)走的是 Bearer Token 流程,沒有這個 realm 連 login 都過不了。這台的 anonymous 是關的,所以一定要 login。
現在的 active realms 只有 NexusAuthenticatingRealm,要加進去不是取代:
Invoke-NexusApi -Url "$nexusUrl/service/rest/v1/security/realms/active" `
-Method PUT -JsonBody '["NexusAuthenticatingRealm","DockerToken"]' | Out-Null
Invoke-NexusApi -Url "$nexusUrl/service/rest/v1/security/realms/active"
["NexusAuthenticatingRealm","DockerToken"]
Docker 客戶端不允許在 registry 位址裡帶 repository context path,所以要從三種 routing 擇一:port connector、subdomain、path。3.83.0 起 path-based routing 是官方首選,port connector 被標為 legacy。
URL 形態是:
nexus.example/repository-name/namespace/image:tag
不含 /repository/。這一段寫錯會直接 404。
選它的代價是省下來的:不必改 StatefulSet 的 containerPort、不必改 Service 的 ports、不必開新 Route,Helm values 完全不用碰。
限制是官方明說同時只能用一種 routing,混用不受支援;之後要改回 port connector 得建新的 repository。
只建 proxy 和 hosted,不建 group。CE 的 feature matrix 明列 Deployment to Group Repositories 為 Pro 專屬,group 只能拉不能推,這階段用不到。
$proxyBody = [ordered]@{
name = "docker-proxy"
online = $true
storage = [ordered]@{ blobStoreName="default"; strictContentTypeValidation=$true }
proxy = [ordered]@{ remoteUrl="https://registry-1.docker.io"; contentMaxAge=1440; metadataMaxAge=1440 }
negativeCache = [ordered]@{ enabled=$true; timeToLive=1440 }
httpClient = [ordered]@{ blocked=$false; autoBlock=$true }
docker = [ordered]@{ v1Enabled=$false; forceBasicAuth=$true; pathEnabled=$true }
dockerProxy = [ordered]@{ indexType="HUB" }
} | ConvertTo-Json -Depth 10 -Compress
Invoke-NexusApi -Url "$nexusUrl/service/rest/v1/repositories/docker/proxy" -Method POST -JsonBody $proxyBody | Out-Null
"docker-proxy exit=$lastExit" # 0
$hostedBody = [ordered]@{
name = "docker-hosted"
online = $true
storage = [ordered]@{ blobStoreName="default"; strictContentTypeValidation=$true; writePolicy="ALLOW" }
docker = [ordered]@{ v1Enabled=$false; forceBasicAuth=$true; pathEnabled=$true }
} | ConvertTo-Json -Depth 10 -Compress
Invoke-NexusApi -Url "$nexusUrl/service/rest/v1/repositories/docker/hosted" -Method POST -JsonBody $hostedBody | Out-Null
"docker-hosted exit=$lastExit" # 0
這兩支成功時回 201 但 body 是空的,Invoke-NexusApi 只回傳 body,所以直接印會什麼都看不到,成功和失敗長得一樣。要看 $lastExit。
repository 總數從 9 變 11。回讀確認 pathEnabled 落地、httpPort / httpsPort / subdomain 皆空:
Invoke-NexusApi -Url "$nexusUrl/service/rest/v1/repositories/docker/proxy/docker-proxy" |
ConvertFrom-Json | Select-Object -ExpandProperty docker
pathEnabled 有沒有生效,用 curl 打 Docker Registry v2 的路徑就看得出來,不必先裝好 CA 和帳號:
| 路徑 | 狀態 |
|---|---|
/v2/ |
401 |
/v2/docker-proxy/library/alpine/manifests/3.20 |
401 |
/v2/library/alpine/manifests/3.20 |
404 |
/docker-proxy/v2/ |
404 |
401 代表路徑被認得、只是要認證;404 代表路徑不存在。第二列是 path routing 的形態,它回 401 就是生效了。
foreach ($p in '/v2/',
'/v2/docker-proxy/library/alpine/manifests/3.20',
'/v2/library/alpine/manifests/3.20',
'/docker-proxy/v2/') {
"{0,-50} {1}" -f $p, (curl.exe -k -s -o NUL -w "%{http_code}" "$nexusUrl$p")
}
這裡刻意不帶認證,不要用 Invoke-NexusApi。 它會帶上 Authorization header,帶了之後第二列就不是 401,而 401 與 404 的差別正是這張表唯一的訊號——認證過了反而看不出 path routing 有沒有生效。
push 需要寫入權,但不要直接用 nx-admin。建 repository 時 Nexus 會自動產生對應的 privileges,不必自己建:
nx-repository-view-docker-docker-{hosted,proxy}-{*,add,browse,delete,edit,read}
nx-repository-admin-docker-docker-{hosted,proxy}-{*,browse,delete,edit,read}
$roleBody = [ordered]@{
id="ci-docker-push"; name="ci-docker-push"
description="CI push to docker-hosted, pull through docker-proxy"
privileges=@(
"nx-repository-view-docker-docker-hosted-browse",
"nx-repository-view-docker-docker-hosted-read",
"nx-repository-view-docker-docker-hosted-add",
"nx-repository-view-docker-docker-hosted-edit",
"nx-repository-view-docker-docker-proxy-browse",
"nx-repository-view-docker-docker-proxy-read"
)
roles=@()
} | ConvertTo-Json -Depth 10 -Compress
Invoke-NexusApi -Url "$nexusUrl/service/rest/v1/security/roles" -Method POST -JsonBody $roleBody
$dpw = [guid]::NewGuid().ToString()
$userBody = [ordered]@{
userId="ci-docker"; firstName="CI"; lastName="Docker"
emailAddress="ci-docker@example.local"
password=$dpw; status="active"; roles=@("ci-docker-push")
} | ConvertTo-Json -Depth 10 -Compress
Invoke-NexusApi -Url "$nexusUrl/service/rest/v1/security/users" -Method POST -JsonBody $userBody
& $OC --kubeconfig $kc create secret generic ci-docker-credentials -n $ns `
--from-literal=username=ci-docker --from-literal=password="$dpw"
這兩支和 4.2 不一樣,成功時回 200 且帶完整 JSON body,直接印就看得到結果,不必再查 $lastExit。
docker-proxy 的讀取權不能漏。 拉取要讀 proxy、推送要寫 hosted,是兩組權限;只給 hosted 的話第 7 節會 401。proxy 只給讀,不給寫。
密碼建完就無處可查,比照 ci-npm-credentials 的慣例存成 Secret。
主機沒有 podman 也沒有 docker,crc podman-env 明說 podman-remote 已不再隨附。但節點有:
& $OC --kubeconfig $kc debug node/crc -- chroot /host podman --version
podman version 5.4.0
用節點端還有一個好處:它比 Windows 更接近之後 buildah 在叢集內跑的環境。
注意節點的 podman 與 CRI-O 共用同一個 storage(/var/lib/containers/storage),podman images 看得到 CRI-O 拉過的 operator index。做實驗時要留意磁碟。
Docker 要求 HTTPS,Nexus 不支援 --insecure-registry,所以憑證信任是硬需求。CRC 的 ingress CA 有兩份不同內容的檔案,用途不能互換:
| 使用者 | 位置 | 檔案 |
|---|---|---|
| 主機 curl / podman | %USERPROFILE%\.config\containers\certs.d\<host>\ca.crt |
2342(leaf+root) |
| 節點 podman | /etc/containers/certs.d/<host>/ca.crt |
2342 |
| 節點 CRI-O | openshift-config/registry-certs → image.config.openshift.io/cluster |
1119(root only) |
| git | %USERPROFILE%\certs\crc-ingress-ca.crt |
1119 |
挑錯檔的症狀是 curl 回 000、podman 回 x509: certificate signed by unknown authority。
certs.d 的目錄名必須與 image reference 裡的用法完全一致——image ref 不帶 port,目錄名就不能寫成 host:443。
從 configmap 取,不要用家目錄裡來路不明的檔案:
$lines = & $OC --kubeconfig $kc get configmap default-ingress-cert -n openshift-config-managed `
-o jsonpath='{.data.ca-bundle\.crt}'
$ca = ($lines -join "`n") # 沒有這個 join,PEM 換行會被空白取代
"chars = $($ca.Length)" # 2342
$cab64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($ca))
$tpl = @'
set -e
D=/etc/containers/certs.d/__HOST__
mkdir -p $D
echo '__CA__' | base64 -d > $D/ca.crt
openssl x509 -in $D/ca.crt -noout -subject
curl -s -o /dev/null -w 'HTTP=%{http_code}\n' --max-time 10 --cacert $D/ca.crt \
https://__HOST__/service/rest/v1/status
'@
$script = $tpl.Replace('__HOST__',$NEXUS_HOST).Replace('__CA__',$cab64)
$sb64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($script))
& $OC --kubeconfig $kc debug node/crc -q -- chroot /host sh -c "echo $sb64 | base64 -d | sh"
subject=CN=*.apps-crc.testing
HTTP=200
podman login nexus-nexus-proxy.apps-crc.testing ← 不帶 path
podman pull nexus-nexus-proxy.apps-crc.testing/docker-proxy/library/alpine:3.20 ← 帶 path
login 的請求打的是 registry 根目錄的 /v2/,帶 repository path 會認證失敗。這兩行位址不一樣,是這一段最容易搞混的地方。
$u = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String(
(& $OC --kubeconfig $kc get secret ci-docker-credentials -n $ns -o jsonpath='{.data.username}')))
$p = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String(
(& $OC --kubeconfig $kc get secret ci-docker-credentials -n $ns -o jsonpath='{.data.password}')))
$tpl = @'
set -e
H=__HOST__
printf '%s' '__PASS__' | podman login --username '__USER__' --password-stdin $H
podman pull $H/docker-proxy/library/alpine:3.20
podman run --rm $H/docker-proxy/library/alpine:3.20 cat /etc/alpine-release
podman pull registry.access.redhat.com/ubi9-micro:latest
podman tag registry.access.redhat.com/ubi9-micro:latest $H/docker-hosted/smoke:v1
podman push --remove-signatures $H/docker-hosted/smoke:v1
podman rmi $H/docker-hosted/smoke:v1
podman pull $H/docker-hosted/smoke:v1
'@
$script = $tpl.Replace('__HOST__',$NEXUS_HOST).Replace('__PASS__',$p).Replace('__USER__',$u)
$sb64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($script))
& $OC --kubeconfig $kc debug node/crc -q -- chroot /host sh -c "echo $sb64 | base64 -d | sh"
Login Succeeded! 這一行同時證明四件事一起生效:DockerToken realm、path routing、Route 的 edge TLS、ci-docker 的權限。
拉下來的 alpine 跑一下確認不是空殼:
3.20.10
--remove-signaturesRed Hat 官方 image 帶簽章,轉存到 Nexus 需要改變層的表示方式,不加這個旗標會失敗:
Error: Copying this image would require changing layer representation,
which we cannot do: "Would invalidate signatures"
之後 buildah 推自建 image 不會遇到(沒有簽章),但拿官方 image 做搬運測試一定會撞到。
加了之後 blob digest 會變,config digest 不變(d09203066f26)——用 config digest 判斷內容一致性。
先用 alpine:3.20(8.1 MB)與 ubi9-micro(23.7 MB),失敗時重試成本低。
Invoke-NexusApi -Url "$nexusUrl/service/rest/v1/components?repository=docker-proxy" |
ConvertFrom-Json | Select-Object -ExpandProperty items | Select-Object name, version
Invoke-NexusApi -Url "$nexusUrl/service/rest/v1/components?repository=docker-hosted" |
ConvertFrom-Json | Select-Object -ExpandProperty items | Select-Object name, version
library/alpine 3.20
smoke v1
Pod 只能掛載自己 namespace 的 Secret,這是 Kubernetes 的硬限制,跨 namespace 給再多 RBAC 也沒用。憑證要複製過去:
$u = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String(
(& $OC --kubeconfig $kc get secret ci-npm-credentials -n $ns -o jsonpath='{.data.username}')))
$p = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String(
(& $OC --kubeconfig $kc get secret ci-npm-credentials -n $ns -o jsonpath='{.data.password}')))
& $OC --kubeconfig $kc create secret generic ci-npm-credentials -n ci `
--from-literal=username="$u" --from-literal=password="$p"
.npmrc 在 Pod 執行期產生,密鑰從環境變數來,不進 YAML:
AUTH=$(printf '%s:%s' "$NPM_USER" "$NPM_PASS" | base64 -w0)
cat > .npmrc <<EOF
registry=http://nexus-nexus3.nexus-proxy.svc.cluster.local:8081/repository/npm-group/
always-auth=true
//nexus-nexus3.nexus-proxy.svc.cluster.local:8081/repository/npm-group/:_auth=${AUTH}
EOF
三件事:
npm 走的是標準 /repository/ 路徑,和 docker 的 path routing 不同。path routing 只影響 docker format,兩種形態在同一台 Nexus 上並存是正常的。
不需要開 NpmToken realm。 Sonatype 文件明列,無法使用 realm + login 流程時可改用 _auth 的 basic auth,在只有 NexusAuthenticatingRealm 的情況下就夠用。npm view express 回得出版本就是通了。
叢集內走 Service 的 8081(HTTP),不走 Route。 npm 不要求 HTTPS,docker 才要求。走 Service 可以避開 Pod 對 *.apps-crc.testing 的解析問題。代價是同一個 Nexus 在節點端與叢集內是兩個不同位址。
測試 Pod 的 image 要用 registry.access.redhat.com/ubi9/nodejs-22(免認證),不要用 image-registry.openshift-image-registry.svc:5000/openshift/nodejs:22-ubi9——openshift 的 ImageStream 指向外部 registry,映像本身不在內部 registry 裡,Pod 會卡在 ContainerCreating 而事件只有 Pulling、沒有錯誤訊息。
這裡跑的是 npm install 而不是 npm ci。 這個 repo 的 package.json 與 package-lock.json 不同步(lock 裡是 yaml@1.10.3,依賴樹要 2.9.0),npm ci 會在兩秒內拒絕執行,連 registry 都不碰。那是原始碼的問題、與 Nexus 無關,但錯誤訊息(can only install packages when your package.json and package-lock.json are in sync)看起來像 npm 設定壞掉,很容易往 registry 方向查。
一次完整安裝(Nx 23.1.1 + Angular 22.0.4,package-lock.json 一百萬 bytes):
added 1720 packages in 3m(200s) node_modules 676 MB
blobstore 12.6 MB → 665.9 MB +2,496 blobs / +653 MB
一次 Angular 安裝就吃掉 653 MB。這台所有 PV 共用同一顆 180G 的盤,規劃時要算進去。
# 1. 全域 proxy 已關(重讀,不要用 2.4 的 $afterRaw——清單的用途是隔一段時間回來確認)
(Invoke-NexusApi -Url "$nexusUrl/service/extdirect" -Method POST -JsonBody $readBody |
ConvertFrom-Json).result.data.httpHost # null
# 2. realm
Invoke-NexusApi -Url "$nexusUrl/service/rest/v1/security/realms/active"
# ["NexusAuthenticatingRealm","DockerToken"]
# 3. 兩個 docker repo 都在,pathEnabled 落地
Invoke-NexusApi -Url "$nexusUrl/service/rest/v1/repositories/docker/proxy/docker-proxy" |
ConvertFrom-Json | Select-Object -ExpandProperty docker
# pathEnabled = True
# 4. 上游真的通了(與 docker 無關的獨立探針)
Invoke-NexusApi -Url "$nexusUrl/repository/maven-central/org/slf4j/slf4j-api/2.0.13/slf4j-api-2.0.13.pom" | Out-Null
"maven-central exit=$lastExit" # 0
# 5. 兩個 repo 都有內容
Invoke-NexusApi -Url "$nexusUrl/service/rest/v1/components?repository=docker-proxy"
Invoke-NexusApi -Url "$nexusUrl/service/rest/v1/components?repository=docker-hosted"
# 6. 節點的 CA 是 2342
& $OC --kubeconfig $kc debug node/crc -q -- chroot /host `
wc -c "/etc/containers/certs.d/$NEXUS_HOST/ca.crt"
# 7. before.json 還在(唯一的回滾依據)
Test-Path (Join-Path $workDir "before.json")
| 症狀 | 先看 | 不要先做 |
|---|---|---|
502 {"code":"UNKNOWN"} |
第 2 節的全域 proxy。打 maven-central 對照 |
不要懷疑 repo 設定或 path routing |
login 回 x509 |
第 6 節的 CA,是不是拿成 1119 bytes | 不要加 --tls-verify=false |
login 回 401 |
第 3 節的 realm 有沒有生效 | 不要先懷疑帳號密碼 |
login 過了但 pull 404 |
URL 有沒有多寫 /repository/ |
不要改 repository 設定 |
pull 回 401 |
login 位址有沒有誤帶 path;role 有沒有給 docker-proxy 讀取權 |
同上 |
push 回 "Would invalidate signatures" |
加 --remove-signatures |
不要改 repository 設定 |
| npm 401 | .npmrc 的 _auth key 是不是完整路徑 |
不要先開 NpmToken realm |
npm ci 兩秒就失敗 |
package-lock.json 與 package.json 是否同步 |
不要懷疑 registry |
Pod 卡 ContainerCreating、事件只有 Pulling |
image 是不是指向內部 registry 但映像不在那裡 | 不要先查網路 |
第 2 節修改了既有設定,其餘都是加法。
第 2 節 把 before.json 的 result.data 20 個欄位原樣組成 update payload 送回
四個 *Enabled 必須全部帶入
第 3 節 PUT /v1/security/realms/active ["NexusAuthenticatingRealm"]
第 4 節 DELETE /v1/repositories/docker-proxy、docker-hosted
第 5 節 DELETE /v1/security/users/ci-docker、roles/ci-docker-push
oc delete secret ci-docker-credentials -n nexus-proxy
第 6 節 刪節點的 /etc/containers/certs.d/<host>/
第 7 節 節點 podman logout、podman rmi
第 8 節 oc delete secret ci-npm-credentials -n ci
沒有動到 Helm release、StatefulSet、Service、Route、nexus.properties、image.config.openshift.io/cluster、節點 registries.conf、Day 14 的觸發鏈路。這是選 path routing 換來的。
image 側到這裡通了:拉得進來、推得上去、拉得回去,全程沒有重啟、沒有停機、沒有動 UI。
三個值得帶走的。
設定會漂移,文件記的是當初值。 Day 7 記的 nonProxyHosts 是兩筆,現況三筆。拿舊文件解釋新現象之前,先確認那份文件描述的還是不是現在的狀態。這和「官方文件說有這個端點」對上「這台回 404」是同一件事的兩面:文件的版別、版本、時點,三個都要對得上才能引用。
查詢回空不等於沒有東西。 先前盤點時打 /service/rest/v1/system/http-proxy 拿到空 body,記成「沒有設任何 HTTP proxy」。那個端點根本不存在,空 body 是 404 的空 body。一個既存設定因此多躲了一輪,直到 502 才浮出來。
Nexus 的 JVM heap 已經到 80%。
MemoryMonitor - High heap usage: 80% (827.6mb/1.0gb), sustained=30000ms
chart 預設是 -Xms1024m -Xmx1024m,而目前只跑過一次 npm 安裝和兩張小 image。下一步把 buildah 接進 Pipeline 推應用 image 時,這是第一個會出事的地方,而且它的失敗形態會偽裝成網路問題——push 逾時、502、連線中斷,讓人先去查 Route 和 TLS。接 buildah 之前先調它。
還沒解決的三項一併列出:叢集內走 Service 8081 的 path routing 未驗(接 buildah 時會變成阻塞)、Route 對大型 blob 上傳未驗(目前推過最大 23.7 MB)、CRC VM 時鐘偏移 8 小時(oc describe 的 event 年齡全部偏移,排查時會誤導)。
Sonatype
| 文件 | 用到的結論 |
|---|---|
| Docker Registry | path-based routing 為 3.83.0 新增、標示 preferred,port connector 標示 legacy;URL 不含 /repository/;同時只能用一種 routing;不支援 --insecure-registry |
| Docker Authentication | Docker Bearer Token Realm 為必要條件;login 位址不可帶 repository path |
| Self-Hosted Feature Matrix | Docker、npm 在 CE 可用;Deployment to Group Repositories 為 Pro 專屬 |
| npm Security | //<host>/<path>/:_auth=<base64> 的 basic auth |
| HTTP Configuration API | /service/rest/v1/http 的規格。列在這裡是記錄它存在但 CE 不適用 |
| HTTP Request and Proxy Settings | 全域 outbound proxy 的欄位語意;官方途徑為 UI |
| Script API | Groovy 引擎自 3.21.2 起預設停用 |
| Realms | active realms 的清單語意 |
| Repositories API | docker proxy / hosted 的 POST body |
| Security Management API | realms / roles / users 端點 |
| 3.71.0 Release Notes | HTTP Configuration API 的加入版本,標示 Pro |
| 3.83.0 Release Notes | path-based routing 的加入版本 |
容器與 OpenShift
| 文件 | 用到的結論 |
|---|---|
containers-certs.d(5) |
<certs.d>/<host[:port]>/ca.crt 的目錄結構;port 部分須與 image reference 一致 |
| Configuring Routes(OCP 4.10) | haproxy.router.openshift.io/timeout 的用法與時間單位。這是 4.10 的文件,本文環境是 4.21,真要調之前該對一次對應版本 |
本系列
| 篇 | 用到的結論 |
|---|---|
| Day 7:部署 Nexus 3 並配置 Outbound Proxy | Invoke-NexusApi 的寫法;coreui_HttpSettings 的 read / update payload;開關管轄規則;nonProxyHosts 須為陣列;型別錯誤時 HTTP 仍是 200;寫入後必須另發獨立 read。本文要清掉的那個 proxy 就是這篇設的 |
| Day 12:跨 Namespace 引用 Task(Cluster Resolver) | openshift-pipelines 的共用 Task,接 buildah 的前置 |
| Day 14:Tekton Triggers 的授權規範與 CEL 的邊界 | 不為跨 namespace 讀取而綁 -clusterroles;ci 既有的觸發鏈路 |