本文所有數值與輸出來自下列環境,量測日期 2026-08-08 至 08-09。換版本需重新驗證。
| 項目 | 版本 |
|---|---|
| CRC | 2.61.0 |
| OpenShift / Kubernetes | 4.21.14 / v1.34.6 |
| OpenShift Pipelines Operator | openshift-pipelines-operator-rh.v1.23.1 |
| Tekton Pipelines controller | v1.12.2 |
| oc / kubectl | 4.21.14 / v1.34.10 |
| Gitea | 1.27.0 |
| Node / npm / Nx | v24.16.0 / 11.13.0 / 23.1.1 |
| PowerShell | 5.1 |
Operator 版本(1.23.1)與 Tekton Pipelines 版本(v1.12.2)是兩個不同數列。TektonConfig.status.operandVersions 在本叢集為空,上游版本要讀 Deployment 的 label:
& $OC --kubeconfig $kubeconfig get deploy tekton-pipelines-controller -n openshift-pipelines -o jsonpath='{.metadata.labels}'
需要一個叢集內匿名 clone 得到的 Git repo。本系列使用 Gitea 上的一個 Angular Nx monorepo(create-nx-workspace --preset=angular-monorepo,--e2eTestRunner=none),後面幾天的建置、測試、掃描使用同一份程式碼。
三個與 CI 有關的設定:
.gitattributes 設 * text=auto eol=lf。在 Windows 建立、Linux 容器內執行,CRLF 進版控會使容器內的 script 出現 bad interpreter。package-lock.json 必須進版控,npm ci 只讀它。basic-auth workspace 與一份 Secret。完整建立步驟見附錄。
叢集內的可達性要單獨驗一次,主機端能連不代表叢集內能連:
$fmt = 'incluster=%{http_code} t=%{time_total}\n'
$url = 'http://gitea-http.gitea.svc.cluster.local:3000/gitea_admin/frontend-nx-mono/info/refs?service=git-upload-pack'
& $OC --kubeconfig $kubeconfig run gitea-probe -n default --restart=Never --attach --rm `
--image=registry.access.redhat.com/ubi9/ubi-minimal:latest -- `
curl -s -o /dev/null -w $fmt --max-time 10 $url
實測輸出 incluster=200 t=0.014955。gitea-http 是 headless Service(ClusterIP: None),解析得到 Pod IP。
& $OC --kubeconfig $kubeconfig create namespace ci --dry-run=client -o yaml |
& $OC --kubeconfig $kubeconfig apply -f -
ci 已存在時 create 會回 AlreadyExists 並以非零碼結束,改用 --dry-run | apply。若該 namespace 原先是用 create 建立的,第一次執行會出現一則補上 kubectl.kubernetes.io/last-applied-configuration 的 Warning,只出現一次。
Operator 對新 namespace 注入 pipeline ServiceAccount 與兩條 RoleBinding,注入是非同步的。namespace 建立後立即送出 PipelineRun 會得到 PodCreationFailed: serviceaccounts "pipeline" not found。等 SA 出現再繼續:
$deadline = (Get-Date).AddSeconds(60)
do {
Start-Sleep -Seconds 2
$sa = & $OC --kubeconfig $kubeconfig get sa pipeline -n ci --ignore-not-found -o name
} until ($sa -or (Get-Date) -gt $deadline)
$sa
& $OC --kubeconfig $kubeconfig get task git-clone -n openshift-pipelines -o jsonpath='{.spec.params[*].name}'
& $OC --kubeconfig $kubeconfig get task git-clone -n openshift-pipelines -o jsonpath='{.spec.results[*].name}'
& $OC --kubeconfig $kubeconfig get task git-clone -n openshift-pipelines -o jsonpath='{.spec.workspaces[*].name}'
實測輸出:
params : CRT_FILENAME HTTP_PROXY HTTPS_PROXY NO_PROXY SUBDIRECTORY USER_HOME
DELETE_EXISTING VERBOSE SSL_VERIFY URL REVISION REFSPEC SUBMODULES
DEPTH SPARSE_CHECKOUT_DIRECTORIES
results : COMMIT URL COMMITTER_DATE
workspaces : ssh-directory basic-auth ssl-ca-directory output
同時確認兩個前提:
& $OC --kubeconfig $kubeconfig get cm cluster-resolver-config -n openshift-pipelines -o jsonpath='{.data}'
& $OC --kubeconfig $kubeconfig get sc
cluster-resolver-config 的 default-namespace 為空,因此 1-4 的 namespace 參數不能省。StorageClass 有預設(crc-csi-hostpath-provisioner),因此 PipelineRun 的 volumeClaimTemplate 不需要指定 storageClassName。
PowerShell 5.1 寫檔用 [System.IO.File]::WriteAllText 搭配不含 BOM 的編碼物件;> 產出 UTF-16LE,Set-Content -Encoding UTF8 會加 BOM。here-string 必須用單引號版 @'...'@,雙引號版會把 $(params.x) 當成子運算式求值。
自訂 Task(放在 ci):
apiVersion: tekton.dev/v1
kind: Task
metadata:
name: d12-show-commit
namespace: ci
spec:
description: local task in ci
params:
- name: commit
type: string
steps:
- name: show
image: registry.access.redhat.com/ubi9/ubi-minimal:latest
script: |
#!/usr/bin/env bash
echo "resolved commit: $(params.commit)"
description 這行是刻意加的,用途見 2-5。自訂 Task 的參數用小寫是合法的。
Pipeline:
apiVersion: tekton.dev/v1
kind: Pipeline
metadata:
name: d12-happy
namespace: ci
spec:
params:
- name: repo-url
type: string
- name: repo-revision
type: string
default: main
workspaces:
- name: source
tasks:
- name: fetch
taskRef:
resolver: cluster
params:
- name: kind
value: task
- name: name
value: git-clone
- name: namespace
value: openshift-pipelines
params:
- name: URL
value: $(params.repo-url)
- name: REVISION
value: $(params.repo-revision)
workspaces:
- name: output
workspace: source
- name: show-commit
runAfter: [fetch]
taskRef:
name: d12-show-commit
params:
- name: commit
value: $(tasks.fetch.results.COMMIT)
$(tasks.fetch.results.COMMIT) 的 fetch 是這份 Pipeline 內的實例名(tasks[].name),不是 Task 名稱。
PipelineRun:
apiVersion: tekton.dev/v1
kind: PipelineRun
metadata:
generateName: d12-happy-
namespace: ci
spec:
pipelineRef:
name: d12-happy
taskRunTemplate:
serviceAccountName: pipeline
params:
- name: repo-url
value: http://gitea-http.gitea.svc.cluster.local:3000/gitea_admin/frontend-nx-mono.git
workspaces:
- name: source
volumeClaimTemplate:
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 1Gi
& $OC --kubeconfig $kubeconfig apply --validate=strict -f .\d12-show-commit.yaml
& $OC --kubeconfig $kubeconfig apply --validate=strict -f .\d12-happy-pipeline.yaml
$run = & $OC --kubeconfig $kubeconfig create -f .\d12-happy-run.yaml -o name
$name = ($run -split '/')[-1]
generateName 不能用 apply。--validate=strict 的作用範圍見 2-2。
& $OC --kubeconfig $kubeconfig get pipelinerun $name -n ci -o jsonpath='{.status.conditions[0].reason}'
實測 Succeeded,message 為 Tasks Completed: 2 (Failed: 0, Cancelled 0), Skipped: 0,從送出到完成約 45 秒。
確認跨 Namespace 解析:
$trs = & $OC --kubeconfig $kubeconfig get taskrun -n ci -l tekton.dev/pipelineRun=$name -o name
foreach ($tr in $trs) {
$n = ($tr -split '/')[-1]
$src = & $OC --kubeconfig $kubeconfig get taskrun $n -n ci -o jsonpath='{.status.provenance.refSource.uri}'
"{0}`t{1}" -f $n, $src
}
實測輸出:
d12-happy-xxxxx-fetch /apis/tekton.dev/v1/namespaces/openshift-pipelines/task/git-clone@<uid>
d12-happy-xxxxx-show-commit (空)
確認 result 傳遞:
& $OC --kubeconfig $kubeconfig logs -n ci -l tekton.dev/pipelineTask=show-commit -c step-show --tail=5
輸出 resolved commit: <40 位 SHA>,與本機 git rev-parse HEAD 相同。-c step-show 不能省,否則 oc 會往 stderr 輸出 Defaulted container ...,在 PowerShell 5.1 會被包成 NativeCommandError。
taskRef 的 schema 不含 namespace查 pipelines.tekton.dev CRD 目前安裝的兩個版本(v1beta1、v1),taskRef 底下都沒有 namespace 屬性。
& $OC --kubeconfig $kubeconfig explain pipeline.spec.tasks.taskRef
FIELDS:
apiVersion <string>
kind <string>
name <string>
套用一份帶 taskRef.namespace 的 Pipeline,再把物件讀回來:
& $OC --kubeconfig $kubeconfig get pipeline probe -n ci -o jsonpath='{.spec.tasks[0].taskRef}'
{"kind":"Task","name":"git-clone"}
namespace 鍵不在物件裡。API Server 依 structural schema 在寫入 etcd 前剪除未定義欄位,該設定沒有到達 controller。
不是「v1 移除、v1beta1 保留」。本叢集這兩個 API 版本的 schema 都沒有這個欄位。至於 Tekton 上游是否曾經支援過 taskRef.namespace,搜尋未找到支持或否定的證據,本文不做判斷。
oc 與 kubectl 的驗證預設相反同一份帶 taskRef.namespace 的 Pipeline:
| 呼叫 | 結果 | exit |
|---|---|---|
oc apply(預設) |
pipeline.tekton.dev/probe created,無錯誤無警告 |
0 |
oc apply --validate=false |
同上 | 0 |
oc apply --validate=strict |
strict decoding error: unknown field "spec.tasks[0].taskRef.namespace" |
1 |
oc apply --server-side |
field not declared in schema |
1 |
kubectl apply(預設) |
Error from server (BadRequest): … strict decoding error |
1 |
兩個 binary 的 help 印出的預設值不同,說明文字相同:
oc apply --validate='ignore'
kubectl apply --validate='strict'
ignore 的說明文字為:不執行任何 schema 驗證,靜默丟棄未知或重複的欄位。
--validate=strict 的作用範圍另有一個限制:
| 呼叫 | 結果 |
|---|---|
oc apply --dry-run=server |
created (server dry run),exit 0,無警告 |
oc apply --dry-run=server --validate=strict |
BadRequest … strict decoding error,exit 1 |
oc apply --dry-run=client --validate=strict |
created (dry run),exit 0,未檢查未知欄位 |
--validate=strict 只在請求送到 API Server 的路徑上生效。配 --dry-run=client 不檢查未知欄位。
GitOps 工具多數使用 server-side apply,該路徑會被擋下。
& $OC --kubeconfig $kubeconfig get cm resolvers-feature-flags -n openshift-pipelines -o jsonpath='{.data}'
{"enable-bundles-resolver":"true","enable-cluster-resolver":"true",
"enable-git-resolver":"true","enable-http-resolver":"true","enable-hub-resolver":"true"}
& $OC --kubeconfig $kubeconfig get cm cluster-resolver-config -n openshift-pipelines -o jsonpath='{.data}'
{"allowed-namespaces":"","blocked-namespaces":"","default-kind":"task","default-namespace":""}
| 鍵 | 值 | 後果 |
|---|---|---|
allowed-namespaces |
空 | 不限制來源 namespace |
blocked-namespaces |
空 | 無黑名單 |
default-kind |
task |
未指定 kind 時以 Task 解析 |
default-namespace |
空 | namespace 參數不能省,無預設值可回退 |
taskRef 底下 resolver 的 params 是自由的 name/value 陣列,不是固定 schema。參數名打錯不會產生任何錯誤,只會被忽略;--validate=strict 與 kubectl 的預設驗證都擋不住(實測 - name: cache 這個非文件記載的參數名可以通過 kubectl apply 的 strict 驗證並正常執行)。
feature-flags 中 enable-concise-resolver-syntax 為 false,簡寫語法在本叢集未啟用。
openshift-pipelines namespace 內有 45 個 Task,參數總數 374。其中 40 個 Task 的參數全大寫,5 個含小寫字母(含小寫的參數共 34 個)。
| Task | 風格 |
|---|---|
helm-upgrade-from-repo、helm-upgrade-from-source |
snake_case(helm_repo、chart_name) |
pull-request、argocd-task-sync-and-wait |
kebab-case(secret-key-ref、application-name) |
kubeconfig-creator |
小寫單字與 camelCase 混用(name、url 與 clientKeyData、clientCertificateData) |
全大寫是這個 bundle 的慣例,不是 Tekton 的規則。kubeconfig-creator 在同一個 Task 內混用兩種風格。
同一份 bundle 內另有 6 個 StepAction,其中 git-clone、cache-fetch、cache-upload 三個名稱與 Task 重複。kind 的預設值是 task,未指定時取得的是 Task。45 個 Task 均未使用 step.ref 引用 StepAction。
工作區名稱為小寫(ssh-directory、basic-auth、ssl-ca-directory、output)。
provenance.refSource同一次 PipelineRun 中兩筆 TaskRun 的比較:
status.taskSpec.description |
status.provenance.refSource |
|
|---|---|---|
| cluster resolver(跨 namespace) | 有值 | 有值 |
taskRef.name(同 namespace) |
有值 | 空 |
taskSpec.description 在兩種引用方式下都有值,不能用來判定是否跨 namespace 解析。1-4 的自訂 Task 刻意宣告了 description,若不宣告,同 namespace 那筆的 description 會是空的,看起來像是本地引用不產生 description。
resolver 那筆的 refSource 完整內容:
{
"digest": {"sha256": "77ac8fe80ef2d5ebf78f7e65032f650d6436aa209957e0e794c7ef8dc33e9a5a"},
"uri": "/apis/tekton.dev/v1/namespaces/d12-tasklib/task/hello-remote@3682d222-aea7-41b9-84d3-7d3dd2ef22cb"
}
uri 含來源 namespace 與 Task 名稱,另附內容 digest。
此欄位受 feature-flags 的 enable-provenance-in-status 控制,本叢集為 true。
三個案例的 oc apply 全部成功(exit 0),validating webhook 未攔截任何一個。錯誤在 PipelineRun 建立時出現。
| 寫錯 | apply | reason |
|---|---|---|
taskRef.namespace |
created(欄位被剪除) | CouldntGetTask |
params 傳小寫 url |
created | PipelineValidationFailed |
result 引用小寫 commit |
created | InvalidTaskResultReference |
message 全文:
CouldntGetTask:
Pipeline ci/case1-taskref-ns can't be Run; it contains Tasks that don't exist:
Couldn't retrieve Task "git-clone": tasks.tekton.dev "git-clone" not found
PipelineValidationFailed:
[User error] Validation failed for pipelinerun run-case2 with error invalid input
params for task git-clone: missing values for these params which have no default
values: [URL]
InvalidTaskResultReference:
invalid result reference in pipeline task "t2": "commit" is not a named result
returned by pipeline task "t1"
[User error] 前綴只出現在第二個案例。
上游同時存在 ParameterMissing 這個 reason 常數,但缺參數的實測結果是 PipelineValidationFailed。
以一個自建的 namespace d12-tasklib(ci/pipeline 對其無任何權限)為來源測試。
呼叫端 SA 的權限:
can-i get tasks -n d12-tasklib --as=system:serviceaccount:ci:pipeline = no
can-i list tasks -n d12-tasklib --as=system:serviceaccount:ci:pipeline = no
直接讀取被拒絕:
Error from server (Forbidden): tasks.tekton.dev "secret-task-a1" is forbidden:
User "system:serviceaccount:ci:pipeline" cannot get resource "tasks"
in API group "tekton.dev" in the namespace "d12-tasklib"
以 cluster resolver 引用同一個 Task,TaskRun 明確指定 serviceAccountName: pipeline:
reason = Succeeded
執行的 SA = pipeline
解析到的 description = only reachable via resolver
refSource = /apis/tekton.dev/v1/namespaces/d12-tasklib/task/secret-task-a1@c08f6f85-…
step 輸出 = A1-RESOLVED
同一個 ServiceAccount,直接讀取為 Forbidden,經由 resolver 讀取成功並執行完成。
原因是解析由 resolver 的控制器執行,不由呼叫端執行:
tekton-pipelines-remote-resolvers 的 serviceAccountName = tekton-pipelines-resolvers
can-i get tasks --all-namespaces --as=…:tekton-pipelines-resolvers = yes
can-i get secrets --all-namespaces --as=…:tekton-pipelines-resolvers = yes
呼叫端的 RBAC 在整個解析過程中沒有被查詢。
一個相關的觀察:ci/pipeline 對 openshift-pipelines 的 Task 有讀取權(can-i get tasks -n openshift-pipelines = yes)。因此「引用官方預建 Task」這個用法的結果與 RBAC 允許的結果一致,用它測不出上述行為,需要一個明確未授權的 namespace 才能觀察到。
tekton-pipelines-resolvers 具備全叢集 get secrets 權限,用途未查證,本文不做推論。
四種設定的實測結果,經 TektonConfig 設定後約 15 秒同步到 ConfigMap。每次使用未解析過的 Task 名稱以避開快取。
| 設定 | 結果 | message |
|---|---|---|
blocked-namespaces = d12-tasklib |
擋下 | access to specified namespace d12-tasklib is blocked |
allowed-namespaces = openshift-pipelines |
擋下 | access to specified namespace d12-tasklib is not allowed |
blocked-namespaces = * |
擋下 | only explicit allowed access to namespaces is allowed |
blocked-namespaces = * + allowed-namespaces = d12-tasklib |
Succeeded | — |
四種情況的 reason 全部是 TaskRunResolutionFailed(第四種成功,無 reason)。與 2-6 的三個錯誤不同:那三個的 reason 各自不同,解析階段的失敗共用同一個 reason,需要讀 message 才能分辨是哪一種設定造成的。
allowed-namespaces 是白名單語意,設定後不在名單上的一律擋下,不需要另外設 blocked-namespaces。第四種組合(全部封鎖後逐一放行)在本版本可行。
blocked-namespaces = * 是叢集層級設定,會影響該叢集上所有使用 cluster resolver 的 Pipeline。
手動修改 cluster-resolver-config 或 feature-flags 之後:
| 觸發 | 結果 |
|---|---|
| 閒置 120 秒 | 未還原 |
patch tektonconfig(加 annotation 觸發 reconcile) |
未還原 |
rollout restart Operator Deployment |
未還原 |
Operator 的行為是在 TektonConfig 對應欄位發生變動時把值推到 ConfigMap,不是持續對帳:
| 情境 | 結果 |
|---|---|
| TektonConfig 設成與現況等效的值 | 不推送 |
| TektonConfig 設成不同的值 | 15 秒內推送 |
| TektonConfig 改變既有值 | 30 秒內推送 |
因此手動改 ConfigMap 不會被自動還原,但只要有人動到 TektonConfig 對應欄位,手動修改會被覆蓋且沒有提示。
TektonConfig 的相關欄位:
& $OC --kubeconfig $kubeconfig explain tektonconfig.spec.pipeline
cluster-resolver-config <map[string]string>
bundles-resolver-config <map[string]string>
git-resolver-config <map[string]string>
default-resolver-type <string>
enable-cluster-resolver <boolean>
Red Hat 文件在說明 feature-flags 時記載改動會在 Operator reconcile 時被覆寫,與上述實測不一致。原因未查明,本文不做判斷。
Resolver 快取:resolver-cache-config 為 {"max-size":"1000","ttl":"5m"}。cluster-resolver-config 沒有 default-cache-mode 鍵。實測修改來源 Task 的 description 後立即重新解析,取得的是新值,不是快取的舊值。此結果僅涵蓋「來源變動後是否取得新值」,未量測「來源未變動時是否使用快取」。
ClusterTask:clustertasks.tekton.dev CRD 在本叢集不存在,oc get clustertask 回報無此資源型別。45 個預建 Task 均為 namespace 範圍的 Task,集中在 openshift-pipelines。
版本化雙胞胎:預建 Task 有不帶後綴與帶 -1-23-0 後綴兩組(如 git-clone 與 git-clone-1-23-0)。不帶後綴的會隨 Operator 升版變動。
Namespace 注入的其他影響:Operator 對非 openshift-* / kube-* 前綴的每個 namespace 注入 pipeline SA 與兩條 RoleBinding。注入後該 namespace 的 security.openshift.io/MinimallySufficientPodSecurityStandard 由 restricted 變為 baseline。TaskRun 的 Pod 實際套用 pipelines-scc。
清理:刪除 PipelineRun 後 TaskRun、Pod、PVC 的回收是非同步的,實測約 90 秒歸零。CRC 的 crc-csi-hostpath-provisioner 的 reclaimPolicy 是 Retain,volumeClaimTemplate 產生的 PV 在 PVC 刪除後停留在 Released 且不自動回收,get pvc -n ci 看不到(PV 是叢集層級物件)。單次約 1.7 MB。
& $OC --kubeconfig $kubeconfig get pv --no-headers | Where-Object { $_ -match 'Released' }
resolver: cluster,且 kind/name/namespace 三個 params 都寫了taskRef.name
provenance.refSource 而非 taskSpec.description 判定跨 Namespace 解析allowed-namespaces 與 blocked-namespaces 現值taskRef.namespace:搜尋未找到支持或否定的證據。可確認的只有本叢集兩個 API 版本的 schema 都沒有這個欄位。tekton-pipelines-resolvers 的全叢集 get secrets 權限用途:未查證。kubectl 的驗證行為:以 v1.34.10 量測。目前這台機器上未安裝 kubectl,該結果無法立即複驗。allowed-namespaces 與 blocked-namespaces 的組合僅測試 2-8 表中四種,未窮舉。本文的數值、指令輸出與行為判定全部來自第〇節環境的實測。外部資料只用於兩件事:確認某個行為是否有官方說明,以及在實測與文件不一致時標出差異。
| 用於 | 來源 |
|---|---|
oc apply 的 --validate 預設值為 ignore、以及 ignore 會靜默丟棄未知欄位 |
oc-apply(1) man page:https://manpages.opensuse.org/Tumbleweed/openshift-clients/oc-apply.1.en.html |
PipelineRunReason 常數清單(含 CouldntGetTask、ParameterMissing、PipelineValidationFailed) |
tektoncd/pipeline 的 Go API 文件:https://pkg.go.dev/github.com/tektoncd/pipeline/pkg/apis/pipeline/v1 |
Retain 之下 PV 停留在 Released、不自動回收、需管理員手動處理 |
Kubernetes 官方文件,Persistent Volumes 的 Reclaiming 章節:https://kubernetes.io/docs/concepts/storage/persistent-volumes/ |