本文发布时间已超过一年。较旧的文章可能包含过时的内容。请检查页面中的信息自发布以来是否已不正确。

使用 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 模块的文件夹结构。如果您已经设置了自己的项目,请随时根据自己的喜好调整本教程

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

生成的架构

请注意,在生成的架构中有两个单独的规则位置。一个直接附加到属性 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)'

带有仅附加键的映射,不可变的值

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

生成的架构

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