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

使用 CEL 轉換規則強制執行 CRD 不可變性

在內建的 Kubernetes 類型中,可以在幾個地方找到不可變的欄位。例如,您無法變更物件的 .metadata.name。特定物件具有欄位,其中對現有物件的變更受到限制;例如,Deployment 的 .spec.selector

除了簡單的不可變性之外,還有其他常見的設計模式,例如僅附加的清單,或具有可變值和不可變鍵的地圖。

直到最近,限制 CustomResourceDefinitions 的欄位可變性的最佳方法是建立驗證 准入 webhook:這表示對於常見的使欄位不可變的情況來說,複雜性很高。

自 Kubernetes 1.25 以來處於 Beta 階段,CEL 驗證規則允許 CRD 作者使用豐富的表達式語言 CEL 在其欄位上表達驗證約束。本文探討了如何使用驗證規則直接在 CRD 的 manifest 中實作一些常見的不可變性模式。

驗證規則基礎知識

Kubernetes 中對 CEL 驗證規則的新支援允許 CRD 作者為其資源新增複雜的准入邏輯,而無需編寫任何程式碼!

例如,將 CRD 的欄位 maximumSize 約束為大於 minimumSize 的 CEL 規則可能如下所示

rule: |
    self.maximumSize > self.minimumSize    
message: 'Maximum size must be greater than minimum size.'

rule 欄位包含以 CEL 撰寫的表達式。self 是 CEL 中的特殊關鍵字,指的是類型包含規則的物件。

message 欄位是錯誤訊息,每當不滿足此特定規則時,都會將其傳送給 Kubernetes 用戶端。

有關使用 CEL 的驗證規則的功能和限制的更多詳細資訊,請參閱 驗證規則CEL 規格也是專門針對該語言的資訊的良好參考。

使用 CEL 驗證規則的不可變性模式

本節使用表示為 kubebuilder 標記註解 的驗證規則,實作 Kubernetes CustomResourceDefinitions 中不可變性的幾個常見用例。kubebuilder 標記註解產生的 OpenAPI 結果也將包含在內,以便如果您手動編寫 CRD manifest,仍然可以繼續學習。

專案設定

若要將 CEL 規則與 kubebuilder 註解搭配使用,您首先需要設定具有 Go 中定義的 CRD 的 Golang 專案結構。

如果您未使用 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 的 manifest 現在可在 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

產生的結構描述

請注意,在產生的結構描述中,有兩個不同的規則位置。一個直接附加到屬性 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

產生的結構描述

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

產生的結構描述

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)'

具有僅允許附加鍵、不可變值的 Map

// 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

應允許將新鍵新增至 map

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

產生的結構描述

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 的 manifest,則相同的規則可以直接新增至 OpenAPI。

對於原生類型,可以使用 kube-openapi 的標記 +validations 來達成相同的行為。

在 Kubernetes 驗證規則中使用 CEL 比本文中展示的更強大。如需更多資訊,請查看 Kubernetes 文件中的驗證規則,以及 CRD 驗證規則 Beta 部落格文章。