本文发表于一年多前。旧文章可能包含过时内容。请检查页面中的信息自发布以来是否已变得不正确。

使用 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 博客文章。