本文發表於一年多前。舊文章可能包含過時內容。請檢查頁面中的資訊自發布以來是否已變得不正確。
使用 CEL 轉換規則強制 CRD 不可變性
不變的欄位在內建的 Kubernetes 型別中隨處可見。例如,你不能更改物件的 .metadata.name
。特定物件有一些欄位,其對現有物件的更改受到限制;例如 Deployment 的 .spec.selector
。
除了簡單的不可變性之外,還有其他常見的涉及列表的設計模式,例如只允許追加,或者鍵不可變但值可變的對映。
直到最近,限制 CustomResourceDefinition 欄位可變性的最佳方法是建立一個驗證 准入 Webhook:這對於使欄位不可變的常見情況來說,意味著很多複雜性。
自 Kubernetes 1.25 起,CEL 驗證規則已進入 Beta 階段,它允許 CRD 作者使用豐富的表示式語言 CEL 來表達對其欄位的驗證約束。本文探討了如何使用驗證規則直接在 CRD 清單中實現一些常見的不可變模式。
驗證規則基礎
Kubernetes 中對 CEL 驗證規則的新支援允許 CRD 作者為其資源新增複雜的准入邏輯,而無需編寫任何程式碼!
例如,一個將 CRD 欄位 maximumSize
約束為大於 minimumSize
的 CEL 規則可能如下所示:
rule: |
self.maximumSize > self.minimumSize
message: 'Maximum size must be greater than minimum size.'
規則欄位包含用 CEL 編寫的表示式。self
是 CEL 中的一個特殊關鍵字,它指代包含該規則的物件的型別。
訊息欄位是一個錯誤訊息,當此特定規則不滿足時,將傳送給 Kubernetes 客戶端。
有關使用 CEL 驗證規則的功能和限制的更多詳細資訊,請參閱驗證規則。 CEL 規範也是有關該語言的良好參考資料。
使用 CEL 驗證規則實現不變性模式
本節將使用表示為 kubebuilder 標記註釋的驗證規則來實現 Kubernetes CustomResourceDefinitions 中幾種常見的不變性用例。還將包含由 kubebuilder 標記註釋生成的 OpenAPI,以便如果您手動編寫 CRD 清單,也可以繼續學習。
專案設定
要將 CEL 規則與 kubebuilder 註釋一起使用,您首先需要設定一個 Go 語言專案結構,其中 CRD 在 Go 中定義。
如果您不使用 kubebuilder 或只對生成的 OpenAPI 擴充套件感興趣,則可以跳過此步驟。
從以下所示的 Go 模組資料夾結構開始。如果您已經設定了自己的專案,請隨意根據您的喜好調整本教程。
這是 Kubernetes 專案用於定義新 API 資源的典型資料夾結構。
doc.go
包含包級元資料,例如組和版本。
// +groupName=stable.example.com
// +versionName=v1
package v1
types.go
包含 stable.example.com/v1 中的所有型別定義。
package v1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// An empty CRD as an example of defining a type using controller tools
// +kubebuilder:storageversion
// +kubebuilder:subresource:status
type TestCRD struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec TestCRDSpec `json:"spec,omitempty"`
Status TestCRDStatus `json:"status,omitempty"`
}
type TestCRDStatus struct {}
type TestCRDSpec struct {
// You will fill this in as you go along
}
tools.go
包含對 controller-gen 的依賴,它將用於生成 CRD 定義。
//go:build tools
package celimmutabilitytutorial
// Force direct dependency on code-generator so that it may be executed with go run
import (
_ "sigs.k8s.io/controller-tools/cmd/controller-gen"
)
最後,generate.go
包含一個 go:generate
指令,用於使用 controller-gen
。controller-gen
解析我們的 types.go
並生成 CRD yaml 檔案到 crd
資料夾中。
package celimmutabilitytutorial
//go:generate go run sigs.k8s.io/controller-tools/cmd/controller-gen crd paths=./pkg/apis/... output:dir=./crds
您現在可能需要為我們的定義新增依賴項並測試程式碼生成。
cd cel-immutability-tutorial
go mod init <your-org>/<your-module-name>
go mod tidy
go generate ./...
執行這些命令後,您已完成基本的專案結構。您的資料夾樹應如下所示:
示例 CRD 的清單現在位於 crds/stable.example.com_testcrds.yaml
中。
首次修改後不可變
一個常見的不變性設計模式是,一旦欄位首次設定,就使其不可變。如果欄位在首次初始化後發生更改,此示例將丟擲驗證錯誤。
// +kubebuilder:validation:XValidation:rule="!has(oldSelf.value) || has(self.value)", message="Value is required once set"
type ImmutableSinceFirstWrite struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
// +kubebuilder:validation:Optional
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable"
// +kubebuilder:validation:MaxLength=512
Value string `json:"value"`
}
註釋中的 +kubebuilder
指令通知 controller-gen 如何註釋生成的 OpenAPI。XValidation
規則使該規則出現在 x-kubernetes-validations
OpenAPI 擴充套件中。然後 Kubernetes 遵守 OpenAPI 規範來強制執行我們的約束。
要強制欄位在首次寫入後不可變,您需要應用以下約束:
- 欄位最初可以未設定:
+kubebuilder:validation:Optional
- 一旦設定,欄位不允許被刪除:
!has(oldSelf.value) | has(self.value)
(類型範圍規則) - 一旦設定,欄位不允許更改值:
self == oldSelf
(欄位範圍規則)
另請注意附加指令 +kubebuilder:validation:MaxLength
。CEL 要求所有字串都附加最大長度,以便它可以估計規則的計算成本。成本過高的規則將被拒絕。有關 CEL 成本預算的更多資訊,請檢視其他教程。
用法示例
生成並安裝 CRD 應該成功
# Ensure the CRD yaml is generated by controller-gen
go generate ./...
kubectl apply -f crds/stable.example.com_immutablesincefirstwrites.yaml
customresourcedefinition.apiextensions.k8s.io/immutablesincefirstwrites.stable.example.com created
建立初始的空物件,沒有 value
是允許的,因為 value
是 optional
kubectl apply -f - <<EOF
---
apiVersion: stable.example.com/v1
kind: ImmutableSinceFirstWrite
metadata:
name: test1
EOF
immutablesincefirstwrite.stable.example.com/test1 created
value
的首次修改成功
kubectl apply -f - <<EOF
---
apiVersion: stable.example.com/v1
kind: ImmutableSinceFirstWrite
metadata:
name: test1
value: Hello, world!
EOF
immutablesincefirstwrite.stable.example.com/test1 configured
嘗試更改 value
被欄位級驗證規則阻止。請注意,顯示給使用者的錯誤訊息來自驗證規則。
kubectl apply -f - <<EOF
---
apiVersion: stable.example.com/v1
kind: ImmutableSinceFirstWrite
metadata:
name: test1
value: Hello, new world!
EOF
The ImmutableSinceFirstWrite "test1" is invalid: value: Invalid value: "string": Value is immutable
嘗試完全刪除 value
欄位被型別上的另一個驗證規則阻止。錯誤訊息也來自該規則。
kubectl apply -f - <<EOF
---
apiVersion: stable.example.com/v1
kind: ImmutableSinceFirstWrite
metadata:
name: test1
EOF
The ImmutableSinceFirstWrite "test1" is invalid: <nil>: Invalid value: "object": Value is required once set
生成的 Schema
請注意,在生成的 Schema 中,有兩個獨立的規則位置。一個直接附加到屬性 immutable_since_first_write
。另一個規則與 CRD 型別本身相關聯。
openAPIV3Schema:
properties:
value:
maxLength: 512
type: string
x-kubernetes-validations:
- message: Value is immutable
rule: self == oldSelf
type: object
x-kubernetes-validations:
- message: Value is required once set
rule: '!has(oldSelf.value) || has(self.value)'
物件建立時不可變
建立時不可變的欄位的實現方式與前面的示例類似。不同之處在於該欄位被標記為必需,並且類型範圍規則不再必要。
type ImmutableSinceCreation struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
// +kubebuilder:validation:Required
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable"
// +kubebuilder:validation:MaxLength=512
Value string `json:"value"`
}
此欄位在物件建立時將是必需的,此後將不允許修改。我們的 CEL 驗證規則為 self == oldSelf
。
使用示例
生成並安裝 CRD 應該成功
# Ensure the CRD yaml is generated by controller-gen
go generate ./...
kubectl apply -f crds/stable.example.com_immutablesincecreations.yaml
customresourcedefinition.apiextensions.k8s.io/immutablesincecreations.stable.example.com created
應用沒有必需欄位的物件應該失敗
kubectl apply -f - <<EOF
apiVersion: stable.example.com/v1
kind: ImmutableSinceCreation
metadata:
name: test1
EOF
The ImmutableSinceCreation "test1" is invalid:
* value: Required value
* <nil>: Invalid value: "null": some validation rules were not checked because the object was invalid; correct the existing errors to complete validation
現在欄位已新增,操作被允許
kubectl apply -f - <<EOF
apiVersion: stable.example.com/v1
kind: ImmutableSinceCreation
metadata:
name: test1
value: Hello, world!
EOF
immutablesincecreation.stable.example.com/test1 created
如果您嘗試更改 value
,則由於 CRD 中的驗證規則,操作將被阻止。請注意,錯誤訊息與驗證規則中定義的相同。
kubectl apply -f - <<EOF
apiVersion: stable.example.com/v1
kind: ImmutableSinceCreation
metadata:
name: test1
value: Hello, new world!
EOF
The ImmutableSinceCreation "test1" is invalid: value: Invalid value: "string": Value is immutable
此外,如果您在新增 value
後嘗試完全刪除它,您將看到預期的錯誤
kubectl apply -f - <<EOF
apiVersion: stable.example.com/v1
kind: ImmutableSinceCreation
metadata:
name: test1
EOF
The ImmutableSinceCreation "test1" is invalid:
* value: Required value
* <nil>: Invalid value: "null": some validation rules were not checked because the object was invalid; correct the existing errors to complete validation
生成的 Schema
openAPIV3Schema:
properties:
value:
maxLength: 512
type: string
x-kubernetes-validations:
- message: Value is immutable
rule: self == oldSelf
required:
- value
type: object
僅可追加的容器列表
對於 Pod 上的臨時容器,Kubernetes 強制列表中的元素是不可變的,並且不能被刪除。以下示例展示瞭如何使用 CEL 實現相同的行為。
// +kubebuilder:validation:XValidation:rule="!has(oldSelf.value) || has(self.value)", message="Value is required once set"
type AppendOnlyList struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
// +kubebuilder:validation:Optional
// +kubebuilder:validation:MaxItems=100
// +kubebuilder:validation:XValidation:rule="oldSelf.all(x, x in self)",message="Values may only be added"
Values []v1.EphemeralContainer `json:"value"`
}
- 一旦設定,欄位不得刪除:
!has(oldSelf.value) || has(self.value)
(類型範圍) - 一旦新增值,它就不能被刪除:
oldSelf.all(x, x in self)
(欄位範圍) - 值最初可以未設定:
+kubebuilder:validation:Optional
請注意,為了預算成本,也需要指定 MaxItems
。
用法示例
生成並安裝 CRD 應該成功
# Ensure the CRD yaml is generated by controller-gen
go generate ./...
kubectl apply -f crds/stable.example.com_appendonlylists.yaml
customresourcedefinition.apiextensions.k8s.io/appendonlylists.stable.example.com created
建立一個包含一個元素的初始列表應該會成功,沒有任何問題
kubectl apply -f - <<EOF
---
apiVersion: stable.example.com/v1
kind: AppendOnlyList
metadata:
name: testlist
value:
- name: container1
image: nginx/nginx
EOF
appendonlylist.stable.example.com/testlist created
向列表中新增一個元素也應該會順利進行,符合預期
kubectl apply -f - <<EOF
---
apiVersion: stable.example.com/v1
kind: AppendOnlyList
metadata:
name: testlist
value:
- name: container1
image: nginx/nginx
- name: container2
image: mongodb/mongodb
EOF
appendonlylist.stable.example.com/testlist configured
但是,如果您現在嘗試刪除一個元素,將觸發來自驗證規則的錯誤。
kubectl apply -f - <<EOF
---
apiVersion: stable.example.com/v1
kind: AppendOnlyList
metadata:
name: testlist
value:
- name: container1
image: nginx/nginx
EOF
The AppendOnlyList "testlist" is invalid: value: Invalid value: "array": Values may only be added
此外,一旦欄位被設定,試圖刪除該欄位也是類型範圍驗證規則所不允許的。
kubectl apply -f - <<EOF
---
apiVersion: stable.example.com/v1
kind: AppendOnlyList
metadata:
name: testlist
EOF
The AppendOnlyList "testlist" is invalid: <nil>: Invalid value: "object": Value is required once set
生成的 Schema
openAPIV3Schema:
properties:
value:
items: ...
maxItems: 100
type: array
x-kubernetes-validations:
- message: Values may only be added
rule: oldSelf.all(x, x in self)
type: object
x-kubernetes-validations:
- message: Value is required once set
rule: '!has(oldSelf.value) || has(self.value)'
鍵只可追加,值不可變的對映
// A map which does not allow keys to be removed or their values changed once set. New keys may be added, however.
// +kubebuilder:validation:XValidation:rule="!has(oldSelf.values) || has(self.values)", message="Value is required once set"
type MapAppendOnlyKeys struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
// +kubebuilder:validation:Optional
// +kubebuilder:validation:MaxProperties=10
// +kubebuilder:validation:XValidation:rule="oldSelf.all(key, key in self && self[key] == oldSelf[key])",message="Keys may not be removed and their values must stay the same"
Values map[string]string `json:"values,omitempty"`
}
- 一旦設定,欄位不得刪除:
!has(oldSelf.values) || has(self.values)
(類型範圍) - 一旦添加了鍵,就不能刪除其值,也不能修改其值:
oldSelf.all(key, key in self && self[key] == oldSelf[key])
(欄位範圍) - 值最初可以未設定:
+kubebuilder:validation:Optional
用法示例
生成並安裝 CRD 應該成功
# Ensure the CRD yaml is generated by controller-gen
go generate ./...
kubectl apply -f crds/stable.example.com_mapappendonlykeys.yaml
customresourcedefinition.apiextensions.k8s.io/mapappendonlykeys.stable.example.com created
建立一個包含 values
中一個鍵的初始物件應該被允許
kubectl apply -f - <<EOF
---
apiVersion: stable.example.com/v1
kind: MapAppendOnlyKeys
metadata:
name: testmap
values:
key1: value1
EOF
mapappendonlykeys.stable.example.com/testmap created
向對映新增新鍵也應該被允許
kubectl apply -f - <<EOF
---
apiVersion: stable.example.com/v1
kind: MapAppendOnlyKeys
metadata:
name: testmap
values:
key1: value1
key2: value2
EOF
mapappendonlykeys.stable.example.com/testmap configured
但是,如果刪除了一個鍵,應該返回來自驗證規則的錯誤訊息
kubectl apply -f - <<EOF
---
apiVersion: stable.example.com/v1
kind: MapAppendOnlyKeys
metadata:
name: testmap
values:
key1: value1
EOF
The MapAppendOnlyKeys "testmap" is invalid: values: Invalid value: "object": Keys may not be removed and their values must stay the same
如果整個欄位被刪除,則會觸發另一個驗證規則,並且操作被阻止。請注意,驗證規則的錯誤訊息會顯示給使用者。
kubectl apply -f - <<EOF
---
apiVersion: stable.example.com/v1
kind: MapAppendOnlyKeys
metadata:
name: testmap
EOF
The MapAppendOnlyKeys "testmap" is invalid: <nil>: Invalid value: "object": Value is required once set
生成的 Schema
openAPIV3Schema:
description: A map which does not allow keys to be removed or their values
changed once set. New keys may be added, however.
properties:
values:
additionalProperties:
type: string
maxProperties: 10
type: object
x-kubernetes-validations:
- message: Keys may not be removed and their values must stay the same
rule: oldSelf.all(key, key in self && self[key] == oldSelf[key])
type: object
x-kubernetes-validations:
- message: Value is required once set
rule: '!has(oldSelf.values) || has(self.values)'
更進一步
上面的示例展示瞭如何將 CEL 規則新增到 kubebuilder 型別。如果手動編寫 CRD 的清單,同樣的規則也可以直接新增到 OpenAPI 中。
對於原生型別,可以使用 kube-openapi 的標記 +validations
來實現相同的行為。
在 Kubernetes 驗證規則中使用 CEL 比本文所示的強大得多。有關更多資訊,請檢視 Kubernetes 文件中的驗證規則和CRD 驗證規則 Beta 部落格文章。