目的:確認 CRC 已啟動,且 $env:KUBECONFIG 為空。第 5 步的 oc login 會寫入該變數指向的檔案,非空時會覆寫 system:admin 憑證。
crc status
"KUBECONFIG = [$env:KUBECONFIG]"
CRC VM: RunningKUBECONFIG = []
$env:KUBECONFIG 非空就先清掉。
目的:集中定義後續各步共用的名稱與路徑。$workDir 帶時間戳,避免與前次執行的產物混在同一目錄。
$OC = (Get-ChildItem "$env:USERPROFILE\.crc\cache" -Recurse -Filter "oc.exe" | Select-Object -First 1).FullName
$kubeconfig = "$env:USERPROFILE\.crc\machines\crc\kubeconfig"
$ns = "nexus-proxy"
$deployerSa = "nexus-deployer"
$nexusSa = "nexus-nexus3"
$chartVer = "5.23.0"
$apiServer = "https://api.crc.testing:6443"
$workDir = Join-Path "$env:USERPROFILE\nexus-run" (Get-Date -Format "yyyyMMdd-HHmmss")
New-Item -ItemType Directory -Path $workDir -Force | Out-Null
"workDir = $workDir"
目的:確認起點身分為 system:admin。第 2–4 步需要叢集層級權限。
& $OC --kubeconfig $kubeconfig whoami
system:admin
目的:建立本次部署的 namespace。此為叢集層級操作。
& $OC --kubeconfig $kubeconfig create namespace $ns
namespace/nexus-proxy created
目的:建立限縮於單一 namespace 的部署身分,取代全程使用 system:admin。edit 涵蓋後續需要的 Secret、StatefulSet、Service、Route 與 Helm release 記錄。
& $OC --kubeconfig $kubeconfig create sa $deployerSa -n $ns
& $OC --kubeconfig $kubeconfig adm policy add-role-to-user edit -z $deployerSa -n $ns
serviceaccount/nexus-deployer createdclusterrole.rbac.authorization.k8s.io/edit added: "nexus-deployer"
目的:授權給 Chart 稍後建立的 SA。Nexus 容器以 UID 200 執行,預設的 restricted-v3 不允許。RoleBinding 的 subjects 不做存在性檢查,此步不需等 SA 存在。
& $OC --kubeconfig $kubeconfig adm policy add-scc-to-user anyuid -z $nexusSa -n $ns
$b = & $OC --kubeconfig $kubeconfig get rolebinding -n $ns -o json | ConvertFrom-Json
$b.items | Where-Object { $_.roleRef.name -like "system:openshift:scc:*" } |
Select-Object @{n='role';e={$_.roleRef.name}}, @{n='subject';e={($_.subjects | ForEach-Object { $_.name }) -join ','}}
clusterrole.rbac.authorization.k8s.io/system:openshift:scc:anyuid added: "nexus-nexus3"role subject ---- ------- system:openshift:scc:anyuid nexus-nexus3
目的:取得部署帳號的 token 並登入。第 6–11 步以該身分執行。
$token = & $OC --kubeconfig $kubeconfig create token $deployerSa -n $ns --duration=8h
& $OC login --token=$token --server=$apiServer --insecure-skip-tls-verify=true
& $OC whoami
Logged into "https://api.crc.testing:6443" as "system:serviceaccount:nexus-proxy:nexus-deployer"system:serviceaccount:nexus-proxy:nexus-deployer
以下到第 11 步不帶 --kubeconfig。
目的:確認部署帳號足以完成第 7–11 步,且未取得叢集層級權限。以權限查詢驗證,不以第 3 步的輸出訊息驗證。
& $OC auth can-i create statefulset -n $ns
& $OC auth can-i create route -n $ns
& $OC auth can-i create namespace
yesyesWarning: resource 'namespaces' is not namespace scopedno
整段退出碼是 1。
目的:確認本版 Chart 的欄位路徑,以及 storageClass 名稱。路徑錯誤時 Helm 不報錯、值被忽略;storageClass 名稱錯誤時 PVC 停在 Pending,而該狀態要到第 9 步之後才顯現。
helm repo add stevehipwell https://stevehipwell.github.io/helm-charts/
helm repo update
$refPath = Join-Path $workDir "chart-values-ref.yaml"
$ref = helm show values stevehipwell/nexus3 --version $chartVer | Out-String
[System.IO.File]::WriteAllText($refPath, $ref, (New-Object System.Text.UTF8Encoding $false))
Get-Content $refPath | Select-String -Pattern "seccompProfile" -SimpleMatch
& $OC get storageclass -o custom-columns=NAME:.metadata.name,DEFAULT:.metadata.annotations."storageclass\.kubernetes\.io/is-default-class",RECLAIM:.reclaimPolicy
seccompProfile命中兩次(用podSecurityContext底下那個,約 157 行;另一個在config.job底下)crc-csi-hostpath-provisioner true Retain
目的:寫入 values 與 admin 密碼。Chart 只引用 Secret 不生成,需先存在。寫檔用 WriteAllText 以避免 BOM,values.yaml 要交給 YAML parser。
$values = @"
persistence:
enabled: true
storageClass: crc-csi-hostpath-provisioner
size: 10Gi
rootPassword:
secret: nexus-admin
key: password
podSecurityContext:
seccompProfile: null
"@
$valuesPath = Join-Path $workDir "values.yaml"
[System.IO.File]::WriteAllText($valuesPath, $values, (New-Object System.Text.UTF8Encoding $false))
$b = [System.IO.File]::ReadAllBytes($valuesPath)
"bytes = $($b.Length); first4 = $(($b[0..3] | ForEach-Object { $_.ToString('X2') }) -join ' ')"
bytes = 181; first4 = 70 65 72 73
$nexusPass = [System.Guid]::NewGuid().ToString()
$pwB64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($nexusPass))
@"
apiVersion: v1
kind: Secret
metadata:
name: nexus-admin
namespace: $ns
type: Opaque
data:
password: $pwB64
"@ | & $OC apply -f -
$back = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String(
(& $OC get secret nexus-admin -n $ns -o jsonpath='{.data.password}')))
"round-trip match = $($back -eq $nexusPass)"
secret/nexus-admin createdround-trip match = True
目的:部署 Nexus 並等待容器就緒。就緒判定看 containerStatuses[].ready,不看 status.phase。
$env:KUBECONFIG = ""
helm install nexus stevehipwell/nexus3 -n $ns -f $valuesPath --version $chartVer
STATUS: deployed,chart 5.23.0 / app 3.93.0
$deadline = (Get-Date).AddMinutes(8)
$last = ""
while ($true) {
$raw = & $OC get pod -n $ns -o json 2>$null
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($raw)) {
"[{0:HH:mm:ss}] (查詢失敗,重試)" -f (Get-Date)
} else {
$pods = ($raw | ConvertFrom-Json).items
if ($pods.Count -eq 0) {
$snap = "(namespace 內尚無 Pod)"; $allReady = $false
} else {
$snap = ($pods | ForEach-Object {
$r = @($_.status.containerStatuses | Where-Object { $_.ready }).Count
$t = @($_.status.containerStatuses).Count
"$($_.metadata.name) $r/$t $($_.status.phase)"
}) -join " | "
$allReady = -not ($pods | Where-Object {
(-not $_.status.containerStatuses) -or
($_.status.containerStatuses | Where-Object { -not $_.ready })
})
}
if ($snap -ne $last) { "[{0:HH:mm:ss}] {1}" -f (Get-Date), $snap; $last = $snap }
if ($allReady) { "ALL READY"; break }
}
if ((Get-Date) -gt $deadline) { throw "等待 Pod 就緒逾時(8 分鐘)" }
Start-Sleep -Seconds 10
}
Pending→0/1 Running→1/1 Running→ALL READY,約 1–2 分鐘
目的:確認 PVC、SCC、執行身分、密碼來源四項與預期一致。
& $OC get pvc -n $ns
& $OC get pod nexus-nexus3-0 -n $ns -o jsonpath='{.metadata.annotations.openshift\.io/scc}'
& $OC exec nexus-nexus3-0 -n $ns -c nexus3 -- id
$ss = & $OC get statefulset nexus-nexus3 -n $ns -o json | ConvertFrom-Json
($ss.spec.template.spec.containers | Where-Object { $_.name -eq 'nexus3' }).env |
Where-Object { $_.name -like 'NEXUS_SECURITY_*' } |
ForEach-Object {
if ($_.value) { "$($_.name)=$($_.value)" }
else { "$($_.name)=<secretKeyRef: $($_.valueFrom.secretKeyRef.name)/$($_.valueFrom.secretKeyRef.key)>" }
}
PVC
STATUS是Bound(CAPACITY顯示的是節點磁碟容量,不是 10Gi)anyuiduid=200(nexus) gid=200(nexus) groups=200(nexus)NEXUS_SECURITY_RANDOMPASSWORD=falseNEXUS_SECURITY_INITIAL_PASSWORD=<secretKeyRef: nexus-admin/password>
目的:建立對外入口,供工作站從叢集外呼叫 API。host 從 Route 讀出,不自行拼接。
& $OC create route edge nexus --service=nexus-nexus3 --port=8081 `
--insecure-policy=Redirect -n $ns
$nexusUrl = "https://" + (& $OC get route nexus -n $ns -o jsonpath='{.spec.host}')
$nexusUrl
route.route.openshift.io/nexus createdhttps://nexus-nexus-proxy.apps-crc.testing
目的:設定自簽憑證的處理方式,並從 Secret 取回密碼組出認證字串。密碼重新讀取,不沿用第 8 步的變數。
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; }
}
"@
}
[System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy
$nexusPass = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String(
(& $OC get secret nexus-admin -n $ns -o jsonpath='{.data.password}')))
$b64cred = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes("admin:$nexusPass"))
"cred length = $($b64cred.Length)"
cred length = 56
目的:定義 EULA 與 ExtDirect 共用的呼叫函式。認證走設定檔不進指令列,暫存檔無 BOM 且刪除於 finally。
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 {
[System.IO.File]::WriteAllText($cfg, "header = `"Authorization: Basic $b64cred`"",
(New-Object System.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())
[System.IO.File]::WriteAllText($tmp, $JsonBody, (New-Object System.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 可接受寫入請求。容器就緒不等於後端可寫,readinessProbe 只涵蓋讀取。
$deadline = (Get-Date).AddMinutes(10)
$lastError = '(尚未探測)'
$probes = 0
while ($true) {
$probes++
try {
$r = Invoke-WebRequest -Uri "$nexusUrl/service/rest/v1/status/writable" `
-UseBasicParsing -TimeoutSec 10 -ErrorAction Stop
if ($r.StatusCode -eq 200) { break }
$lastError = "HTTP $($r.StatusCode)"
} catch {
$lastError = $_.Exception.Message
if ($_.Exception.InnerException) { $lastError += " / 內層:$($_.Exception.InnerException.Message)" }
}
if ((Get-Date) -gt $deadline) { throw "等待就緒逾時。最後失敗:$lastError" }
Start-Sleep -Seconds 5
}
"Nexus 可寫 (probes=$probes)"
Nexus 可寫 (probes=1)
目的:取得原始 disclaimer 字串,供第 16 步原封不動送回。後端要求逐字元相符,手寫會回 500。
$eulaRaw = Invoke-NexusApi -Url "$nexusUrl/service/rest/v1/system/eula"
"exit=$lastExit"
$eula = $eulaRaw | ConvertFrom-Json
"簽署前 accepted = $($eula.accepted); disclaimer 長度 = $($eula.disclaimer.Length)"
exit=0簽署前 accepted = False; disclaimer 長度 = 348
目的:送出 EULA 簽署請求。
$eulaBody = [ordered]@{ disclaimer = $eula.disclaimer; accepted = $true } |
ConvertTo-Json -Depth 10 -Compress
$post = Invoke-NexusApi -Url "$nexusUrl/service/rest/v1/system/eula" -Method POST -JsonBody $eulaBody
"exit=$lastExit; body=<$post>"
exit=0; body=<>
目的:以獨立的一次 GET 確認 accepted 已切換。POST 回 204 且無 body,只表示請求被接受。
$after = (Invoke-NexusApi -Url "$nexusUrl/service/rest/v1/system/eula" | ConvertFrom-Json)
"簽署後 accepted = $($after.accepted)"
簽署後 accepted = True
目的:記錄設定前的狀態,作為第 21 步的對照基準。
$readBody = [ordered]@{
action="coreui_HttpSettings"; method="read"; data=$null; type="rpc"; tid=1
} | ConvertTo-Json -Depth 10 -Compress
$beforeRaw = Invoke-NexusApi -Url "$nexusUrl/service/extdirect" -Method POST -JsonBody $readBody
"exit=$lastExit"
$beforeRaw
[System.IO.File]::WriteAllText((Join-Path $workDir "before.json"), $beforeRaw,
(New-Object System.Text.UTF8Encoding $false))
exit=0,httpEnabled/httpHost/httpPort皆null,檔案 530 bytes
目的:組出 update 的 payload 並確認序列化結果。四個 *Enabled 全部帶入——開關未帶入時,其管轄欄位不會寫入,而回應仍為 success: true。
$sentData = [ordered]@{
httpEnabled = $true
httpHost = "proxy.internal.local"
httpPort = 3128
httpAuthEnabled = $false
httpsEnabled = $true
httpsHost = "proxy.internal.local"
httpsPort = 3128
httpsAuthEnabled = $false
nonProxyHosts = @("*.cluster.local", "localhost")
}
$updBody = [ordered]@{
action="coreui_HttpSettings"; method="update"; data=@($sentData); type="rpc"; tid=1
} | ConvertTo-Json -Depth 10 -Compress
$updBody
"depth truncation = $($updBody -match 'OrderedDictionary|Hashtable')"
"nonProxyHosts is array = $($updBody -match '"nonProxyHosts":\[')"
depth truncation = FalsenonProxyHosts is array = True
目的:送出 outbound proxy 設定。
$updRaw = Invoke-NexusApi -Url "$nexusUrl/service/extdirect" -Method POST -JsonBody $updBody
"exit=$lastExit"
$updRaw
exit=0,"success":true,四個 host/port 有值,四個 auth 欄位null
目的:以獨立的一次 read 逐欄位確認送出值已寫入。不採用 update 自身的回顯,該回顯與寫入屬同一次呼叫。
$afterRaw = Invoke-NexusApi -Url "$nexusUrl/service/extdirect" -Method POST -JsonBody $readBody
$afterRaw
[System.IO.File]::WriteAllText((Join-Path $workDir "after.json"), $afterRaw,
(New-Object System.Text.UTF8Encoding $false))
$stored = ($afterRaw | ConvertFrom-Json).result.data
$governedBy = @{
httpHost = 'httpEnabled'; httpPort = 'httpEnabled'
httpsHost = 'httpsEnabled'; httpsPort = 'httpsEnabled'
}
$mismatch = @()
foreach ($k in @($sentData.Keys)) {
$sent = $sentData[$k]; $got = $stored.$k
if ($sent -is [bool] -and -not $sent -and $null -eq $got) { continue }
if ($governedBy.ContainsKey($k) -and -not $sentData[$governedBy[$k]]) { continue }
$sL = ($sent -is [System.Collections.IEnumerable]) -and ($sent -isnot [string])
$gL = ($got -is [System.Collections.IEnumerable]) -and ($got -isnot [string])
$ok = if ($sL -or $gL) {
((@($sent) | Sort-Object) -join "`n") -eq ((@($got) | Sort-Object) -join "`n")
} else { $sent -eq $got }
if (-not $ok) {
$gt = if ($null -eq $got) { '(null)' } else { "$got" }
$mismatch += [pscustomobject]@{ 欄位=$k; 送出="$sent"; 回存=$gt }
}
}
if ($mismatch.Count -gt 0) {
$mismatch | Format-Table -AutoSize | Out-String | Write-Host
throw "有 $($mismatch.Count) 個欄位未落地"
}
Write-Host "9 個欄位全數回讀相符,組態已寫入"
9 個欄位全數回讀相符,組態已寫入,after.json593 bytes
目的:確認產物齊全、無殘留行程,並記錄 current-context 的變動。
Get-ChildItem $workDir | Select-Object Name, Length
Get-Process -Name "oc" -ErrorAction SilentlyContinue
& $OC config current-context
values.yaml181 /before.json530 /after.json593 /chart-values-ref.yamloc行程查無結果nexus-proxy/api-crc-testing:6443/system:serviceaccount:nexus-proxy:nexus-deployer
目的:記錄快樂路徑腳本中六處設計的實測依據,供日後升版或改寫腳本時核對。不含操作步驟,操作步驟見快樂路徑本身。
| 腳本中的作法 | 對應步驟 | 省略後的結果 |
|---|---|---|
| disclaimer 取自 GET 回應 | 15–16 | HTTP 500 |
使用 /service/extdirect |
18–21 | CE 無其他可用途徑 |
nonProxyHosts 傳 @() |
19 | 拋出型別例外 |
四個 *Enabled 全部帶入 |
19 | 回應 success: true,欄位未寫入 |
| 寫入後另發一次讀取請求 | 17、21 | 以同一次呼叫的回應驗證自己 |
| 比對前兩側排序 | 21 | 設定成功但誤報不一致 |
測試環境:OpenShift 4.21.14、Chart stevehipwell/nexus3 5.23.0、Nexus 3.93.0-06(COMMUNITY)。部分結論在 3.94.0-12 / Chart 5.24.0 上複驗,結果一致。
ExtDirect 為未公開文件化的內部介面,更換版本後欄位與行為需重新核對。
目的:說明第 15–16 步為何不能寫死 disclaimer 字串,以及不遵守時的實際錯誤形式。
全新實例的 accepted 為 false,對外存取相關功能受限。簽署端點為 POST /service/rest/v1/system/eula,payload 需包含 disclaimer 與 accepted: true。
GET /service/rest/v1/system/eula 的回應:
{
"accepted" : false,
"disclaimer" : "Use of Sonatype Nexus Repository - Community Edition is governed by the End User License Agreement at https://links.sonatype.com/products/nxrm/ce-eula. By returning the value from ‘accepted:false’ to ‘accepted:true’, you acknowledge that you have read and agree to the End User License Agreement at https://links.sonatype.com/products/nxrm/ce-eula."
}
長度 348 字元。字串中的 ‘accepted:false’、‘accepted:true’ 使用排版引號 U+2018 / U+2019,與鍵盤輸入的 U+0027 為不同字元。編輯器的自動校正會將其替換為直引號。
後端要求逐字元相符。將該 348 字元字串截去末尾 1 個字元後送出,回應為:
HTTP/1.1 500 Server Error
ERROR: (ID 8cb4de16-8a78-40da-b31f-3d5ad95d771c)
java.lang.IllegalArgumentException: Invalid EULA disclaimer
引號替換造成 2 個字元差異,結果相同。換行符號(LF/CRLF)與多餘空白預期會造成同樣結果,未實測。
因此 disclaimer 由 GET 取得後直接送回,轉義交由 ConvertTo-Json 處理,寫入無 BOM UTF-8 暫存檔後傳送。
目的:確認此段腳本可否在已簽署的環境重複執行,以決定測試時是否需要另起實例。
對已簽署的實例重送同一份 disclaimer,回應 HTTP 204。回讀結果與送出前一致(accepted: true,disclaimer 內容不變),既有 proxy repository 未受影響。
在現有環境執行此段可用於驗證流程,不會變更既有設定。
目的:說明第 18–21 步為何使用未公開文件化的 ExtDirect,而非公開 REST API。此節為排除論證,結論綁定 Nexus 版本與版別。
Sonatype 文件〈HTTP Request and Proxy Settings〉記載的唯一途徑為 Administration 介面的 Settings → System → HTTP。該畫面儲存時呼叫的是 /service/extdirect。
各版本的公開端點可由該實例自身的 OpenAPI 定義清點:
$spec = curl.exe -k -s "$nexusUrl/service/rest/swagger.json" | ConvertFrom-Json
$paths = $spec.paths.PSObject.Properties.Name
$paths.Count
$paths | Where-Object { $_ -match 'http|proxy|setting|system' } | Sort-Object
3.93.0 共 255 個端點。關鍵字篩選的命中項目:
| 端點 | 用途 |
|---|---|
/v1/repositories/{format}/proxy |
各 repository 格式的 proxy 類型設定,與系統層級 outbound proxy 無關。25 種格式 × 2 條,共 50 條 |
/v1/repositorySettings |
repository 清單 |
/v1/system/eula、/license、/node |
系統資訊 |
/v1/http 在 CE 未註冊目的:排除第一條候選途徑。此端點存在於官方文件,需確認它在 CE 上不可用而非權限問題。
Sonatype 有一支文件化的 HTTP Configuration API,端點為 /service/rest/v1/http。該端點在此實例上的狀態:
curl.exe -k -s -o NUL -w "%{http_code}`n" -u "admin:$nexusPass" "$nexusUrl/service/rest/v1/http"
# → 404
$paths -contains '/v1/http' # → False
$paths | Where-Object { $_ -like '*/http*' } # → (無輸出)
回應為 404 而非 403,表示端點未註冊而非權限不足。swagger 中無任何符合 */http* 的路徑;上述篩選字串涵蓋 /v1/http,該路徑未出現於命中清單。
該 API 於 3.71.0 加入,release note 標示為 Nexus Repository Pro 管理者功能。Pro 版未實測。
目的:排除其餘兩條候選途徑——透過 Groovy 腳本設定,以及透過設定檔或啟動參數設定。
GET /service/rest/v1/script → 200,body 為 [ ]
POST /service/rest/v1/script → 410 Gone
GET /service/rest/v1/configuration → 400,body 為零位元組
410 的 body:
{"name":"probe-should-be-rejected","result":"Creating and updating scripts is disable"}
(原文缺一個 d。)Sonatype 自 3.21.2 起將 Groovy 腳本引擎預設停用,受影響的資源回 410,唯讀與執行類操作不受影響。
啟用途徑為 $data-dir/etc/nexus.properties。該檔內容:
nexus.datastore.enabled=true
nexus.loadAsOSS=true
無 nexus.scripts.allowCreation。INSTALL4J_ADD_VM_PARAMS 中亦無 proxy 相關系統屬性。
該檔案與 JVM 參數皆不含 proxy 設定,可知 outbound proxy 設定僅存於 config datastore,無法透過掛載 ConfigMap 或修改啟動參數設定。
排除結果:公開 REST 端點在 CE 未註冊、Script API 無法建立腳本、設定不在檔案系統、JVM 參數未使用。可用途徑僅剩 ExtDirect。
ExtDirect 無相容性承諾。升版或改用 Pro 後應重跑 swagger 清點與 /v1/http 探測。
nonProxyHosts 的型別目的:說明第 19 步為何使用 @()。此為型別錯誤,會在送出當下報錯,屬可自行發現的一類。
此欄位定義不經過 proxy 直連的網域。後端要求 JSON 陣列,非逗號分隔字串。
"nonProxyHosts": "*.cluster.local,localhost" ← 型別錯誤
"nonProxyHosts": ["*.cluster.local", "localhost"] ← 正確
傳入字串的錯誤訊息:
Expected BEGIN_ARRAY but was STRING at path *.nonProxyHosts
該回應的 HTTP 狀態碼為 200,頂層無 success 欄位,type 為 exception。--fail-with-body 無法攔截此類錯誤,需以 result.success 判斷。
錯誤訊息中的反序列化目標型別為 org.sonatype.nexus.coreui.HttpSettingsXO。例外鏈為 com.google.gson.JsonSyntaxException 與 java.lang.IllegalStateException,序列化器為 Gson。
同一設定在三個介面的型別各異:UI 為逐行輸入的清單;ExtDirect 為 JSON 陣列;Groovy CoreApi 接收 Java http.nonProxyHosts 的萬用字元樣式。逗號分隔的形式來自 Nexus 2 的 XML 設定與 JVM 系統屬性。
目的:說明第 19 步為何四個 *Enabled 全部帶入,包含兩個值為 false 的。此為不會報錯的一類,回應仍為成功。
將 nonProxyHosts 改為陣列後重送,回應:
{"tid":1,"action":"coreui_HttpSettings","method":"update","result":{"success":true,"data":{"userAgentSuffix":null,"timeout":null,"retries":null,"httpEnabled":null,"httpHost":null,"httpPort":null,"httpAuthEnabled":null,...,"nonProxyHosts":["*.cluster.local","localhost"]}},"type":"rpc"}
success 為 true。請求送出 9 個欄位,回存的 data 有 20 個欄位,除 nonProxyHosts 外其餘 19 個為 null,包含已送出的 httpHost、httpPort、httpsHost、httpsPort 及四個開關本身。
回應中無警告或欄位忽略提示。
目的:確立哪個欄位由哪個開關管轄,供第 21 步的 $governedBy 對照表使用。
於拋棄式實例分次送出,每次 update 後立即 read 回讀比對:
| 開關 | 送 true |
送 false |
|---|---|---|
httpEnabled → httpHost/httpPort |
落地,逐字相符 | null |
httpsEnabled → httpsHost/httpsPort |
落地,逐字相符 | null |
httpAuthEnabled → httpAuthUsername/NtlmHost/NtlmDomain |
落地 | null |
httpsAuthEnabled → 同上三個 |
落地 | null |
(無開關)nonProxyHosts |
內容一致 | — |
各組欄位由各自的開關管轄。開關為 false 時底下欄位回存 null;開關為 true 時欄位依送出值落地。開關本身送 false 時回存為 null,false 與 null 在此語意等價。
nonProxyHosts 無對應開關,因此在 httpEnabled=false 的請求中為唯一寫入成功的欄位。
目的:說明此規則為何需要在腳本層面處理,而非僅記錄於文件。
此規則未見於公開文件。未帶入或誤帶 false 的開關,其管轄欄位不會寫入,而回應仍為 success: true。問題在後續使用 proxy repository 時才顯現。
另有兩項影響腳本寫法:
httpAuthPassword 與 httpsAuthPassword 回讀為固定字串 #~NXRM~PLACEHOLDER~PASSWORD~#,核對邏輯需跳過。nonProxyHosts 基準值為 null 而非 [],設定後再清空才會變為 []。此項僅一次觀測。目的:說明第 17 步與第 21 步為何各自另發一次讀取請求,而非採用前一次呼叫的回應。
腳本中有兩處:第 17 步以另一次 GET 確認 EULA 的 accepted,第 21 步以另一次 read 核對 proxy 欄位。兩處皆不採用前一次呼叫的回應。
update 的回顯與寫入屬同一次呼叫;read 為獨立呼叫。EULA 的 POST 回 204 且無 body,僅表示請求被接受。
read 的回傳格式與 update 的 result.data 相同,可直接比對。
read 讀出的 payload 不可直接修改後回送。密碼欄位讀出為佔位字串,Nexus 是否將其識別為「維持原密碼」僅一次測試(結果為保留),未複驗。腳本每次送出完整組出的 payload,以避免該結論有誤時清除出站密碼。
目的:標示本文驗證範圍的上界,以及超出範圍的那一步長什麼樣。
accepted: true 僅確認狀態切換。功能層級的驗證為 proxy repository 的實際拉取:
curl.exe -k -s -o NUL -w "%{http_code}`n" "$nexusUrl/repository/npm-proxy/express"
在 EULA 已簽署且 proxy repository 運作中的實例上,此請求回 200。
快樂路徑無法執行此驗證:其使用的 proxy.internal.local 不存在,且該實例未建立任何 proxy repository。上述 200 為正式實例的量測結果。
nonProxyHosts 回存順序不保證目的:說明第 21 步的兩處 Sort-Object 為必要而非防禦性寫法,避免日後被當作冗餘移除。
第一次執行快樂路徑,送出 ["*.cluster.local","localhost"],回存順序一致。
第二次執行,payload、Chart 版本、Nexus 版本均相同:
送出:["*.cluster.local","localhost"]
回存:["localhost","*.cluster.local"]
順序反轉。
較早在拋棄式實例上以三個元素測試:
送出: zzz.example.com, aaa.example.com, mmm.example.com
回存: aaa.example.com, zzz.example.com, mmm.example.com
回存順序既非送出順序亦非字典序。
三次觀測支持的結論為回存順序不保證。成因未確認;未測試重複元素,無法判斷後端是否使用 set。
比對前不排序會在設定成功的情況下對 nonProxyHosts 誤報。
目的:標示本文結論的邊界,避免被引用到未支撐的範圍。
accepted = false。httpAuthEnabled 相關欄位與密碼佔位字串的 round-trip 行為未複驗。/v1/http 的 404 僅證明 CE 未註冊該端點。目的:歸納三種回應形式與各自對應的驗證方式,供處理其他 Nexus API 時參照。
| 回應 | 含義 | 驗證方式 |
|---|---|---|
type: exception,HTTP 200 |
請求未被接受 | 判斷 result.success |
success: true |
請求被接受,寫入結果未知 | 另發 read 逐欄位比對 |
| HTTP 204,無 body | 請求被接受 | 另發 GET 讀取狀態 |
三者的共通處理方式為:寫入後另發一次讀取請求,逐欄位比對送出值。
參考文件