本文发布时间已超过一年。较旧的文章可能包含过时的内容。请检查页面中的信息自发布以来是否已不正确。
使用 CEL 转换规则强制 CRD 不可变性
不可变字段可以在内置的 Kubernetes 类型中的几个地方找到。例如,您不能更改对象的 .metadata.name
。特定对象的字段对现有对象的更改是受约束的;例如,Deployment 的 .spec.selector
。
除了简单的不可变性之外,还有其他常见的设计模式,例如仅附加的列表,或者具有可变值和不可变键的映射。
直到最近,限制 CustomResourceDefinitions 字段可变性的最佳方法是创建一个验证 准入 Webhook:这意味着对于使字段不可变的常见情况来说,存在很多复杂性。
自 Kubernetes 1.25 起为 Beta 版本,CEL 验证规则允许 CRD 作者使用丰富的表达式语言 CEL 在其字段上表达验证约束。本文探讨了如何使用验证规则在 CRD 的清单中直接实现一些常见的不可变性模式。
验证规则的基础知识
Kubernetes 中对 CEL 验证规则的新支持允许 CRD 作者为其资源添加复杂的准入逻辑,而无需编写任何代码!
例如,一个 CEL 规则,将 CRD 的字段 maximumSize
约束为大于 minimumSize
,可能如下所示
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 清单,仍然可以继续操作。
项目设置
要将 CEL 规则与 kubebuilder 注释一起使用,您首先需要设置一个在 Go 中定义了 CRD 的 Golang 项目结构。
如果您不使用 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 博客文章。