这篇文章已超过一年。较旧的文章可能包含过时的内容。请检查页面中的信息自发布以来是否仍然正确。
使用 CEL 转换规则强制执行 CRD 不变性
内置 Kubernetes 类型中存在一些不可变字段。例如,你不能更改对象的 .metadata.name
。特定对象有一些字段,对现有对象的更改受到限制;例如,Deployment 的 .spec.selector
。
除了简单的不可变性之外,还有其他常见的设计模式,例如只允许追加的列表,或者值可变但键不可变的映射。
直到最近,限制 CustomResourceDefinitions 字段可变性的最佳方法是创建一个验证 准入 Webhook:对于使字段不可变的常见情况,这意味着增加了很多复杂性。
从 Kubernetes 1.25 开始进入 Beta 阶段,CEL 验证规则允许 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 注释一起使用,首先需要设置一个 Golang 项目结构,并在 Go 中定义 CRD。
如果你不使用 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
生成的模式
请注意,在生成的模式中,有两个独立的规则位置。一个直接附加到属性 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"`
}
- 一旦设置,字段不允许删除:
!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
生成的模式
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
生成的模式
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 博客文章。