本文發表於一年多前。舊文章可能包含過時內容。請檢查頁面中的資訊自發布以來是否已變得不正確。

使用 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 模組資料夾結構開始。如果您已經設定了自己的專案,請隨意根據您的喜好調整本教程。

graph LR . --> generate.go . --> pkg --> apis --> stable.example.com --> v1 v1 --> doc.go v1 --> types.go . --> tools.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-gencontroller-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 ./...

執行這些命令後,您已完成基本的專案結構。您的資料夾樹應如下所示:

graph LR . --> crds --> stable.example.com_testcrds.yaml . --> generate.go . --> go.mod . --> go.sum . --> pkg --> apis --> stable.example.com --> v1 v1 --> doc.go v1 --> types.go . --> tools.go

示例 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 規範來強制執行我們的約束。

要強制欄位在首次寫入後不可變,您需要應用以下約束:

  1. 欄位最初可以未設定:+kubebuilder:validation:Optional
  2. 一旦設定,欄位不允許被刪除:!has(oldSelf.value) | has(self.value)(類型範圍規則)
  3. 一旦設定,欄位不允許更改值: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 是允許的,因為 valueoptional

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"`
}
  1. 一旦設定,欄位不得刪除:!has(oldSelf.value) || has(self.value)(類型範圍)
  2. 一旦新增值,它就不能被刪除:oldSelf.all(x, x in self)(欄位範圍)
  3. 值最初可以未設定:+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"`
}
  1. 一旦設定,欄位不得刪除:!has(oldSelf.values) || has(self.values)(類型範圍)
  2. 一旦添加了鍵,就不能刪除其值,也不能修改其值:oldSelf.all(key, key in self && self[key] == oldSelf[key])(欄位範圍)
  3. 值最初可以未設定:+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 部落格文章。