介绍
[!Tip] 心急的读者可以直接跳到 快速开始。
这适合谁
Kubernetes 的用户
Kubernetes 用户将通过学习 API 设计和实现的基本概念,深入理解 Kubernetes。 本书将教会读者如何开发自己的 Kubernetes API 以及核心 Kubernetes API 设计的原则。
包括:
- Kubernetes API 和资源的结构
- API 版本控制语义
- 自我修复
- 垃圾收集与终结器
- 声明式 API 与 命令式 API
- 基于级别 vs 基于边沿的 API
- 资源与子资源
Kubernetes API 扩展开发者
API 扩展开发者将学习实现规范 Kubernetes API 的原则和概念,以及用于快速执行的简单工具和库。 本书涵盖了扩展开发者常遇到的陷阱和误解。
包括:
- 如何将多个事件批量合并到一次对账调用中
- 如何配置定期对账
- 即将到来
- 何时使用列出缓存与实时查找
- 垃圾回收(Garbage Collection)与终结器(Finalizers)
- 如何使用声明式验证与 Webhook 验证
- 如何实现 API 版本控制
为什么选择 Kubernetes API
Kubernetes API 为遵循一致且丰富结构的对象提供统一且定义清晰的端点。
这种方式促进了丰富的工具和库生态系统与 Kubernetes API 进行交互。
用户通过将对象声明为 YAML 或 JSON 配置,并使用通用工具来管理这些对象,从而使用 API。
将服务构建为 Kubernetes API 相较于传统的REST,有以下优势:
- 托管的 API 网络端点、存储和验证。
- 丰富的工具和命令行接口,例如
kubectl和kustomize。 - 支持身份验证(AuthN)和细粒度授权(AuthZ)。
- 通过 API 版本控制和转换并且支持 API 版本演进。
- 促进自适应/自愈 API 的实现,它们能够在没有用户干预的情况下持续响应系统状态的变化。
- Kubernetes 是一个托管环境
开发者可以构建并发布自己的 Kubernetes API,方便安装到运行中的 Kubernetes 集群中。
贡献
如果您想为本书或代码贡献内容,请先阅读我们的 贡献 指南。
资源
-
Slack频道:#kubebuilder
-
Google 群组: kubebuilder@googlegroups.com
架构概念图
下面的图表将帮助您更好地了解 Kubebuilder 的概念和架构。
快速开始
本快速入门指南将涵盖:
前提条件
安装
安装 kubebuilder:
# 下载 kubebuilder 并在本地安装。
curl -L -o kubebuilder "https://go.kubebuilder.io/dl/latest/$(go env GOOS)/$(go env GOARCH)"
chmod +x kubebuilder && sudo mv kubebuilder /usr/local/bin/
创建一个项目
创建一个目录,然后在其中运行 init 命令以初始化新项目。示例如下:
mkdir -p ~/projects/guestbook
cd ~/projects/guestbook
kubebuilder init --domain my.domain --repo my.domain/guestbook
创建一个API
运行以下命令以创建一个新的 API(组/版本)webapp/v1,并在其上创建新的 Kind(CRD)Guestbook:
kubebuilder create api --group webapp --version v1 --kind Guestbook
可选: 编辑 API 定义和调整业务逻辑。有关更多信息,请参见 设计 API 和 控制器中的内容。
如果您正在编辑 API 定义,请使用生成清单,例如自定义资源(CRs)或自定义资源定义(CRDs)。
make manifests
Click here to see an example. (api/v1/guestbook_types.go)
// GuestbookSpec 定义了 Guestbook 的期望状态
type GuestbookSpec struct {
// 插入额外的规格字段 - 集群的期望状态
// 注意:修改此文件后,运行"make"以重新生成代码
// 实例数量
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:Maximum=10
Size int32 `json:"size"`
// GuestbookSpec 配置的 ConfigMap 名称
// +kubebuilder:validation:MaxLength=15
// +kubebuilder:validation:MinLength=1
ConfigMapName string `json:"configMapName"`
// +kubebuilder:validation:Enum=电话;地址;名称
Type string `json:"type,omitempty"`
}
// GuestbookStatus 定义了 Guestbook 的观察状态
type GuestbookStatus struct {
// 插入额外的状态字段 - 定义集群的观察状态
// 重要:在修改此文件后,请运行 "make" 以重新生成代码
// 活动的 Guestbook 节点的 Pod 名称。
Active string `json:"active"`
// 待命 Guestbook 节点的 Pod 名称。
Standby []string `json:"standby"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:scope=Cluster
// Guestbook 是 guestbooks API 的 Schema
type Guestbook struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec GuestbookSpec `json:"spec,omitempty"`
Status GuestbookStatus `json:"status,omitempty"`
}
试一试
您需要一个可运行的 Kubernetes 集群。可以使用 KinD 获取本地测试集群,也可以针对远程集群运行。
将 CRD 安装到集群中:
make install
为了快速反馈和代码级调试,请运行控制器(该命令在前台运行;若希望保持运行,请另开终端):
make run
安装自定义资源实例
如果在 Create Resource [y/n] 时选择了 y,则会在 config/samples/ 目录中为 CRD 创建示例 CR。
编辑 config/samples/webapp_v1_guestbook.yaml,填入有效的 spec。例如:
# ...
spec:
foo: bar
提示:“foo” 是在 api/v1/guestbook_types.go 中定义的字符串字段:
// foo 是 Guestbook 的示例字段。请编辑 guestbook_types.go 以删除/更新
// +optional
Foo *string `json:"foo,omitempty"`
kubectl apply -k config/samples/
现在可以查看已应用的资源:
kubectl get guestbooks.webapp.my.domain guestbook-sample -o yaml
在集群中运行
当您的控制器准备好进行打包并在其他集群中测试时。
构建并将您的镜像推送到 IMG 所指定的位置:
make docker-build docker-push IMG=<some-registry>/<project-name>:tag
将控制器部署到集群,使用 IMG 指定的镜像:
make deploy IMG=<some-registry>/<project-name>:tag
卸载 CRD(自定义资源定义)
要从集群中删除您的 CRD:
make uninstall
取消部署控制器
将控制器从集群中撤销部署:
make undeploy
使用插件
Kubebuilder 的设计基于插件,您可以使用可用插件为项目添加可选功能。
后续步骤
- 入门指南(约 30 分钟)— 打下坚实基础
- CronJob 教程 — 通过构建示例项目学习
- 组、版本与种类 — 理解 API 设计概念
入门
本指南创建一个示例项目以演示其工作方式。该示例:
- 对 Memcached CR 进行调整 - 该 CR 表示在集群上部署/管理的 Memcached 实例
- 使用 Memcached 镜像创建一个部署
- 不允许超过 CR 中定义的 size 的实例数
- 更新 Memcached CR 状态
创建一个项目
首先,创建并导航到你的项目目录。然后,使用 kubebuilder 初始化它:
mkdir $GOPATH/memcached-operator
cd $GOPATH/memcached-operator
kubebuilder init --domain=example.com
创建 Memcached API(CRD)
接下来,创建负责在集群上部署并管理 Memcached 实例的 API。
kubebuilder create api --group cache --version v1alpha1 --kind Memcached
理解API
该命令的主要目的是为 Memcached Kind 生成自定义资源(CR)与自定义资源定义(CRD)。它创建 group 为 cache.example.com、version 为 v1alpha1 的 API,唯一标识 Memcached Kind 的新 CRD。借助 Kubebuilder,您可以定义代表这些平台上解决方案的 API 与对象。
本示例只添加一种 Kind 资源,但您可以按需拥有任意数量的 Groups 与 Kinds。为便于理解,可将 CRD 视为自定义对象的定义,而 CR 是它们的实例。
定义我们的 API
定义 Spec
现在定义集群上每个 Memcached 资源实例可取的值。本例中,配置允许用如下方式设置实例数量:
type MemcachedSpec struct {
...
// +kubebuilder:validation:Minimum=0
// +required
Size *int32 `json:"size,omitempty"`
}
创建 Status 定义
控制器还需要跟踪为管理 Memcached CR 所执行操作的状态。这样可以核验自定义资源对 API 的描述,并判断是否全部成功或是否遇到错误,方式与处理任何 Kubernetes API 资源相同。
// MemcachedStatus 定义了 Memcached 的观察状态
type MemcachedStatus struct {
// +listType=map
// +listMapKey=type
// +optional
Conditions []metav1.Condition `json:"conditions,omitempty"`
}
标记和验证
此外,应对 CustomResource 中的取值进行校验以确保有效。为此可使用标记,例如 +kubebuilder:validation:Minimum=1。
现在,请查看我们完全完成的示例。
Apache License
Copyright 2026 The Kubernetes authors.
Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
Imports
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
// 请编辑本文件!这是供您拥有的脚手架代码!
// 注意:需要 json 标签。新增字段必须带有 json 标签才能被序列化。
// MemcachedSpec 定义 Memcached 的期望状态
type MemcachedSpec struct {
// 在此插入额外 SPEC 字段 — 集群期望状态
// 重要:修改本文件后请运行 "make" 重新生成代码
// 下列标记将使用 OpenAPI v3 schema 校验取值
// 更多信息: https://book.kubebuilder.io/reference/markers/crd-validation.html
// size 定义 Memcached 实例数量
// 下列标记将使用 OpenAPI v3 schema 校验取值
// 更多信息: https://book.kubebuilder.io/reference/markers/crd-validation.html
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:Maximum=3
// +kubebuilder:validation:ExclusiveMaximum=false
// +optional
Size *int32 `json:"size,omitempty"`
}
// MemcachedStatus 定义 Memcached 的观测状态.
type MemcachedStatus struct {
// 插入额外的状态字段 - 定义集群的观察状态
// 重要:在修改此文件后,请运行 "make" 以重新生成代码
// Kubernetes API 约定参见:
// https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties
// conditions 表示 Memcached 资源的当前状态。
// 每个 condition 有唯一类型,反映资源某一方面的状态。
//
// Standard condition types include:
// - "Available": the resource is fully functional
// - "Progressing": the resource is being created or updated
// - "Degraded": the resource failed to reach or maintain its desired state
//
// The status of each condition is one of True, False, or Unknown.
// +listType=map
// +listMapKey=type
// +optional
Conditions []metav1.Condition `json:"conditions,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// Memcached 是 memcacheds API 的 Schema
type Memcached struct {
metav1.TypeMeta `json:",inline"`
// metadata 是标准对象元数据
// +optional
metav1.ObjectMeta `json:"metadata,omitzero"`
// spec 定义 Memcached 的期望状态
// +required
Spec MemcachedSpec `json:"spec"`
// status 定义 Memcached 的观测状态
// +optional
Status MemcachedStatus `json:"status,omitzero"`
}
// +kubebuilder:object:root=true
// MemcachedList 包含 Memcached 的列表
type MemcachedList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitzero"`
Items []Memcached `json:"items"`
}
func init() {
SchemeBuilder.Register(func(s *runtime.Scheme) error {
s.AddKnownTypes(SchemeGroupVersion, &Memcached{}, &MemcachedList{})
return nil
})
}
生成具有规格和验证的清单
生成所有所需的文件:
-
运行
make generate,会在api/v1alpha1/zz_generated.deepcopy.go文件中添加深度拷贝(DeepCopy)内容。 -
然后运行
make manifests,在config/crd/bases下生成 CRD 清单,并在config/samples下生成示例。
这两个命令都使用 controller-gen 工具,但是用于代码和清单生成的标志会有所不同。
config/crd/bases/cache.example.com_memcacheds.yaml: Our Memcached CRD
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.22.0
name: memcacheds.cache.example.com
spec:
group: cache.example.com
names:
kind: Memcached
listKind: MemcachedList
plural: memcacheds
singular: Memcached
scope: Namespaced
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
description: Memcached 是 memcacheds API 的 Schema
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: spec defines the desired state of Memcached
properties:
size:
description: |-
size defines the number of Memcached instances
The following markers will use OpenAPI v3 schema to validate the value
More info: https://book.kubebuilder.io/reference/markers/crd-validation.html
format: int32
maximum: 3
minimum: 1
type: integer
type: object
status:
description: status defines the observed state of Memcached
properties:
conditions:
description: |-
conditions represent the current state of the Memcached resource.
Each condition has a unique type and reflects the status of a specific aspect of the resource.
Standard condition types include:
- "Available": the resource is fully functional
- "Progressing": the resource is being created or updated
- "Degraded": the resource failed to reach or maintain its desired state
The status of each condition is one of True, False, or Unknown.
items:
description: Condition contains details for one aspect of the current
state of this API Resource.
properties:
lastTransitionTime:
description: |-
lastTransitionTime is the last time the condition transitioned from one status to another.
This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: |-
message is a human readable message indicating details about the transition.
This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: |-
observedGeneration represents the .metadata.generation that the condition was set based upon.
For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date
with respect to the current state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: |-
reason contains a programmatic identifier indicating the reason for the condition's last transition.
Producers of specific condition types may define expected values and meanings for this field,
and whether the values are considered a guaranteed API.
The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
type: object
required:
- spec
type: object
served: true
storage: true
subresources:
status: {}
自定义资源示例
在 config/samples 目录下的清单,可以应用于集群的自定义资源示例。在这个例子中,通过将给定的资源应用于集群,我们将生成一个实例大小为1的部署(请参见 size: 1)。
apiVersion: cache.example.com/v1alpha1
kind: Memcached
metadata:
labels:
app.kubernetes.io/name: project
app.kubernetes.io/managed-by: kustomize
name: memcached-sample
spec:
# TODO(用户):编辑以下值以确保您的操作数在集群中的 Pod/实例数量
size: 1
协调过程
简单来说,Kubernetes 允许声明系统期望状态,控制器持续观察集群并采取行动,使实际状态与期望状态一致。自定义 API 与控制器的过程类似。请记住,您是在扩展 Kubernetes 行为及其 API 以满足特定需求。
在控制器中,我们实现协调过程。
本质上, reconciliation 过程作为一个循环运行,不断检查条件并执行必要的操作,直到达到所需的状态。该过程将持续运行,直到系统中的所有条件与我们实施中定义的所需状态一致。
这是一个伪代码示例来说明这一点:
reconcile App {
// 检查应用的 Deployment 是否存在,若不存在则创建
// 若出错,则从协调过程开头重新开始
if err != nil {
return reconcile.Result{}, err
}
// 检查应用的 Service 是否存在,若不存在则创建
// 若出错,则从协调过程开头重新开始
if err != nil {
return reconcile.Result{}, err
}
// 查找 Database CR/CRD
// 检查 Database Deployment 的副本数
// 若 deployment.replicas 与 cr.size 不匹配, then update it
// Then, requeue the reconciliation to ensure the desired state is reached. For example, by returning `reconcile.Result{RequeueAfter: time.Minute}, nil`.
if err != nil {
return reconcile.Result{RequeueAfter: time.Minute}, nil
}
...
// 若循环结束时:
// 全部执行成功,协调可以停止
return reconcile.Result{}, nil
}
在我们例子的背景下。
将示例自定义资源(CR)应用到集群时(即 kubectl apply -f config/sample/cache_v1alpha1_memcached.yaml),确保控制器为 Memcached 镜像创建 Deployment,且副本数与 CR 中定义一致。
为此,首先实现检查集群上是否已有该 Memcached 实例的 Deployment 的操作;若不存在,控制器再创建。因此协调过程必须包含用于持续维持该期望状态的操作,例如:
// 检查部署是否已经存在,如果不存在则创建一个新部署。
found := &appsv1.Deployment{}
err = r.Get(ctx, types.NamespacedName{Name: memcached.Name, Namespace: memcached.Namespace}, found)
if err != nil && apierrors.IsNotFound(err) {
// 定义新的部署
dep := r.deploymentForMemcached()
// 在集群上创建部署
if err = r.Create(ctx, dep); err != nil {
log.Error(err, "无法创建新的部署",
"Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
return ctrl.Result{}, err
}
...
}
接下来注意,deploymentForMemcached() 需要定义并返回应在集群上创建的 Deployment。该函数应按必要规格构造 Deployment 对象,如下例所示:
dep := &appsv1.Deployment{
Spec: appsv1.DeploymentSpec{
Replicas: &replicas,
Template: corev1.PodTemplateSpec{
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Image: "memcached:1.6.26-alpine3.19",
Name: "memcached",
ImagePullPolicy: corev1.PullIfNotPresent,
Ports: []corev1.ContainerPort{{
ContainerPort: 11211,
Name: "memcached",
}},
Command: []string{"memcached", "--memory-limit=64", "-o", "modern", "-v"},
}},
},
},
},
}
此外,实现机制以校验集群上 Memcached 副本数是否与 Custom Resource(CR)中的期望数量一致。若不一致,协调必须更新集群以保持一致。这意味着每当创建或更新 Memcached Kind 的 CR 时,控制器会持续协调,直到实际副本数与期望一致。下例说明该过程:
...
size := memcached.Spec.Size
if *found.Spec.Replicas != size {
found.Spec.Replicas = &size
if err = r.Update(ctx, found); err != nil {
log.Error(err, "Failed to update Deployment",
"Deployment.Namespace", found.Namespace, "Deployment.Name", found.Name)
return ctrl.Result{}, err
}
...
现在,您可以查看负责管理 Memcached 类型自定义资源的完整控制器。该控制器确保集群中维持所需状态,确保我们的 Memcached 实例继续运行,并保持用户指定的副本数量。
internal/controller/memcached_controller.go: Our Controller Implementation
/*
Copyright 2026 The Kubernetes authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package controller
import (
"context"
"fmt"
"time"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
logf "sigs.k8s.io/controller-runtime/pkg/log"
cachev1alpha1 "example.com/memcached/api/v1alpha1"
)
// 管理状态条件的定义
const (
// typeAvailableMemcached 表示部署调整的状态
typeAvailableMemcached = "Available"
)
const memcachedContainerName = "memcached"
// MemcachedReconciler 负责调整 Memcached 对象。
type MemcachedReconciler struct {
client.Client
Scheme *runtime.Scheme
}
// +kubebuilder:rbac:groups=cache.example.com,resources=memcacheds,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=cache.example.com,resources=memcacheds/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=cache.example.com,resources=memcacheds/finalizers,verbs=update
// +kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch
// Reconcile 是主 kubernetes 协调循环的一部分,其目标是
// 使集群当前状态更接近期望状态。
// It is essential for the controller's reconciliation loop to be idempotent. By following the Operator
// pattern you will create Controllers which provide a reconcile function
// responsible for synchronizing resources until the desired state is reached on the cluster.
// Breaking this recommendation goes against the design principles of controller-runtime.
// and may lead to unforeseen consequences such as resources becoming stuck and requiring manual intervention.
// For further info:
// - About Operator Pattern: https://kubernetes.io/docs/concepts/extend-kubernetes/operator/
// - About Controllers: https://kubernetes.io/docs/concepts/architecture/controller/
//
// 更多细节参见 Reconcile 及其 Result:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.25.0/pkg/reconcile
func (r *MemcachedReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := logf.FromContext(ctx)
// 获取 Memcached 实例
// 目的是检查 Kind Memcached 的自定义资源是否
// 已在集群上应用,如果没有,我们返回 nil 以停止对账过程
memcached := &cachev1alpha1.Memcached{}
err := r.Get(ctx, req.NamespacedName, memcached)
if err != nil {
if apierrors.IsNotFound(err) {
// 如果未找到自定义资源,通常意味着它已被删除或未创建
// 这样,我们将停止调整过程
log.Info("Memcached resource not found. Ignoring since object must be deleted")
return ctrl.Result{}, nil
}
// 读取对象时出错 - 重新排队请求。
log.Error(err, "Failed to get memcached")
return ctrl.Result{}, err
}
// 当没有可用状态时,我们将状态设置为未知。
if len(memcached.Status.Conditions) == 0 {
meta.SetStatusCondition(&memcached.Status.Conditions, metav1.Condition{Type: typeAvailableMemcached, Status: metav1.ConditionUnknown, Reason: reasonReconciling, Message: "Starting reconciliation"})
if err = r.Status().Update(ctx, memcached); err != nil {
log.Error(err, "Failed to update Memcached status")
return ctrl.Result{}, err
}
// 在更新状态后,让我们重新获取 memcached 自定义资源
// 这样我们就可以获得集群中资源的最新状态,避免
// 引发错误"对象已被修改,请将
// 您的更改应用于最新版本并重试",这将在后续操作中
// 如果我们再次尝试更新它,将重新触发对账过程
if err := r.Get(ctx, req.NamespacedName, memcached); err != nil {
log.Error(err, "Failed to re-fetch memcached")
return ctrl.Result{}, err
}
}
// 检查部署是否已经存在,如果不存在则创建一个新部署。
found := &appsv1.Deployment{}
err = r.Get(ctx, types.NamespacedName{Name: memcached.Name, Namespace: memcached.Namespace}, found)
if err != nil && apierrors.IsNotFound(err) {
// 定义新的部署
dep, err := r.deploymentForMemcached(memcached)
if err != nil {
log.Error(err, "Failed to define new Deployment resource for Memcached")
// 以下实现将更新状态
meta.SetStatusCondition(&memcached.Status.Conditions, metav1.Condition{Type: typeAvailableMemcached,
Status: metav1.ConditionFalse, Reason: reasonReconciling,
Message: fmt.Sprintf("Failed to create Deployment for the custom resource (%s): (%s)", memcached.Name, err)})
if err := r.Status().Update(ctx, memcached); err != nil {
log.Error(err, "Failed to update Memcached status")
return ctrl.Result{}, err
}
return ctrl.Result{}, err
}
log.Info("Creating a new Deployment",
"Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
if err = r.Create(ctx, dep); err != nil {
log.Error(err, "无法创建新的部署",
"Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
return ctrl.Result{}, err
}
// 部署成功创建
// 我们将重新排队调整,以确保状态
// 并继续进行下一步操作
return ctrl.Result{RequeueAfter: time.Minute}, nil
} else if err != nil {
log.Error(err, "Failed to get Deployment")
// 让我们返回错误,以便重新触发对账。
return ctrl.Result{}, err
}
// 若 Custom Resource 未定义 size,则将期望副本数设为 0
var desiredReplicas int32 = 0
if memcached.Spec.Size != nil {
desiredReplicas = *memcached.Spec.Size
}
// CRD API 定义了 Memcached 类型具有一个 MemcachedSpec.Size 字段
// 用于设置集群中部署实例的数量到所需状态。
// 因此,以下代码将确保部署的大小与我们正在对账的
// 自定义资源的 Size 规格中定义的相同。
if found.Spec.Replicas == nil || *found.Spec.Replicas != desiredReplicas {
found.Spec.Replicas = new(desiredReplicas)
if err = r.Update(ctx, found); err != nil {
log.Error(err, "Failed to update Deployment",
"Deployment.Namespace", found.Namespace, "Deployment.Name", found.Name)
// 在更新状态之前重新获取 memcached 自定义资源,
// 以便我们能够获得集群中资源的最新状态,并避免
// 引发错误"对象已被修改,请将您的更改应用于
// 最新版本并重试",这将重新触发调整。
if err := r.Get(ctx, req.NamespacedName, memcached); err != nil {
log.Error(err, "Failed to re-fetch memcached")
return ctrl.Result{}, err
}
// 以下实现将更新状态
meta.SetStatusCondition(&memcached.Status.Conditions, metav1.Condition{Type: typeAvailableMemcached,
Status: metav1.ConditionFalse, Reason: "Resizing",
Message: fmt.Sprintf("Failed to update the size for the custom resource (%s): (%s)", memcached.Name, err)})
if err := r.Status().Update(ctx, memcached); err != nil {
log.Error(err, "Failed to update Memcached status")
return ctrl.Result{}, err
}
return ctrl.Result{}, err
}
// 现在我们更新了大小,我们想要重新排队对账
// 以便确保在更新之前我们拥有资源的最新状态。
// 此外,它还将有助于确保集群上的期望状态。
return ctrl.Result{RequeueAfter: time.Minute}, nil
}
// 以下实现将更新状态
meta.SetStatusCondition(&memcached.Status.Conditions, metav1.Condition{Type: typeAvailableMemcached,
Status: metav1.ConditionTrue, Reason: reasonReconciling,
Message: fmt.Sprintf("自定义资源(%s)部署成功,创建了 %d 个副本", memcached.Name, desiredReplicas)})
if err := r.Status().Update(ctx, memcached); err != nil {
log.Error(err, "Failed to update Memcached status")
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
// SetupWithManager 将控制器与管理器进行设置。
func (r *MemcachedReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&cachev1alpha1.Memcached{}).
Owns(&appsv1.Deployment{}).
Named("memcached").
Complete(r)
}
// deploymentForMemcached 返回一个 Memcached 部署对象。
func (r *MemcachedReconciler) deploymentForMemcached(
memcached *cachev1alpha1.Memcached) (*appsv1.Deployment, error) {
image := "memcached:1.6.26-alpine3.19"
dep := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: memcached.Name,
Namespace: memcached.Namespace,
},
Spec: appsv1.DeploymentSpec{
Replicas: memcached.Spec.Size,
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{"app.kubernetes.io/name": "project"},
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{"app.kubernetes.io/name": "project"},
},
Spec: corev1.PodSpec{
SecurityContext: &corev1.PodSecurityContext{
RunAsNonRoot: new(true),
SeccompProfile: &corev1.SeccompProfile{
Type: corev1.SeccompProfileTypeRuntimeDefault,
},
},
Containers: []corev1.Container{{
Image: image,
Name: memcachedContainerName,
ImagePullPolicy: corev1.PullIfNotPresent,
// 确保容器的限制性上下文
// 详细信息请参考: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted
SecurityContext: &corev1.SecurityContext{
RunAsNonRoot: new(true),
RunAsUser: new(int64(1001)),
AllowPrivilegeEscalation: new(false),
Capabilities: &corev1.Capabilities{
Drop: []corev1.Capability{
"ALL",
},
},
},
Ports: []corev1.ContainerPort{{
ContainerPort: 11211,
Name: memcachedContainerName,
}},
Command: []string{"memcached", "--memory-limit=64", "-o", "modern", "-v"},
}},
},
},
},
}
// 设置 Deployment 的 ownerRef
// 更多信息: https://kubernetes.io/docs/concepts/overview/working-with-objects/owners-dependents/
if err := ctrl.SetControllerReference(memcached, dep, r.Scheme); err != nil {
return nil, err
}
return dep, nil
}
深入控制器实现
配置 Manager 监视资源
整个想法是关注对控制器重要的资源。当控制器感兴趣的资源发生变化时,Watch 会触发控制器的对账循环,确保资源的实际状态与控制器逻辑中定义的期望状态相匹配。
注意如何配置 Manager,以监视 Memcached Kind 自定义资源(CR)的创建、更新或删除,以及控制器所管理并拥有的 Deployment 的任何变更:
// SetupWithManager 将控制器与管理器设置起来。
// 部署也会被监视以确保其
// 在集群中的期望状态。
func (r *MemcachedReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
// 监视 Memcached 自定义资源,并在其
//创建、更新或删除时触发协调
For(&cachev1alpha1.Memcached{}).
// 监视由 Memcached 控制器管理的 Deployment。若 Deployment 发生任何变更
// owned and managed by this controller, it triggers reconciliation, ensuring that the cluster
// state aligns with the desired state.
Owns(&appsv1.Deployment{}).
Complete(r)
}
但是,Manager 如何知道哪些资源归它所有?
控制器不应监视集群上任意 Deployment 并触发协调循环,而应仅在运行 Memcached 实例的特定 Deployment 发生变化时触发。例如,若有人误删 Deployment 或更改副本数,应触发协调以恢复期望状态。
Manager 知道应观察哪个 Deployment,是因为设置了 ownerRef(所有者引用):
if err := ctrl.SetControllerReference(memcached, dep, r.Scheme); err != nil {
return nil, err
}
授予权限
务必确保控制器对其管理的资源具备必要权限(例如 create、get、update、list)。
通过 RBAC 标记 配置 RBAC 权限,controller-gen 据此生成并更新 config/rbac/ 中的清单。可在各控制器的 Reconcile() 方法上找到(并应定义)这些标记,参见示例实现:
// +kubebuilder:rbac:groups=cache.example.com,resources=memcacheds,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=cache.example.com,resources=memcacheds/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=cache.example.com,resources=memcacheds/finalizers,verbs=update
// +kubebuilder:rbac:groups=events.k8s.io,resources=events,verbs=create;patch
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch
修改控制器后,运行 make manifests。这将让 controller-gen 刷新 config/rbac 下的文件。
config/rbac/role.yaml: Our RBAC Role generated
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: manager-role
rules:
- apiGroups:
- ""
resources:
- pods
verbs:
- get
- list
- watch
- apiGroups:
- apps
resources:
- deployments
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- cache.example.com
resources:
- memcacheds
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- cache.example.com
resources:
- memcacheds/finalizers
verbs:
- update
- apiGroups:
- cache.example.com
resources:
- memcacheds/status
verbs:
- get
- patch
- update
- apiGroups:
- events.k8s.io
resources:
- events
verbs:
- create
- patch
监控程序 (main.go)
cmd/main.go 文件中的 Manager 负责管理您应用程序中的控制器。
cmd/main.go: Our main.go
/*
Copyright 2026 The Kubernetes authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"crypto/tls"
"flag"
"os"
// 导入所有 Kubernetes 客户端身份验证插件(例如 Azure、GCP、OIDC 等)
// 确保 exec-entrypoint 和 run 可以使用它们。
_ "k8s.io/client-go/plugin/pkg/client/auth"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/healthz"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
"sigs.k8s.io/controller-runtime/pkg/webhook"
cachev1alpha1 "example.com/memcached/api/v1alpha1"
"example.com/memcached/internal/controller"
// +kubebuilder:scaffold:imports
)
var (
scheme = runtime.NewScheme()
setupLog = ctrl.Log.WithName("setup")
)
func init() {
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
utilruntime.Must(cachev1alpha1.AddToScheme(scheme))
// +kubebuilder:scaffold:scheme
}
// nolint:gocyclo
func main() {
var metricsAddr string
var metricsCertPath, metricsCertName, metricsCertKey string
var webhookCertPath, webhookCertName, webhookCertKey string
var webhookPort int
var enableLeaderElection bool
var probeAddr string
var secureMetrics bool
var enableHTTP2 bool
var tlsOpts []func(*tls.Config)
flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+
"使用 :8443 进行 HTTPS,使用 :8080 进行 HTTP,或者保持为 0 以禁用指标服务。")
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
flag.BoolVar(&enableLeaderElection, "leader-elect", false,
"Enable leader election for controller manager. "+
"启用此功能将确保只有一个活动的控制管理器。")
flag.BoolVar(&secureMetrics, "metrics-secure", true,
"如果设置,指标端点将通过 HTTPS 安全提供。请使用 --metrics-secure=false 以改为使用 HTTP。")
flag.StringVar(&webhookCertPath, "webhook-cert-path", "", "The directory that contains the webhook certificate.")
flag.StringVar(&webhookCertName, "webhook-cert-name", "tls.crt", "The name of the webhook certificate file.")
flag.StringVar(&webhookCertKey, "webhook-cert-key", "tls.key", "The name of the webhook key file.")
flag.IntVar(&webhookPort, "webhook-port", 9443, "Port the webhook server listens on. "+
"Defaults to 9443. Set -1 to disable the webhook server.")
flag.StringVar(&metricsCertPath, "metrics-cert-path", "",
"The directory that contains the metrics server certificate.")
flag.StringVar(&metricsCertName, "metrics-cert-name", "tls.crt", "The name of the metrics server certificate file.")
flag.StringVar(&metricsCertKey, "metrics-cert-key", "tls.key", "The name of the metrics server key file.")
flag.BoolVar(&enableHTTP2, "enable-http2", false,
"If set, HTTP/2 will be enabled for the metrics and webhook servers")
opts := zap.Options{
Development: true,
}
opts.BindFlags(flag.CommandLine)
flag.Parse()
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
// 如果 enable-http2 标志为 false(默认值),则应禁用 http/2
// 由于其存在的漏洞。更具体地说,禁用 http/2 将
// 防止受到 HTTP/2 流取消和
// 快速重置 CVE 的漏洞影响。有关更多信息,请参见:
// - https://github.com/advisories/GHSA-qppj-fm5r-hxr3
// - https://github.com/advisories/GHSA-4374-p667-p6c8
disableHTTP2 := func(c *tls.Config) {
setupLog.Info("Disabling HTTP/2")
c.NextProtos = []string{"http/1.1"}
}
if !enableHTTP2 {
tlsOpts = append(tlsOpts, disableHTTP2)
}
// 初始Webhook TLS选项
webhookTLSOpts := tlsOpts
webhookServerOptions := webhook.Options{
TLSOpts: webhookTLSOpts,
Port: webhookPort,
}
if len(webhookCertPath) > 0 {
setupLog.Info("使用提供的证书初始化 Webhook 证书监视器",
"webhook-cert-path", webhookCertPath, "webhook-cert-name", webhookCertName, "webhook-cert-key", webhookCertKey)
webhookServerOptions.CertDir = webhookCertPath
webhookServerOptions.CertName = webhookCertName
webhookServerOptions.KeyName = webhookCertKey
}
webhookServer := webhook.NewServer(webhookServerOptions)
// Metrics 端点已在 'config/default/kustomization.yaml'. Metrics 选项用于配置服务器。
// 更多信息:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.25.0/pkg/metrics/server
// - https://book.kubebuilder.io/reference/metrics.html
metricsServerOptions := metricsserver.Options{
BindAddress: metricsAddr,
SecureServing: secureMetrics,
TLSOpts: tlsOpts,
}
if secureMetrics {
// FilterProvider 用于通过认证/鉴权保护 metrics 端点。
// These configurations ensure that only authorized users and service accounts
// can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info:
// https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.25.0/pkg/metrics/filters#WithAuthenticationAndAuthorization
metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization
}
// 如果未指定证书,controller-runtime 将自动
// 为指标服务器生成自签名证书。虽然对于开发和测试来说很方便,
// 但这种设置不建议用于生产环境。
//
// TODO(用户):如果您启用 certManager,请取消注释以下行:
// - [METRICS-WITH-CERTS] 在 config/default/kustomization.yaml 中生成并使用
// 由 cert-manager 管理的指标服务器证书。
// - [PROMETHEUS-WITH-CERTS] 在 config/prometheus/kustomization.yaml 中用于 TLS 认证。
if len(metricsCertPath) > 0 {
setupLog.Info("使用提供的证书初始化指标证书监视器",
"metrics-cert-path", metricsCertPath, "metrics-cert-name", metricsCertName, "metrics-cert-key", metricsCertKey)
metricsServerOptions.CertDir = metricsCertPath
metricsServerOptions.CertName = metricsCertName
metricsServerOptions.KeyName = metricsCertKey
}
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
Metrics: metricsServerOptions,
WebhookServer: webhookServer,
HealthProbeBindAddress: probeAddr,
LeaderElection: enableLeaderElection,
LeaderElectionID: "4b13cc52.example.com",
// LeaderElectionReleaseOnCancel 定义了当管理器结束时,领导者是否应自愿辞职
// 当管理器停止时,这要求二进制文件立即结束,否则这个设置是不安全的。设置这个可以显著
// 加快自愿领导者的过渡,因为新的领导者不必首先等待
// LeaseDuration 时间。
//
// 在提供的默认脚手架中,程序会在
// 管理器停止后立即结束,因此启用此选项是可以的。不过,
// 如果您正在进行或打算在管理器停止后执行任何操作,如执行清理,
// 那么使用它可能是不安全的。
// LeaderElectionReleaseOnCancel: true,
})
if err != nil {
setupLog.Error(err, "Failed to start manager")
os.Exit(1)
}
if err := (&controller.MemcachedReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "Failed to create controller", "controller", "memcached")
os.Exit(1)
}
// +kubebuilder:scaffold:builder
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
setupLog.Error(err, "Failed to set up health check")
os.Exit(1)
}
if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
setupLog.Error(err, "Failed to set up ready check")
os.Exit(1)
}
setupLog.Info("Starting manager")
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
setupLog.Error(err, "Failed to run manager")
os.Exit(1)
}
}
使用 Kubebuilder 插件脚手架附加选项
在更好理解如何创建自有 API 与控制器后,让我们在本项目中脚手架插件 autoupdate.kubebuilder.io/v1-alpha,以便项目能跟进最新 Kubebuilder 发行版的脚手架变更,从而采用生态中的改进。
kubebuilder edit --plugins="autoupdate/v1-alpha"
查看 .github/workflows/auto-update.yml 文件以了解其工作方式。
检查集群中运行的项目
此时,您可以通过查看快速入门中定义的步骤来检查在集群上验证项目的步骤,请参见:在集群上运行。
后续步骤
- 若要更深入地开发解决方案,可继续阅读 CronJob 教程
- 有关优化您方法的见解,请参阅最佳实践文档。
教程:构建 CronJob
太多教程开始时使用一些非常生硬的设置,或者是一些简单应用来讲解基础知识,然后在更复杂的内容上停滞不前。相反,本教程将带您几乎体验 Kubebuilder 的全部复杂性,从简单开始,逐渐构建到一个功能齐全的应用。
假设(是的,这有点牵强)你终于厌倦了维护 Kubernetes 中非 Kubebuilder 实现的 CronJob 控制器的负担,并希望用 Kubebuilder 重写它。
CronJob 控制器的工作(并不是双关)是在 Kubernetes 集群中定期运行一次性任务。它是通过在 Job 控制器之上构建来实现这一目标的,Job 控制器的任务是一次性运行任务,并确保其完成。
不必同时重写 Job 控制器,把这当作学习如何与外部类型交互的机会。
搭建我们的项目
正如在快速入门中所述,我们需要搭建一个新的项目。请确保您已安装 Kubebuilder,然后搭建一个新项目:
# 创建一个项目目录,然后运行初始化命令。
mkdir project
cd project
# 本示例使用域名 tutorial.kubebuilder.io,
# so all API groups is <group>.tutorial.kubebuilder.io.
kubebuilder init --domain tutorial.kubebuilder.io --repo tutorial.kubebuilder.io/project
现在项目已经就绪,来看看 Kubebuilder 目前脚手架了哪些内容……
一个基本项目都包含了什么?
在搭建新项目时,Kubebuilder 为我们提供了一些基本的模板代码。
构建基础设施
首先,构建项目所需的基本基础设施:
go.mod: A new Go module matching our project, with
basic dependencies
module tutorial.kubebuilder.io/project
go 1.26.0
require (
github.com/onsi/ginkgo/v2 v2.27.4
github.com/onsi/gomega v1.39.0
github.com/robfig/cron v1.2.0
k8s.io/api v0.37.0
k8s.io/apimachinery v0.37.0
k8s.io/client-go v0.37.0
sigs.k8s.io/controller-runtime v0.25.0
)
require (
cel.dev/expr v0.25.1 // indirect
github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.1 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-logr/zapr v1.3.0 // indirect
github.com/go-openapi/jsonpointer v1.0.0 // indirect
github.com/go-openapi/jsonreference v1.0.0 // indirect
github.com/go-openapi/swag v0.27.1 // indirect
github.com/go-openapi/swag/cmdutils v0.27.1 // indirect
github.com/go-openapi/swag/conv v0.27.1 // indirect
github.com/go-openapi/swag/fileutils v0.27.1 // indirect
github.com/go-openapi/swag/jsonutils v0.27.1 // indirect
github.com/go-openapi/swag/loading v0.27.1 // indirect
github.com/go-openapi/swag/mangling v0.27.1 // indirect
github.com/go-openapi/swag/netutils v0.27.1 // indirect
github.com/go-openapi/swag/pools v0.27.1 // indirect
github.com/go-openapi/swag/stringutils v0.27.1 // indirect
github.com/go-openapi/swag/typeutils v0.27.1 // indirect
github.com/go-openapi/swag/yamlutils v0.27.1 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/google/cel-go v0.29.2 // indirect
github.com/google/gnostic-models v0.7.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.24.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.70.0 // indirect
github.com/prometheus/procfs v0.21.1 // indirect
github.com/spf13/cobra v1.10.2 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect
go.opentelemetry.io/otel v1.44.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0 // indirect
go.opentelemetry.io/otel/metric v1.44.0 // indirect
go.opentelemetry.io/otel/sdk v1.44.0 // indirect
go.opentelemetry.io/otel/trace v1.44.0 // indirect
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.1 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
golang.org/x/mod v0.37.0 // indirect
golang.org/x/net v0.57.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sync v0.22.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/term v0.45.0 // indirect
golang.org/x/text v0.40.0 // indirect
golang.org/x/time v0.15.0 // indirect
golang.org/x/tools v0.47.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/grpc v1.82.1 // indirect
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
k8s.io/apiextensions-apiserver v0.37.0 // indirect
k8s.io/apiserver v0.37.0 // indirect
k8s.io/component-base v0.37.0 // indirect
k8s.io/klog/v2 v2.140.0 // indirect
k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad // indirect
k8s.io/streaming v0.37.0 // indirect
k8s.io/utils v0.0.0-20260626114624-be93311217bd // indirect
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.36.0 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.4.2 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
)
Makefile: Make targets for building and deploying your controller
# 用于所有构建/推送镜像目标的镜像 URL
IMG ?= controller:latest
# YEAR 定义用于替换样板文件头中 YEAR 占位符的年份值。
YEAR ?= $(shell date +%Y)
# 获取当前使用的 Go 安装路径(在 GOPATH/bin 中,除非设置了 GOBIN)
ifeq (,$(shell go env GOBIN))
GOBIN=$(shell go env GOPATH)/bin
else
GOBIN=$(shell go env GOBIN)
endif
# CONTAINER_TOOL 定义用于构建镜像的容器工具。
# 请注意,目标命令仅针对默认脚手架的 Docker 进行了测试。
# 不过,您可能想换成其他工具。
# (例如 podman)
CONTAINER_TOOL ?= docker
将SHELL设置为bash允许通过配方执行bash命令。
# 选项设置为:当配方行以非零退出或管道命令失败时退出。
SHELL = /usr/bin/env bash -o pipefail
.SHELLFLAGS = -ec
.PHONY: all
all: build
##@ 通用
# help 目标按类别组织并打印所有目标及其描述
# 。类别用 '##@' 表示,
# 目标描述用 '##' 表示。awk 命令负责读取
# 本次调用包含的全部 makefile,查找形如
# xyz: ## something 的行,并美化格式化目标与帮助。然后,
# 若有 ##@ something 行,则作为类别美化打印。
# 有关终端格式化 ANSI 控制字符用法的更多信息:
# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters
# 有关 awk 命令的更多信息:
# http://linuxcommand.org/lc3_adv_awk.php
.PHONY: help
help: ## 显示此帮助。
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
##@ 开发
.PHONY: manifests
manifests: controller-gen ## 生成 WebhookConfiguration、ClusterRole 和 CustomResourceDefinition 对象。
# 注意:默认脚手架加入了 maxDescLen=0 选项以解决该问题
# Too long: must have at most 262144 bytes。使用 kubectl apply 创建/更新资源时会写入注解
# 由 K8s API 创建,用于存储资源的最新版本 (kubectl.kubernetes.io/last-applied-configuration)。
# 但该注解有大小限制;若 CRD 过大且描述很长,会导致失败。
"$(CONTROLLER_GEN)" rbac:roleName=manager-role crd:maxDescLen=0 webhook paths="./..." output:crd:artifacts:config=config/crd/bases
.PHONY: generate
generate: controller-gen ## 生成包含 DeepCopy、DeepCopyInto 和 DeepCopyObject 方法实现的代码。
"$(CONTROLLER_GEN)" object:headerFile="hack/boilerplate.go.txt",year=$(YEAR) paths="./..."
.PHONY: fmt
fmt: ## 对代码运行 go fmt。
go fmt ./...
.PHONY: vet
vet: ## 对代码运行 go vet。
go vet ./...
.PHONY: test
test: manifests generate fmt vet setup-envtest ## 运行测试。
KUBEBUILDER_ASSETS="$(shell "$(ENVTEST)" use $(ENVTEST_K8S_VERSION) --bin-dir "$(LOCALBIN)" -p path)" go test $$(go list ./... | grep -v /e2e) -coverprofile cover.out
# TODO(user):若要为 e2e 测试使用其他厂商工具,请修改 tests/e2e 下的设置。
# 默认设置假定已预装 Kind,并在本地构建/加载 Manager Docker 镜像。
# 为测试隔离默认禁用 kubectl kuberc;启用方式:
# - KUBECTL_KUBERC=true
# 默认安装 CertManager;跳过方式:
# - CERT_MANAGER_INSTALL_SKIP=true
KIND_CLUSTER ?= project-test-e2e
.PHONY: setup-test-e2e
setup-test-e2e: ## 若不存在则为 e2e 测试搭建 Kind 集群
@command -v $(KIND) >/dev/null 2>&1 || { \
echo "Kind is not installed. Please install Kind manually."; \
exit 1; \
}
@case "$$($(KIND) get clusters)" in \
*"$(KIND_CLUSTER)"*) \
echo "Kind cluster '$(KIND_CLUSTER)' already exists. Skipping creation." ;; \
*) \
echo "Creating Kind cluster '$(KIND_CLUSTER)'..."; \
$(KIND) create cluster --name $(KIND_CLUSTER) ;; \
esac
.PHONY: test-e2e
test-e2e: setup-test-e2e manifests generate fmt vet ## 运行 e2e 测试。预期使用 Kind 的隔离环境。
KIND=$(KIND) KIND_CLUSTER=$(KIND_CLUSTER) go test -tags=e2e ./test/e2e/ -v -ginkgo.v
$(MAKE) cleanup-test-e2e
.PHONY: cleanup-test-e2e
cleanup-test-e2e: ## 拆除用于 e2e 测试的 Kind 集群
@$(KIND) delete cluster --name $(KIND_CLUSTER)
.PHONY: lint
lint: golangci-lint ## 运行 golangci-lint 检查器
"$(GOLANGCI_LINT)" run
.PHONY: lint-fix
lint-fix: golangci-lint ## 运行 golangci-lint 检查器并自动修复
"$(GOLANGCI_LINT)" run --fix
.PHONY: lint-config
lint-config: golangci-lint ## 校验 golangci-lint 配置
"$(GOLANGCI_LINT)" config verify
##@ 构建
.PHONY: build
build: manifests generate fmt vet ## 构建 manager 二进制文件。
go build -o bin/manager cmd/main.go
.PHONY: run
run: manifests generate fmt vet ## 在主机上运行控制器。
go run ./cmd/main.go
# 若要构建面向其他平台的 manager 镜像,可使用 --platform 标志。
# (例如 docker build --platform linux/arm64)。但必须为此启用 docker buildKit。
# 更多信息:https://docs.docker.com/develop/develop-images/build_enhancements/
# 覆盖 BASE_IMAGE 以从其他仓库构建,例如:
# make docker-build IMG=<img> BASE_IMAGE=docker.io/library/golang:1.26
.PHONY: docker-build
docker-build: ## 构建包含 manager 的 docker 镜像。
$(CONTAINER_TOOL) build $(if $(BASE_IMAGE),--build-arg BASE_IMAGE=$(BASE_IMAGE)) -t ${IMG} .
.PHONY: docker-push
docker-push: ## 推送包含 manager 的 docker 镜像。
$(CONTAINER_TOOL) push ${IMG}
# PLATFORMS 定义构建 manager 镜像的目标平台,以提供多
# 架构支持。(例如 make docker-buildx IMG=myregistry/mypoperator:0.0.1)。使用此选项需要:
# - 能够使用 docker buildx。更多信息:https://docs.docker.com/build/buildx/
# - 已启用 BuildKit。更多信息:https://docs.docker.com/develop/develop-images/build_enhancements/
# - 能够将镜像推送到仓库(即若未通过 IMG=<myregistry/image:<tag>> 设置有效值,导出将失败)
# 为充分提供兼容多平台的方案,应考虑使用此选项。
PLATFORMS ?= linux/arm64,linux/amd64,linux/s390x,linux/ppc64le
.PHONY: docker-buildx
docker-buildx: ## 为跨平台支持构建并推送 manager 的 docker 镜像
# 复制现有 Dockerfile,向 Dockerfile.cross 插入 --platform=${BUILDPLATFORM},并保留原始 Dockerfile
sed -e '1 s/\(^FROM\)/FROM --platform=\$$\{BUILDPLATFORM\}/; t' -e ' 1,// s//FROM --platform=\$$\{BUILDPLATFORM\}/' Dockerfile > Dockerfile.cross
- $(CONTAINER_TOOL) buildx create --name project-builder
$(CONTAINER_TOOL) buildx use project-builder
- $(CONTAINER_TOOL) buildx build --push --platform=$(PLATFORMS) $(if $(BASE_IMAGE),--build-arg BASE_IMAGE=$(BASE_IMAGE)) --tag ${IMG} -f Dockerfile.cross .
- $(CONTAINER_TOOL) buildx rm project-builder
rm Dockerfile.cross
.PHONY: build-installer
build-installer: manifests generate kustomize ## 生成包含 CRD 与 deployment 的合并 YAML。
mkdir -p dist
cd config/manager && "$(KUSTOMIZE)" edit set image controller=${IMG}
"$(KUSTOMIZE)" build config/default > dist/install.yaml
##@ 部署
ifndef ignore-not-found
ignore-not-found = false
endif
.PHONY: install
install: manifests kustomize ## 将 CRD 安装到 ~/.kube/config 指定的 K8s 集群。
@out="$$( "$(KUSTOMIZE)" build config/crd 2>/dev/null || true )"; \
if [ -n "$$out" ]; then echo "$$out" | "$(KUBECTL)" apply -f -; else echo "No CRDs to install; skipping."; fi
.PHONY: uninstall
uninstall: manifests kustomize ## 从 ~/.kube/config 指定的 K8s 集群卸载 CRD。调用时可加 ignore-not-found=true 以在删除时忽略资源未找到错误。
@out="$$( "$(KUSTOMIZE)" build config/crd 2>/dev/null || true )"; \
if [ -n "$$out" ]; then echo "$$out" | "$(KUBECTL)" delete --ignore-not-found=$(ignore-not-found) -f -; else echo "No CRDs to delete; skipping."; fi
.PHONY: deploy
deploy: manifests kustomize ## 将控制器部署到 ~/.kube/config 指定的 K8s 集群。
cd config/manager && "$(KUSTOMIZE)" edit set image controller=${IMG}
"$(KUSTOMIZE)" build config/default | "$(KUBECTL)" apply -f -
.PHONY: undeploy
undeploy: kustomize ## 从 ~/.kube/config 指定的 K8s 集群卸载控制器。调用时可加 ignore-not-found=true 以在删除时忽略资源未找到错误。
"$(KUSTOMIZE)" build config/default | "$(KUBECTL)" delete --ignore-not-found=$(ignore-not-found) -f -
##@ 依赖
## 依赖安装位置
LOCALBIN ?= $(shell pwd)/bin
$(LOCALBIN):
mkdir -p "$(LOCALBIN)"
## 工具二进制
KUBECTL ?= kubectl
KIND ?= kind
KUSTOMIZE ?= $(LOCALBIN)/kustomize
CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen
ENVTEST ?= $(LOCALBIN)/setup-envtest
GOLANGCI_LINT = $(LOCALBIN)/golangci-lint
## 工具版本
KUSTOMIZE_VERSION ?= v5.8.1
CONTROLLER_TOOLS_VERSION ?= v0.22.0
# ENVTEST_VERSION 是 setup-envtest 使用的 controller-runtime 版本,从 go.mod 推导
ENVTEST_VERSION ?= $(shell v='$(call gomodver,sigs.k8s.io/controller-runtime)'; \
[ -n "$$v" ] || { echo "请手动设置 ENVTEST_VERSION(controller-runtime replace 无 tag)" >&2; exit 1; }; \
printf '%s\n' "$$v")
# ENVTEST_K8S_VERSION 是设置 ENVTEST 二进制所用的 Kubernetes 版本(例如 1.31)
ENVTEST_K8S_VERSION ?= $(shell v='$(call gomodver,k8s.io/api)'; \
[ -n "$$v" ] || { echo "请手动设置 ENVTEST_K8S_VERSION(k8s.io/api replace 无 tag)" >&2; exit 1; }; \
printf '%s\n' "$$v" | sed -E 's/^v?[0-9]+\.([0-9]+).*/1.\1/')
GOLANGCI_LINT_VERSION ?= v2.13.1
.PHONY: kustomize
kustomize: $(KUSTOMIZE) ## 如有必要则本地下载 kustomize。
$(KUSTOMIZE): $(LOCALBIN)
$(call go-install-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v5,$(KUSTOMIZE_VERSION))
.PHONY: controller-gen
controller-gen: $(CONTROLLER_GEN) ## 如有必要则本地下载 controller-gen。
$(CONTROLLER_GEN): $(LOCALBIN)
$(call go-install-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/controller-gen,$(CONTROLLER_TOOLS_VERSION))
.PHONY: setup-envtest
setup-envtest: envtest ## 将 ENVTEST 所需二进制下载到本地 bin 目录。
@echo "Setting up envtest binaries for Kubernetes version $(ENVTEST_K8S_VERSION)..."
@"$(ENVTEST)" use $(ENVTEST_K8S_VERSION) --bin-dir "$(LOCALBIN)" -p path || { \
echo "Error: Failed to set up envtest binaries for version $(ENVTEST_K8S_VERSION)."; \
exit 1; \
}
.PHONY: envtest
envtest: $(ENVTEST) ## 如有必要则本地下载 setup-envtest。
$(ENVTEST): $(LOCALBIN)
$(call go-install-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest,$(ENVTEST_VERSION))
.PHONY: golangci-lint
golangci-lint: $(GOLANGCI_LINT) ## 如有必要则本地下载 golangci-lint。
$(GOLANGCI_LINT): $(LOCALBIN)
$(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/v2/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION))
@test -f .custom-gcl.yml && { \
echo "Building custom golangci-lint with plugins..." && \
$(GOLANGCI_LINT) custom --destination $(LOCALBIN) --name golangci-lint-custom && \
mv -f $(LOCALBIN)/golangci-lint-custom $(GOLANGCI_LINT); \
} || true
# go-install-tool 将对任何包执行 'go install',并使用自定义目标和二进制文件名称,如果该文件不存在的话。
# $1 - 目标路径和二进制文件名称
# $2 - 可安装的包 URL
# $3 - 特定版本的包
define go-install-tool
@[ -f "$(1)-$(3)" ] && [ "$$(readlink -- "$(1)" 2>/dev/null)" = "$(1)-$(3)" ] || { \
set -e; \
package=$(2)@$(3) ;\
echo "Downloading $${package}" ;\
rm -f "$(1)" ;\
GOBIN="$(LOCALBIN)" go install $${package} ;\
mv "$(LOCALBIN)/$$(basename "$(1)")" "$(1)-$(3)" ;\
} ;\
ln -sf "$$(realpath "$(1)-$(3)")" "$(1)"
endef
define gomodver
$(shell go list -m -f '{{if .Replace}}{{.Replace.Version}}{{else}}{{.Version}}{{end}}' $(1) 2>/dev/null)
endef
##@ Helm Deployment
## Helm binary to use for deploying the chart
HELM ?= helm
## Namespace to deploy the Helm release
HELM_NAMESPACE ?= project-system
## Name of the Helm release
HELM_RELEASE ?= project
## Path to the Helm chart directory
HELM_CHART_DIR ?= dist/chart
## Additional arguments to pass to helm commands
HELM_EXTRA_ARGS ?=
.PHONY: install-helm
install-helm: ## Install the latest version of Helm.
@command -v $(HELM) >/dev/null 2>&1 || { \
echo "Installing Helm..." && \
curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-4 | bash; \
}
.PHONY: helm-deploy
helm-deploy: install-helm ## Deploy manager to the K8s cluster via Helm. Specify an image with IMG.
IMG="$(IMG)"; $(HELM) upgrade --install $(HELM_RELEASE) $(HELM_CHART_DIR) \
--namespace $(HELM_NAMESPACE) \
--create-namespace \
--set manager.image.repository=$${IMG%:*} \
--set manager.image.tag=$${IMG##*:} \
--wait \
--timeout 5m \
$(HELM_EXTRA_ARGS)
.PHONY: helm-uninstall
helm-uninstall: ## Uninstall the Helm release from the K8s cluster.
$(HELM) uninstall $(HELM_RELEASE) --namespace $(HELM_NAMESPACE)
.PHONY: helm-status
helm-status: ## Show Helm release status.
$(HELM) status $(HELM_RELEASE) --namespace $(HELM_NAMESPACE)
.PHONY: helm-history
helm-history: ## Show Helm release history.
$(HELM) history $(HELM_RELEASE) --namespace $(HELM_NAMESPACE)
.PHONY: helm-rollback
helm-rollback: ## Rollback to previous Helm release.
$(HELM) rollback $(HELM_RELEASE) --namespace $(HELM_NAMESPACE)
PROJECT: Kubebuilder metadata for scaffolding new components
# Code generated by tool. DO NOT EDIT.
# 此文件用于跟踪脚手架项目所用的信息
# 并使插件能正常工作。
# 更多信息: https://book.kubebuilder.io/reference/project-config.html
cliVersion: (devel)
domain: tutorial.kubebuilder.io
layout:
- go.kubebuilder.io/v4
plugins:
helm.kubebuilder.io/v2-alpha:
manifests: dist/install.yaml
output: dist
projectName: project
repo: 教程.kubebuilder.io/项目
resources:
- api:
crdVersion: v1
namespaced: true
controller: true
domain: tutorial.kubebuilder.io
group: batch
kind: CronJob
path: tutorial.kubebuilder.io/project/api/v1
version: v1
webhooks:
defaulting: true
validation: true
webhookVersion: v1
version: "3"
启动配置
我们还在 config/ 目录下获取启动配置。目前,它只包含启动控制器所需的 Kustomize YAML 定义,但一旦我们开始编写控制器,它还将包含我们的 CustomResourceDefinitions、自定义访问控制配置(RBAC)和 WebhookConfigurations。
config/default 包含一个 Kustomize 基础,用于以标准配置启动控制器。
每个目录包含不同的配置,这些配置被重构到它们自己的基础中:
-
config/manager: 在集群中以 pod 形式启动您的控制器 -
config/rbac: 运行您的控制器所需的权限,以便在它们自己的服务帐户下运行。
入口点
最后但同样重要的是,Kubebuilder 搭建了我们项目的基本入口文件:main.go。接下来我们来看看这个文件…
每段旅程都需要一个开始,每个程序都需要一个主函数。
Apache License
Copyright 2022 The Kubernetes authors.
Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
包以一些基本导入开始。特别是:
- 核心 controller-runtime 库
- controller-runtime 默认日志库 Zap(稍后详述)
package main
import (
"flag"
"os"
// 导入所有 Kubernetes 客户端身份验证插件(例如 Azure、GCP、OIDC 等)
// 确保 exec-entrypoint 和 run 可以使用它们。
_ "k8s.io/client-go/plugin/pkg/client/auth"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/cache"
"sigs.k8s.io/controller-runtime/pkg/healthz"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
"sigs.k8s.io/controller-runtime/pkg/metrics/server"
"sigs.k8s.io/controller-runtime/pkg/webhook"
// +kubebuilder:scaffold:imports
)
每组控制器都需要一个 Scheme,用于在 Kind 与对应 Go 类型之间提供映射。编写 API 定义时会再谈 Kind,此处先记住即可。
var (
scheme = runtime.NewScheme()
setupLog = ctrl.Log.WithName("setup")
)
func init() {
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
// +kubebuilder:scaffold:scheme
}
此时 main 函数相当简单:
-
我们为 metrics 设置一些基本标志。
-
我们实例化一个 manager,它负责运行所有控制器,并设置共享缓存与到 API 服务器的客户端(注意我们把 Scheme 告诉了 manager)。
-
我们运行 manager,进而运行所有控制器与 webhook。manager 会一直运行直到收到优雅关闭信号。这样在 Kubernetes 上运行时,能与 Pod 优雅终止良好配合。
虽然现在还没什么可运行,请记住 +kubebuilder:scaffold:builder 注释的位置——很快那里会变得有趣。
func main() {
var metricsAddr string
var enableLeaderElection bool
var probeAddr string
flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.")
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
flag.BoolVar(&enableLeaderElection, "leader-elect", false,
"Enable leader election for controller manager. "+
"启用此功能将确保只有一个活动的控制管理器。")
opts := zap.Options{
Development: true,
}
opts.BindFlags(flag.CommandLine)
flag.Parse()
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
Metrics: server.Options{
BindAddress: metricsAddr,
},
WebhookServer: webhook.NewServer(webhook.Options{Port: 9443}),
HealthProbeBindAddress: probeAddr,
LeaderElection: enableLeaderElection,
LeaderElectionID: "80807133.tutorial.kubebuilder.io",
})
if err != nil {
setupLog.Error(err, "unable to start manager")
os.Exit(1)
}
注意:Manager 可通过如下方式限制所有控制器监视资源的命名空间:
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
Cache: cache.Options{
DefaultNamespaces: map[string]cache.Config{
namespace: {},
},
},
Metrics: server.Options{
BindAddress: metricsAddr,
},
WebhookServer: webhook.NewServer(webhook.Options{Port: 9443}),
HealthProbeBindAddress: probeAddr,
LeaderElection: enableLeaderElection,
LeaderElectionID: "80807133.tutorial.kubebuilder.io",
})
上例会将项目作用域改为单个 Namespace。此时也建议把默认的 ClusterRole/ClusterRoleBinding 分别换成 Role/RoleBinding,以把授权限制在该命名空间。更多信息见 Kubernetes 文档 使用 RBAC 授权。
也可以使用 cache.Options{} 中的 DefaultNamespaces 在一组特定命名空间中缓存对象:
var namespaces []string // 命名空间列表
defaultNamespaces := make(map[string]cache.Config)
for _, ns := range namespaces {
defaultNamespaces[ns] = cache.Config{}
}
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
Cache: cache.Options{
DefaultNamespaces: defaultNamespaces,
},
Metrics: server.Options{
BindAddress: metricsAddr,
},
WebhookServer: webhook.NewServer(webhook.Options{Port: 9443}),
HealthProbeBindAddress: probeAddr,
LeaderElection: enableLeaderElection,
LeaderElectionID: "80807133.tutorial.kubebuilder.io",
})
更多信息参见 cache.Options{}
// +kubebuilder:scaffold:builder
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
setupLog.Error(err, "unable to set up health check")
os.Exit(1)
}
if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
setupLog.Error(err, "unable to set up ready check")
os.Exit(1)
}
setupLog.Info("starting manager")
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
setupLog.Error(err, "problem running manager")
os.Exit(1)
}
}
解决了这个问题后,我们可以开始搭建我们的 API 了!
组、版本和种类,真是让人头大!
开始 API 之前,先谈一点术语。
讨论 Kubernetes API 时常用四个术语:groups(组)、versions(版本)、kinds(种类)和 resources(资源)。
组与版本
Kubernetes 中的 API 组 只是相关功能的集合。每个组都有一个或多个 版本,顾名思义,这些版本允许我们随着时间的推移改变 API 的工作方式。
种类与资源
每个 API 组版本包含一个或多个 API 类型,我们称之为 Kinds。虽然一个 Kind 可能在不同版本之间改变形式,但每种形式必须能够以某种方式存储其他形式的所有数据(我们可以将数据存储在字段中或注释中)。这意味着使用旧的 API 版本不会导致较新的数据丢失或损坏。有关更多信息,请参阅 Kubernetes API 指南。
您有时还会听到提到 资源。资源只是 API 中对 Kind 的一种使用。通常,Kinds 和资源之间存在一对一的映射关系。例如,pods 资源对应于 Pod Kind。然而,有时候,同一个 Kind 可能会由多个资源返回。例如,Scale Kind 会由所有缩放子资源返回,如 deployments/scale 或 replicasets/scale。这使得 Kubernetes 的 HorizontalPodAutoscaler 能够与不同的资源进行交互。然而,对于自定义资源定义(CRDs)而言,每个 Kind 将对应一个单一的资源。
请注意,资源始终使用小写字母,并且根据惯例,它们是 Kind 的小写形式。
那么,这与围棋有什么关系呢?
指称某一特定组版本中的 kind 时,称为 GroupVersionKind,简称 GVK。资源对应有 GVR。稍后可见,每个 GVK 对应包中某个根 Go 类型。
术语清楚后,真正 创建 API!
那么,如何创建 API?
在下一部分添加新的 API中,我们将检查该工具如何通过命令 kubebuilder create api 帮助我们创建自己的 API。
此命令的目标是为我们的类型创建一个自定义资源(CR)和自定义资源定义(CRD)。要进一步了解,请参见:使用自定义资源定义扩展Kubernetes API。
但是,为什么要创建API呢?
新 API 是你向 Kubernetes 介绍自定义对象的方式。Controller-gen 用 Go 结构体生成 CRD,其中包含数据 schema 以及新类型名称等跟踪信息。随后可创建由控制器管理的自定义对象实例。
API 与资源代表你在集群上的方案。简单说,CRD 是自定义对象的定义,CR 是其实例。
啊,你有例子吗?
想想经典场景:目标是在 Kubernetes 平台上运行应用及其数据库。可用一个 CRD 表示 App,另一个表示 DB。分别描述 App 与 DB 可避免破坏封装、单一职责与内聚等概念;损害这些概念可能导致难以扩展、复用或维护等副作用。
通过这种方式,我们可以创建应用程序的 CRD,它将拥有自己的控制器,负责诸如创建包含应用程序的 Deployment、创建访问该应用程序的 Service 等。类似地,我们可以创建一个 CRD 来表示数据库,并部署一个控制器来管理数据库实例。
呃,那个 Scheme 是什么?
我们之前看到的 Scheme 只是一个用来跟踪给定 GVK 对应的 Go 类型的方式(不要被它的 godocs 压倒)。
例如,假设我们将 "tutorial.kubebuilder.io/api/v1".CronJob{} 类型标记为属于 batch.tutorial.kubebuilder.io/v1 API 组(隐含地表示它的 Kind 为 CronJob)。
然后,我们可以根据API服务器提供的JSON数据,稍后构建一个新的 &CronJob{}。
{
"kind": "CronJob",
"apiVersion": "batch.tutorial.kubebuilder.io/v1",
...
}
或者在我们提交一个 &CronJob{} 的更新时,正确查找组版本。
添加一个新 API
要构建一个新的 Kind(你有注意到上一章吗?),以及相应的控制器,我们可以使用 kubebuilder create api:
kubebuilder create api --group batch --version v1 --kind CronJob
按 y 以确认\
第一次为每个组版本调用此命令时,它将为新的组版本创建一个目录。
在这种情况下,api/v1/ 目录被创建,对应于 batch.tutorial.kubebuilder.io/v1(还记得我们最开始的 --domain 设置 吗?)。
它还为我们的 CronJob 类型添加了一个文件 api/v1/cronjob_types.go。每次我们调用带有不同类型的命令时,它都会添加一个相应的新文件。
先看看开箱即得的内容,再动手填写。
Apache License
Copyright 2022.
Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
开始很简单:导入 meta/v1 API 组——它通常不单独暴露,而是包含所有 Kubernetes Kind 共用的元数据。
package v1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
接下来为 Kind 定义 Spec 与 Status 类型。Kubernetes 通过协调期望状态(Spec)与实际集群状态(其他对象的 Status)及外部状态,并记录观测结果(Status)来工作。因此每个_功能性_对象都包含 spec 与 status。少数类型如 ConfigMap 不遵循该模式(它们不编码期望状态),但多数类型如此。
// 请编辑本文件!这是供您拥有的脚手架代码!
// 注意:需要 json 标签。新增字段必须带有 json 标签才能被序列化。
// CronJobSpec 定义 CronJob 的期望状态
type CronJobSpec struct {
// 插入额外的规格字段 - 集群的期望状态
// 注意:修改此文件后,运行"make"以重新生成代码
}
// CronJobStatus 定义 CronJob 的观测状态
type CronJobStatus struct {
// 插入额外的状态字段 - 定义集群的观察状态
// 重要:在修改此文件后,请运行 "make" 以重新生成代码
}
接着定义对应实际 Kind 的类型:CronJob 与 CronJobList。CronJob 是根类型,描述 CronJob kind。与所有 Kubernetes 对象一样,它包含 TypeMeta(描述 API 版本与 Kind)以及 ObjectMeta(名称、命名空间、标签等)。
CronJobList 只是多个 CronJob 的容器,用于 LIST 等批量操作。
通常不会直接修改这两者——所有修改都在 Spec 或 Status 中进行。
那个小小的 +kubebuilder:object:root 注释叫做标记。稍后会见到更多;它们是额外元数据,告诉 controller-tools(代码与 YAML 生成器)更多信息。这一条告诉 object 生成器该类型表示一个 Kind,于是生成器为我们实现 runtime.Object 接口——所有表示 Kind 的类型都必须实现它。
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// CronJob 是 cronjobs API 的 Schema
type CronJob struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec CronJobSpec `json:"spec,omitempty"`
Status CronJobStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// CronJobList 包含一个 CronJob 列表
type CronJobList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []CronJob `json:"items"`
}
最后把 Go 类型加入 API 组,以便把该组中的类型加入任意 Scheme。
func init() {
SchemeBuilder.Register(&CronJob{}, &CronJobList{})
}
现在我们已经了解了基本结构,让我们来完善它吧!
设计一个 API
在 Kubernetes 中设计 API 有若干规则。即:所有序列化字段_必须_为 camelCase,因此用 JSON 结构体标签指定。也可用 omitempty 标记空值时省略序列化。
字段可以使用大部分原始类型。数字是一个例外:出于API兼容性考虑,我们接受三种数字形式:int32 和 int64 用于整数,resource.Quantity 用于小数。
Hold up, what is a Quantity?
数量是对十进制数字的一种特殊表示法,它具有明确固定的表示方式,使其在不同机器之间更具可移植性。您可能在Kubernetes中为pod指定资源请求和限制时注意到了它们。
它们在概念上与浮点数相似:具有有效数字、基数和指数。它们的可序列化和可读格式使用整数和后缀来指定值,方式与我们描述计算机存储的方式类似。
例如,值 2m 在十进制表示中代表 0.002。 2Ki 在十进制中表示 2048,而 2K 在十进制中表示 2000。如果我们想指定小数,我们可以切换到一个后缀,使我们能够使用整数:2.5 表示为 2500m。
支持两种进制:10 进制和 2 进制(分别称为十进制和二进制)。十进制使用“常规“国际单位制后缀表示(例如 M 和 K),而二进制使用“兆比特“(mebi)表示法(例如 Mi 和 Ki)。可以参考 兆字节与兆比特 的区别。
我们使用的另一种特殊类型是 metav1.Time。它的功能与 time.Time 相同,但具有固定的可移植序列化格式。
在处理完这些之后,我们来看看我们的 CronJob 对象是什么样的!
Apache License
Copyright 2026 The Kubernetes authors.
Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
package v1
Imports
import (
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
// 请编辑本文件!这是供您拥有的脚手架代码!
// 注意:需要 json 标签。新增字段必须带有 json 标签才能被序列化。
先看 spec。如前所述,spec 保存_期望状态_,因此控制器的所有“输入”都放这里。
从根本上说,CronJob 需要以下几部分:
- 一个调度表达式(CronJob 中的 cron)
- 要运行的 Job 模板(CronJob 中的 job)
还需要一些额外项,让用户更省心:
- 启动 Job 的截止时间(错过则等到下次调度)
- 若多个 Job 本应同时运行该怎么办(等待?停掉旧的?都跑?)
- 暂停 CronJob 运行的方式,以防出问题
- 旧 Job 历史的保留限制
请记住,由于我们从不读取自己的 status,需要用其他方式跟踪 Job 是否已运行。我们可以至少用一个旧 Job 来做到这一点。
我们将使用多个标记(// +comment)指定额外元数据。生成 CRD 清单时,controller-tools 会使用它们。稍后会看到,controller-tools 还会用 GoDoc 形成字段描述。
// CronJobSpec 定义 CronJob 的期望状态
type CronJobSpec struct {
// Cron 格式的调度表达式,参见 https://en.wikipedia.org/wiki/Cron.
// +kubebuilder:validation:MinLength=0
// +required
Schedule string `json:"schedule"`
// startingDeadlineSeconds 定义若错过计划时间则启动 Job 的秒数截止
// time for any reason. Missed jobs executions will be counted as failed ones.
// +optional
// +kubebuilder:validation:Minimum=0
StartingDeadlineSeconds *int64 `json:"startingDeadlineSeconds,omitempty"`
// concurrencyPolicy 指定如何处理 Job 的并发执行。
// 有效值为:
// - "Allow"(默认):允许 CronJob 并发运行;
// - "Forbid":禁止并发,若上次运行未结束则跳过下次;
// - "Replace":取消当前运行的 Job 并用新 Job 替换
// +optional
// +kubebuilder:default:=Allow
ConcurrencyPolicy ConcurrencyPolicy `json:"concurrencyPolicy,omitempty"`
// suspend 告诉控制器暂停后续执行,它
// 不适用于已开始的执行。默认为 false。
// +optional
Suspend *bool `json:"suspend,omitempty"`
// jobTemplate 定义执行 CronJob 时将创建的 Job。
// +required
JobTemplate batchv1.JobTemplateSpec `json:"jobTemplate"`
// successfulJobsHistoryLimit 定义要保留的成功完成 Job 数量。
// 使用指针以区分显式零值与未指定。
// +optional
// +kubebuilder:validation:Minimum=0
SuccessfulJobsHistoryLimit *int32 `json:"successfulJobsHistoryLimit,omitempty"`
// failedJobsHistoryLimit 定义要保留的失败完成 Job 数量。
// 使用指针以区分显式零值与未指定。
// +optional
// +kubebuilder:validation:Minimum=0
FailedJobsHistoryLimit *int32 `json:"failedJobsHistoryLimit,omitempty"`
}
我们定义自定义类型来保存并发策略。底层实际只是字符串,但该类型能提供额外文档,并允许把校验附在类型而非字段上,使校验更易复用。
// ConcurrencyPolicy 描述如何处理 Job。
// 下列并发策略只能指定其一。
// 若未指定下列任一策略,则默认
// 为 AllowConcurrent。
// +kubebuilder:validation:Enum=Allow;Forbid;Replace
type ConcurrencyPolicy string
const (
// AllowConcurrent 允许 CronJob 并发运行。
AllowConcurrent ConcurrencyPolicy = "Allow"
// ForbidConcurrent 禁止并发运行;若上次
// 尚未结束则跳过下次运行。
ForbidConcurrent ConcurrencyPolicy = "Forbid"
// ReplaceConcurrent 取消当前运行的 Job 并用新 Job 替换。
ReplaceConcurrent ConcurrencyPolicy = "Replace"
)
接下来设计 status,保存观测状态,包含希望用户或其他控制器能轻松获取的信息。
我们将保留活动运行 Job 的列表,以及成功运行 Job 的最后时间。注意我们使用 metav1.Time 而非 time.Time 以获得稳定序列化,如前所述。
// CronJobStatus 定义 CronJob 的观测状态.
type CronJobStatus struct {
// 插入额外的状态字段 - 定义集群的观察状态
// 重要:在修改此文件后,请运行 "make" 以重新生成代码
// active 定义指向当前运行 Job 的指针列表。
// +optional
// +listType=atomic
// +kubebuilder:validation:MinItems=1
// +kubebuilder:validation:MaxItems=10
Active []corev1.ObjectReference `json:"active,omitempty"`
// lastScheduleTime 定义 Job 上次成功调度的时间。
// +optional
LastScheduleTime *metav1.Time `json:"lastScheduleTime,omitempty"`
// Kubernetes API 约定参见:
// https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties
// conditions 表示 CronJob 资源的当前状态。
// 每个 condition 有唯一类型,反映资源某一方面的状态。
//
// Standard condition types include:
// - "Available": the resource is fully functional
// - "Progressing": the resource is being created or updated
// - "Degraded": the resource failed to reach or maintain its desired state
//
// The status of each condition is one of True, False, or Unknown.
// +listType=map
// +listMapKey=type
// +optional
Conditions []metav1.Condition `json:"conditions,omitempty"`
}
最后是我们已经讨论过的其余样板。如前所述,除了标记我们需要 status 子资源以表现得像内置 kubernetes类型外,无需更改这些内容。
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// CronJob 是 cronjobs API 的 Schema
type CronJob struct {
Root Object Definitions
metav1.TypeMeta `json:",inline"`
// metadata 是标准对象元数据
// +optional
metav1.ObjectMeta `json:"metadata,omitzero"`
// spec 定义 CronJob 的期望状态
// +required
Spec CronJobSpec `json:"spec"`
// status 定义 CronJob 的观测状态
// +optional
Status CronJobStatus `json:"status,omitzero"`
}
// +kubebuilder:object:root=true
// CronJobList 包含一个 CronJob 列表
type CronJobList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitzero"`
Items []CronJob `json:"items"`
}
func init() {
SchemeBuilder.Register(func(s *runtime.Scheme) error {
s.AddKnownTypes(SchemeGroupVersion, &CronJob{}, &CronJobList{})
return nil
})
}
现在我们有了一个API,我们需要编写一个控制器来实际实现该功能。
简短的插曲:这些其他东西是什么?
如果你查看了 api/v1/ 目录中的其他文件,可能会注意到除了 cronjob_types.go 之外还有两个额外的文件:groupversion_info.go 和 zz_generated.deepcopy.go。
这两个文件都不需要编辑(前者保持不变,后者是自动生成的),但了解它们的内容是很有用的。
groupversion_info.go
groupversion_info.go 包含有关组版本的常见元数据:
Apache License
Copyright 2026 The Kubernetes authors.
Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
首先是一些_包级_标记,表示此包中有 Kubernetes 对象,且此包代表 group batch.tutorial.kubebuilder.io。前者由 object 生成器使用,后者由 CRD 生成器用来为其从此包创建的 CRD 生成正确元数据。
// Package v1 包含 batch v1 API 组的 API Schema 定义。
// +kubebuilder:object:generate=true
// +groupName=batch.tutorial.kubebuilder.io
package v1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)
然后是帮助我们设置 Scheme 的常用变量。由于需要在控制器中使用此包中的所有类型,有一个方便的方法把所有类型加到其他 Scheme 会很有帮助(也是约定)。SchemeBuilder 让这变得容易。
var (
// SchemeGroupVersion 是用于注册这些对象的组版本。
// This name is used by applyconfiguration generators (e.g. controller-gen).
SchemeGroupVersion = schema.GroupVersion{Group: "batch.tutorial.kubebuilder.io", Version: "v1"}
// GroupVersion 是 SchemeGroupVersion 的别名,用于向后兼容。
GroupVersion = SchemeGroupVersion
// SchemeBuilder 用于将 go 类型添加到 GroupVersionKind scheme。
SchemeBuilder = runtime.NewSchemeBuilder(func(scheme *runtime.Scheme) error {
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
return nil
})
// AddToScheme 将该组版本中的类型添加到给定 scheme。
AddToScheme = SchemeBuilder.AddToScheme
)
zz_generated.deepcopy.go
zz_generated.deepcopy.go 包含了上述 runtime.Object 接口的自动生成实现,它将我们的所有根类型标记为表示种类(Kinds)。
runtime.Object 接口的核心是一个深拷贝方法 DeepCopyObject。
controller-tools中的object生成器还为每个根类型及其所有子类型生成了两个其他实用方法:DeepCopy和DeepCopyInto。
控制器中包含什么?
控制器是Kubernetes和任何操作器的核心。
控制器的职责是确保对于任何给定的对象,实际的世界状态(包括集群状态以及可能的外部状态,例如 Kubelet 的运行容器或云服务提供商的负载均衡器)与对象中的期望状态相匹配。每个控制器专注于一个_根_ Kind,但可能与其他 Kind 进行交互。
我们称这个过程为 调整。
在 controller-runtime 中,实现特定种类的对账逻辑的部分称为 Reconciler。一个 reconciler 接收一个对象的名称,并返回是否需要重新尝试(例如,在出现错误或周期性控制器的情况下,如水平 Pod 自动缩放器)。
Apache License
Copyright 2022.
Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
先从标准导入开始。与之前一样,需要核心 controller-runtime 库、client 包,以及 API 类型包。
package controllers
import (
"context"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
logf "sigs.k8s.io/controller-runtime/pkg/log"
batchv1 "tutorial.kubebuilder.io/project/api/v1"
)
接下来,kubebuilder 已为我们脚手架生成基本的 reconciler 结构体。几乎每个 reconciler 都需要日志并能获取对象,因此这些已开箱即加。
// CronJobReconciler 协调 CronJob 对象
type CronJobReconciler struct {
client.Client
Scheme *runtime.Scheme
}
大多数控制器最终会在集群上运行,因此需要 RBAC 权限,我们用 controller-tools RBAC 标记 指定。这些是运行所需的最低权限。随着功能增加,我们需要再审视它们。
// +kubebuilder:rbac:groups=batch.tutorial.kubebuilder.io,resources=cronjobs,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=batch.tutorial.kubebuilder.io,resources=cronjobs/status,verbs=get;update;patch
config/rbac/role.yaml 中的 ClusterRole 清单由上述标记经 controller-gen 用以下命令生成:
make manifests
注意:若收到错误,请运行错误中给出的命令,然后重新执行 make manifests。
Reconcile 实际对单个具名对象执行协调。我们的 Request 只有名称,但可用 client 从缓存获取该对象。
我们返回空结果且无错误,这向 controller-runtime 表明已成功协调该对象,在出现变更前无需重试。
多数控制器需要日志句柄与 context,因此在此设置。
context 用于允许取消请求,以及可能的追踪等。它是所有客户端方法的第一个参数。Background context 只是没有额外数据或时间限制的基本 context。
日志句柄让我们记录日志。controller-runtime 通过名为 logr 的库使用结构化日志。稍后会看到,日志通过向静态消息附加键值对工作。我们可以在 reconcile 方法顶部预分配一些键值对,使它们附加到此 reconciler 的所有日志行。
func (r *CronJobReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
_ = logf.FromContext(ctx)
// 在此编写逻辑
return ctrl.Result{}, nil
}
最后把该 reconciler 加入 manager,以便 manager 启动时一并启动。
目前只标明该 reconciler 操作 CronJob。稍后会用此标记我们也关心相关对象。
func (r *CronJobReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&batchv1.CronJob{}).
Complete(r)
}
现在我们已经看到了 reconciler 的基本结构,让我们来完善 CronJob 的逻辑。
实现控制器
我们CronJob控制器的基本逻辑是这样的:
-
加载指定的 CronJob
-
列出所有有效的工作,并更新状态。
-
根据历史限制清理旧的工作记录。
-
检查是否已挂起(若已挂起则不做其他事)
-
获取下一个计划的运行时间
-
如果新工作在计划时间内、未超过截止日期且未被我们的并发策略阻止,则可以运行。
-
当我们看到一个正在运行的任务(自动完成)或是下一个定时运行的时间到时,请重新排队。
Apache License
Copyright 2026 The Kubernetes authors.
Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
从一些导入开始。下面会看到比脚手架更多的导入,用到时再说明。
package controller
import (
"context"
"fmt"
"maps"
"slices"
"time"
"github.com/robfig/cron"
kbatch "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
ref "k8s.io/client-go/tools/reference"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
logf "sigs.k8s.io/controller-runtime/pkg/log"
batchv1 "tutorial.kubebuilder.io/project/api/v1"
)
接下来需要 Clock,以便在测试中伪造时间。
// CronJobReconciler 协调 CronJob 对象
type CronJobReconciler struct {
client.Client
Scheme *runtime.Scheme
Clock
}
Clock Code Implementation
我们会 mock 时钟以便测试中随意跳转时间;“真实”时钟只是调用 time.Now。
type realClock struct{}
func (_ realClock) Now() time.Time { return time.Now() } //nolint:staticcheck
// Clock 知道如何获取当前时间。
// 可用于在测试中伪造时间。
type Clock interface {
Now() time.Time
}
// 管理状态条件的定义
const (
// typeAvailableCronJob 表示 CronJob 协调的状态
typeAvailableCronJob = "Available"
// typeProgressingCronJob 表示 CronJob 正在协调时使用的状态
typeProgressingCronJob = "Progressing"
// typeDegradedCronJob 表示 CronJob 遇到错误时使用的状态
typeDegradedCronJob = "Degraded"
)
注意我们还需要更多 RBAC 权限——因为现在要创建和管理 Job,需要相应权限,这意味着再添加几条 标记。
// +kubebuilder:rbac:groups=batch.tutorial.kubebuilder.io,resources=cronjobs,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=batch.tutorial.kubebuilder.io,resources=cronjobs/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=batch.tutorial.kubebuilder.io,resources=cronjobs/finalizers,verbs=update
// +kubebuilder:rbac:groups=batch,resources=jobs,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=batch,resources=jobs/status,verbs=get
现在进入控制器核心——reconciler 逻辑。
var (
scheduledTimeAnnotation = "batch.tutorial.kubebuilder.io/scheduled-at"
)
// Reconcile 是主 kubernetes 协调循环的一部分,其目标是
// 使集群当前状态更接近期望状态。
// TODO(user): Modify the Reconcile function to compare the state specified by
// the CronJob object against the actual cluster state, and then
// perform operations to make the cluster state reflect the state specified by
// the user.
//
// 更多细节参见 Reconcile 及其 Result:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.25.0/pkg/reconcile
// nolint:gocyclo
func (r *CronJobReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := logf.FromContext(ctx)
1:按名称加载 CronJob
我们用客户端获取 CronJob。所有客户端方法以 context(允许取消)为第一个参数,以相关对象为最后一个参数。Get 有些特殊,中间参数是 NamespacedName(如下文所见,多数方法没有中间参数)。
许多 client 方法末尾还接受可变参数选项。
var cronJob batchv1.CronJob
if err := r.Get(ctx, req.NamespacedName, &cronJob); err != nil {
if apierrors.IsNotFound(err) {
// 如果未找到自定义资源,通常意味着它已被删除或未创建
// 这样,我们将停止调整过程
log.Info("CronJob resource not found. Ignoring since object must be deleted")
return ctrl.Result{}, nil
}
// 读取对象时出错 - 重新排队请求。
log.Error(err, "Failed to get CronJob")
return ctrl.Result{}, err
}
// 若尚未存在则初始化 status conditions
if len(cronJob.Status.Conditions) == 0 {
meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{
Type: typeProgressingCronJob,
Status: metav1.ConditionUnknown,
Reason: "Reconciling",
Message: "Starting reconciliation",
})
if err := r.Status().Update(ctx, &cronJob); err != nil {
log.Error(err, "Failed to update CronJob status")
return ctrl.Result{}, err
}
更新 status 后重新获取 CronJob,确保使用 API 服务器上的最新版本。
Kubernetes 使用乐观并发,意味着任何更新(包括 status 更新)都可能更改资源版本。若继续用过期副本协调,后续更新可能因冲突失败,例如:“the object has been modified; please apply your changes to the latest version and try again”。
在此重新获取可使协调逻辑与实际集群状态同步,避免不必要的冲突与重新入队。
if err := r.Get(ctx, req.NamespacedName, &cronJob); err != nil {
log.Error(err, "Failed to re-fetch CronJob")
return ctrl.Result{}, err
}
}
2:列出所有活动 Job,并更新 status
要完整更新 status,需要列出此命名空间中属于该 CronJob 的所有子 Job。与 Get 类似,可用 List 方法列出子 Job。注意我们用可变参数选项设置命名空间与字段匹配(实际是下面设置的索引查找)。
var childJobs kbatch.JobList
if err := r.List(ctx, &childJobs, client.InNamespace(req.Namespace), client.MatchingFields{jobOwnerKey: req.Name}); err != nil {
log.Error(err, "unable to list child Jobs")
更新前请确保拿到资源的最新状态,以避免冲突错误(例如 “the object has been modified”),否则会重新触发协调循环。
if fetchErr := r.Get(ctx, req.NamespacedName, &cronJob); fetchErr != nil {
log.Error(fetchErr, "Failed to re-fetch CronJob")
return ctrl.Result{}, fetchErr
}
// 更新 status condition 以反映错误
meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{
Type: typeDegradedCronJob,
Status: metav1.ConditionTrue,
Reason: "ReconciliationError",
Message: fmt.Sprintf("Failed to list child jobs: %v", err),
})
if statusErr := r.Status().Update(ctx, &cronJob); statusErr != nil {
log.Error(statusErr, "Failed to update CronJob status")
}
return ctrl.Result{}, err
}
一旦拥有所有归我们所有的 Job,就把它们分成活动、成功与失败的 Job,并跟踪最近一次运行以便记入 status。请记住,status 应能从世界状态重建,因此通常不宜读取根对象的 status。相反,每次运行都应重建它。我们在这里就这样做。
我们可以使用 status 条件检查 Job 是否「已完成」以及成功还是失败。我们把该逻辑放在辅助函数中,使代码更清晰。
// 查找活动 Job 列表
var activeJobs []*kbatch.Job
var successfulJobs []*kbatch.Job
var failedJobs []*kbatch.Job
var mostRecentTime *time.Time // 查找上次运行以便更新 status
isJobFinished
若 Job 具有标记为 true 的 “Complete” 或 “Failed” 条件,我们视其为「已完成」。Status 条件允许向对象添加可扩展状态信息,供其他人或控制器检查完成度与健康状况等。
isJobFinished := func(job *kbatch.Job) (bool, kbatch.JobConditionType) {
for _, c := range job.Status.Conditions {
if (c.Type == kbatch.JobComplete || c.Type == kbatch.JobFailed) && c.Status == corev1.ConditionTrue {
return true, c.Type
}
}
return false, ""
}
getScheduledTimeForJob
用辅助函数从创建 Job 时添加的注解中提取计划时间。
getScheduledTimeForJob := func(job *kbatch.Job) (*time.Time, error) {
timeRaw := job.Annotations[scheduledTimeAnnotation]
if len(timeRaw) == 0 {
return nil, nil
}
timeParsed, err := time.Parse(time.RFC3339, timeRaw)
if err != nil {
return nil, err
}
return &timeParsed, nil
}
for i, job := range childJobs.Items {
_, finishedType := isJobFinished(&job)
switch finishedType {
case "": // 进行中
activeJobs = append(activeJobs, &childJobs.Items[i])
case kbatch.JobFailed:
failedJobs = append(failedJobs, &childJobs.Items[i])
case kbatch.JobComplete:
successfulJobs = append(successfulJobs, &childJobs.Items[i])
}
// 我们将启动时间存在注解中,以便从
// 活动 Job 自身重建。
scheduledTimeForJob, err := getScheduledTimeForJob(&job)
if err != nil {
log.Error(err, "unable to parse schedule time for child job", "job", &job)
continue
}
if scheduledTimeForJob != nil {
if mostRecentTime == nil || mostRecentTime.Before(*scheduledTimeForJob) {
mostRecentTime = scheduledTimeForJob
}
}
}
if mostRecentTime != nil {
cronJob.Status.LastScheduleTime = &metav1.Time{Time: *mostRecentTime}
} else {
cronJob.Status.LastScheduleTime = nil
}
cronJob.Status.Active = nil
for _, activeJob := range activeJobs {
jobRef, err := ref.GetReference(r.Scheme, activeJob)
if err != nil {
log.Error(err, "unable to make reference to active job", "job", activeJob)
continue
}
cronJob.Status.Active = append(cronJob.Status.Active, *jobRef)
}
这里我们以稍高的日志级别记录观察到的 Job 数量,便于调试。注意我们不用格式化字符串,而是用固定消息,并用键值对附加额外信息。这样更易过滤与查询日志行。
log.V(1).Info("job count", "active jobs", len(activeJobs), "successful jobs", len(successfulJobs), "failed jobs", len(failedJobs))
// 检查 CronJob 是否已挂起
isSuspended := cronJob.Spec.Suspend != nil && *cronJob.Spec.Suspend
// 根据当前状态更新 status conditions
if isSuspended {
meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{
Type: typeAvailableCronJob,
Status: metav1.ConditionFalse,
Reason: "Suspended",
Message: "CronJob is suspended",
})
} else if len(failedJobs) > 0 {
meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{
Type: typeDegradedCronJob,
Status: metav1.ConditionTrue,
Reason: "JobsFailed",
Message: fmt.Sprintf("%d job(s) have failed", len(failedJobs)),
})
meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{
Type: typeAvailableCronJob,
Status: metav1.ConditionFalse,
Reason: "JobsFailed",
Message: fmt.Sprintf("%d job(s) have failed", len(failedJobs)),
})
} else if len(activeJobs) > 0 {
meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{
Type: typeProgressingCronJob,
Status: metav1.ConditionTrue,
Reason: "JobsActive",
Message: fmt.Sprintf("%d job(s) are currently active", len(activeJobs)),
})
meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{
Type: typeAvailableCronJob,
Status: metav1.ConditionTrue,
Reason: "JobsActive",
Message: fmt.Sprintf("CronJob is progressing with %d active job(s)", len(activeJobs)),
})
} else {
meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{
Type: typeAvailableCronJob,
Status: metav1.ConditionTrue,
Reason: "AllJobsCompleted",
Message: "All jobs have completed successfully",
})
meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{
Type: typeProgressingCronJob,
Status: metav1.ConditionFalse,
Reason: "NoJobsActive",
Message: "No jobs are currently active",
})
}
利用已收集的数据,我们将更新 CRD 的 status。与之前一样使用客户端。要专门更新 status 子资源,使用客户端的 Status 部分及其 Update 方法。
status 子资源忽略对 spec 的更改,因此更不易与其他更新冲突,并可有独立权限。
if err := r.Status().Update(ctx, &cronJob); err != nil {
log.Error(err, "unable to update CronJob status")
return ctrl.Result{}, err
}
更新 status 后,继续确保现实状态与 spec 中的期望一致。
3:按历史限制清理旧 Job
首先清理旧 Job,以免残留过多。
// 注意:删除为“尽力而为”——若某一个失败,
// 不会仅为完成删除而重新入队。
if cronJob.Spec.FailedJobsHistoryLimit != nil {
slices.SortStableFunc(failedJobs, func(a, b *kbatch.Job) int {
aStartTime := a.Status.StartTime
bStartTime := b.Status.StartTime
switch {
case aStartTime == nil && bStartTime == nil:
return 0
case aStartTime == nil:
return -1
case bStartTime == nil:
return 1
case aStartTime.Before(bStartTime):
return -1
case bStartTime.Before(aStartTime):
return 1
default:
return 0
}
})
for i, job := range failedJobs {
if int32(i) >= int32(len(failedJobs))-*cronJob.Spec.FailedJobsHistoryLimit {
break
}
if err := r.Delete(ctx, job, client.PropagationPolicy(metav1.DeletePropagationBackground)); client.IgnoreNotFound(err) != nil {
log.Error(err, "unable to delete old failed job", "job", job)
} else {
log.V(1).Info("deleted old failed job", "job", job)
}
}
}
if cronJob.Spec.SuccessfulJobsHistoryLimit != nil {
slices.SortStableFunc(successfulJobs, func(a, b *kbatch.Job) int {
aStartTime := a.Status.StartTime
bStartTime := b.Status.StartTime
switch {
case aStartTime == nil && bStartTime == nil:
return 0
case aStartTime == nil:
return -1
case bStartTime == nil:
return 1
case aStartTime.Before(bStartTime):
return -1
case bStartTime.Before(aStartTime):
return 1
default:
return 0
}
})
for i, job := range successfulJobs {
if int32(i) >= int32(len(successfulJobs))-*cronJob.Spec.SuccessfulJobsHistoryLimit {
break
}
if err := r.Delete(ctx, job, client.PropagationPolicy(metav1.DeletePropagationBackground)); err != nil {
log.Error(err, "unable to delete old successful job", "job", job)
} else {
log.V(1).Info("deleted old successful job", "job", job)
}
}
}
4:检查是否已挂起
若此对象被挂起,我们不想运行任何 Job,因此到此停止。当正在运行的 Job 出了问题、想暂停运行以排查或调整集群而又不想删除对象时,这很有用。
if cronJob.Spec.Suspend != nil && *cronJob.Spec.Suspend {
log.V(1).Info("cronjob suspended, skipping")
return ctrl.Result{}, nil
}
5:获取下次计划运行
若未暂停,需计算下次计划运行,以及是否还有未处理的运行。
getNextSchedule
我们将使用方便的 cron 库计算下次计划时间。从上次运行开始计算合适时间;若找不到上次运行,则从 CronJob 创建时开始。
如果错过的运行太多且未设置任何截止时间,我们将退出,以免在控制器重启或卡住时引发问题。
否则返回错过的运行(只用最新一次)和下次运行,以便知道何时再次协调。
getNextSchedule := func(cronJob *batchv1.CronJob, now time.Time) (lastMissed time.Time, next time.Time, err error) {
sched, err := cron.ParseStandard(cronJob.Spec.Schedule)
if err != nil {
return time.Time{}, time.Time{}, fmt.Errorf("unparseable schedule %q: %w", cronJob.Spec.Schedule, err)
}
// 为优化,略取巧地从上次观测到的运行时间开始
// we could reconstitute this here, but there's not much point, since we've
// just updated it.
var earliestTime time.Time
if cronJob.Status.LastScheduleTime != nil {
earliestTime = cronJob.Status.LastScheduleTime.Time
} else {
earliestTime = cronJob.CreationTimestamp.Time
}
if cronJob.Spec.StartingDeadlineSeconds != nil {
// 控制器不会在此点之下调度任何内容
schedulingDeadline := now.Add(-time.Second * time.Duration(*cronJob.Spec.StartingDeadlineSeconds))
if schedulingDeadline.After(earliestTime) {
earliestTime = schedulingDeadline
}
}
if earliestTime.After(now) {
return time.Time{}, sched.Next(now), nil
}
starts := 0
for t := sched.Next(earliestTime); !t.After(now); t = sched.Next(t) {
lastMissed = t
// 对象可能错过多次启动。例如,若
// 控制器在周五下午 5:01 卡住,而大家都
// gone home, and someone comes in on Tuesday AM and discovers
// the problem and restarts the controller, then all the hourly
// jobs, more than 80 of them for one hourly scheduledJob, should
// all start running with no further intervention (if the scheduledJob
// allows concurrency and late starts).
//
// However, if there is a bug somewhere, or incorrect clock
// on controller's server or apiservers (for setting creationTimestamp)
// then there could be so many missed start times (it could be off
// by decades or more), that it would eat up all the CPU and memory
// of this controller. In that case, we want to not try to list
// all the missed start times.
starts++
if starts > 100 {
// 无法获取最近时间则返回空切片
return time.Time{}, time.Time{}, fmt.Errorf("Too many missed start times (> 100). Set or decrease .spec.startingDeadlineSeconds or check clock skew.") //nolint:staticcheck
}
}
return lastMissed, sched.Next(now), nil
}
// 算出下次需要创建的时间
// Job 的时间点(或任何错过的)。
missedRun, nextRun, err := getNextSchedule(&cronJob, r.Now())
if err != nil {
log.Error(err, "unable to figure out CronJob schedule")
if fetchErr := r.Get(ctx, req.NamespacedName, &cronJob); fetchErr != nil {
log.Error(fetchErr, "Failed to re-fetch CronJob")
return ctrl.Result{}, fetchErr
}
// 更新 status condition 以反映调度错误
meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{
Type: typeDegradedCronJob,
Status: metav1.ConditionTrue,
Reason: "InvalidSchedule",
Message: fmt.Sprintf("Failed to parse schedule: %v", err),
})
if statusErr := r.Status().Update(ctx, &cronJob); statusErr != nil {
log.Error(statusErr, "Failed to update CronJob status")
}
// 在收到能修复调度的更新前不必重新入队
// 因此不返回错误
return ctrl.Result{}, nil
}
准备最终的重新入队请求直到下次 Job,再判断是否真正需要运行。
scheduledResult := ctrl.Result{RequeueAfter: nextRun.Sub(r.Now())} // 保存以便别处复用
log = log.WithValues("now", r.Now(), "next run", nextRun)
6:若已到点、未过截止且未被并发策略阻塞,则运行新 Job
若错过一次运行且仍在启动截止时间内,则需要运行 Job。
if missedRun.IsZero() {
log.V(1).Info("no upcoming scheduled times, sleeping until next")
return scheduledResult, nil
}
// 确保启动运行尚未过晚
log = log.WithValues("current run", missedRun)
tooLate := false
if cronJob.Spec.StartingDeadlineSeconds != nil {
tooLate = missedRun.Add(time.Duration(*cronJob.Spec.StartingDeadlineSeconds) * time.Second).Before(r.Now())
}
if tooLate {
log.V(1).Info("missed starting deadline for last run, sleeping till next")
if fetchErr := r.Get(ctx, req.NamespacedName, &cronJob); fetchErr != nil {
log.Error(fetchErr, "Failed to re-fetch CronJob")
return ctrl.Result{}, fetchErr
}
// 更新 status condition 以反映错过截止时间
meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{
Type: typeDegradedCronJob,
Status: metav1.ConditionTrue,
Reason: "MissedSchedule",
Message: fmt.Sprintf("Missed starting deadline for run at %v", missedRun),
})
if statusErr := r.Status().Update(ctx, &cronJob); statusErr != nil {
log.Error(statusErr, "Failed to update CronJob status")
}
return scheduledResult, nil
}
若确实需要运行 Job,我们要么等待现有 Job 完成,要么替换现有 Job,要么直接添加新 Job。若因缓存延迟导致信息过时,在拿到最新信息时会重新入队。
// 确定如何运行此 Job——并发策略可能禁止我们
// 同时运行多个……
if cronJob.Spec.ConcurrencyPolicy == batchv1.ForbidConcurrent && len(activeJobs) > 0 {
log.V(1).Info("concurrency policy blocks concurrent runs, skipping", "num active", len(activeJobs))
return scheduledResult, nil
}
// ……或指示我们替换现有的……
if cronJob.Spec.ConcurrencyPolicy == batchv1.ReplaceConcurrent {
for _, activeJob := range activeJobs {
// 不关心 Job 是否已删除
if err := r.Delete(ctx, activeJob, client.PropagationPolicy(metav1.DeletePropagationBackground)); client.IgnoreNotFound(err) != nil {
log.Error(err, "unable to delete active job", "job", activeJob)
return ctrl.Result{}, err
}
}
}
确定如何处理现有 Job 后,实际创建期望的 Job
constructJobForCronJob
需根据 CronJob 模板构造 Job。复制模板中的 spec 与部分基本 object meta。
然后设置“计划时间”注解,以便每次协调重建 LastScheduleTime 字段。
最后,需要设置所有者引用。这允许 Kubernetes 垃圾回收器在删除 CronJob 时清理 Job,并让 controller-runtime 在给定 Job 变更(添加、删除、完成等)时确定需要协调哪个 CronJob。
constructJobForCronJob := func(cronJob *batchv1.CronJob, scheduledTime time.Time) (*kbatch.Job, error) {
// 希望给定名义启动时间的 Job 名称确定性,避免重复创建同一 Job
name := fmt.Sprintf("%s-%d", cronJob.Name, scheduledTime.Unix())
job := &kbatch.Job{
ObjectMeta: metav1.ObjectMeta{
Labels: make(map[string]string),
Annotations: make(map[string]string),
Name: name,
Namespace: cronJob.Namespace,
},
Spec: *cronJob.Spec.JobTemplate.Spec.DeepCopy(),
}
maps.Copy(job.Annotations, cronJob.Spec.JobTemplate.Annotations)
job.Annotations[scheduledTimeAnnotation] = scheduledTime.Format(time.RFC3339)
maps.Copy(job.Labels, cronJob.Spec.JobTemplate.Labels)
if err := ctrl.SetControllerReference(cronJob, job, r.Scheme); err != nil {
return nil, err
}
return job, nil
}
// 实际创建 Job……
job, err := constructJobForCronJob(&cronJob, missedRun)
if err != nil {
log.Error(err, "unable to construct job from template")
// 不要立即重试;失败发生在构造 Job 时。
// 将在下次计划运行时再次协调, and updates to the CronJob
// can also trigger reconciliation sooner
return scheduledResult, nil
}
// ...and create it on the cluster
if err := r.Create(ctx, job); err != nil {
log.Error(err, "unable to create Job for CronJob", "job", job)
if fetchErr := r.Get(ctx, req.NamespacedName, &cronJob); fetchErr != nil {
log.Error(fetchErr, "Failed to re-fetch CronJob")
return ctrl.Result{}, fetchErr
}
// 更新 status condition 以反映错误
meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{
Type: typeDegradedCronJob,
Status: metav1.ConditionTrue,
Reason: "JobCreationFailed",
Message: fmt.Sprintf("Failed to create job: %v", err),
})
if statusErr := r.Status().Update(ctx, &cronJob); statusErr != nil {
log.Error(statusErr, "Failed to update CronJob status")
}
return ctrl.Result{}, err
}
log.V(1).Info("created Job for CronJob run", "job", job)
if fetchErr := r.Get(ctx, req.NamespacedName, &cronJob); fetchErr != nil {
log.Error(fetchErr, "Failed to re-fetch CronJob")
return ctrl.Result{}, fetchErr
}
// Update status condition to reflect successful job creation
meta.SetStatusCondition(&cronJob.Status.Conditions, metav1.Condition{
Type: typeProgressingCronJob,
Status: metav1.ConditionTrue,
Reason: "JobCreated",
Message: fmt.Sprintf("Created job %s", job.Name),
})
if statusErr := r.Status().Update(ctx, &cronJob); statusErr != nil {
log.Error(statusErr, "Failed to update CronJob status")
}
7:当我们看到正在运行的 Job,或到了下次计划运行时间时重新入队
最后,返回上面准备好的结果,表示希望在下次运行需要发生时重新入队。这视为最大截止时间——若其间发生其他变更,如 Job 开始或完成、我们被修改等,可能更早再次协调。
// we'll requeue once we see the running job, and update our status
return scheduledResult, nil
}
Setup
最后,更新我们的设置。为了让 reconciler 能按所有者快速查找 Job,需要索引。我们声明一个稍后可与客户端一起用作伪字段名的索引键,然后描述如何从 Job 对象提取索引值。索引器会自动处理命名空间,因此若 Job 有 CronJob 所有者,我们只需提取所有者名称。
此外,我们将告知 manager 此控制器拥有一些 Job,这样当 Job 变更、被删除等时,它会自动对底层 CronJob 调用 Reconcile。
var (
jobOwnerKey = ".metadata.controller"
apiGVStr = batchv1.GroupVersion.String()
)
// SetupWithManager 将控制器与管理器进行设置。
func (r *CronJobReconciler) SetupWithManager(mgr ctrl.Manager) error {
// set up a real clock, since we're not in a test
if r.Clock == nil {
r.Clock = realClock{}
}
if err := mgr.GetFieldIndexer().IndexField(context.Background(), &kbatch.Job{}, jobOwnerKey, func(rawObj client.Object) []string {
// grab the job object, extract the owner...
job := rawObj.(*kbatch.Job)
owner := metav1.GetControllerOf(job)
if owner == nil {
return nil
}
// ...make sure it's a CronJob...
if owner.APIVersion != apiGVStr || owner.Kind != "CronJob" {
return nil
}
// ...and if so, return it
return []string{owner.Name}
}); err != nil {
return err
}
return ctrl.NewControllerManagedBy(mgr).
For(&batchv1.CronJob{}).
Owns(&kbatch.Job{}).
Named("cronjob").
Complete(r)
}
那真是个难题,不过现在我们有了一个可用的控制器。接下来让我们对集群进行测试,如果没有问题,就可以部署了!
你提到过主吗?
但是首先,记得我们说过要再次回到 main.go 吗?让我们来看看有没有什么变化,以及我们需要添加的内容。
Apache License
Copyright 2026 The Kubernetes authors.
Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
Imports
package main
import (
"crypto/tls"
"flag"
"os"
// 导入所有 Kubernetes 客户端身份验证插件(例如 Azure、GCP、OIDC 等)
// 确保 exec-entrypoint 和 run 可以使用它们。
_ "k8s.io/client-go/plugin/pkg/client/auth"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/healthz"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
"sigs.k8s.io/controller-runtime/pkg/webhook"
batchv1 "tutorial.kubebuilder.io/project/api/v1"
"tutorial.kubebuilder.io/project/internal/controller"
webhookv1 "tutorial.kubebuilder.io/project/internal/webhook/v1"
// +kubebuilder:scaffold:imports
)
首先要注意的差异是,kubebuilder 已将新 API group 的包(batchv1)加入我们的 Scheme。这意味着我们可以在控制器中使用这些对象。
如果使用任何其他 CRD,也需以相同方式添加其 Scheme。内置类型如 Job 的 Scheme 由 clientgoscheme 添加。
var (
scheme = runtime.NewScheme()
setupLog = ctrl.Log.WithName("setup")
)
func init() {
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
utilruntime.Must(batchv1.AddToScheme(scheme))
// +kubebuilder:scaffold:scheme
}
另一处变化是 kubebuilder 添加了一个调用我们 CronJob 控制器 SetupWithManager 方法的代码块。
// nolint:gocyclo
func main() {
Remaining code from main.go
var metricsAddr string
var metricsCertPath, metricsCertName, metricsCertKey string
var webhookCertPath, webhookCertName, webhookCertKey string
var webhookPort int
var enableLeaderElection bool
var probeAddr string
var secureMetrics bool
var enableHTTP2 bool
var tlsOpts []func(*tls.Config)
flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+
"使用 :8443 进行 HTTPS,使用 :8080 进行 HTTP,或者保持为 0 以禁用指标服务。")
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
flag.BoolVar(&enableLeaderElection, "leader-elect", false,
"Enable leader election for controller manager. "+
"启用此功能将确保只有一个活动的控制管理器。")
flag.BoolVar(&secureMetrics, "metrics-secure", true,
"如果设置,指标端点将通过 HTTPS 安全提供。请使用 --metrics-secure=false 以改为使用 HTTP。")
flag.StringVar(&webhookCertPath, "webhook-cert-path", "", "The directory that contains the webhook certificate.")
flag.StringVar(&webhookCertName, "webhook-cert-name", "tls.crt", "The name of the webhook certificate file.")
flag.StringVar(&webhookCertKey, "webhook-cert-key", "tls.key", "The name of the webhook key file.")
flag.IntVar(&webhookPort, "webhook-port", 9443, "Port the webhook server listens on. "+
"Defaults to 9443. Set -1 to disable the webhook server.")
flag.StringVar(&metricsCertPath, "metrics-cert-path", "",
"The directory that contains the metrics server certificate.")
flag.StringVar(&metricsCertName, "metrics-cert-name", "tls.crt", "The name of the metrics server certificate file.")
flag.StringVar(&metricsCertKey, "metrics-cert-key", "tls.key", "The name of the metrics server key file.")
flag.BoolVar(&enableHTTP2, "enable-http2", false,
"If set, HTTP/2 will be enabled for the metrics and webhook servers")
opts := zap.Options{
Development: true,
}
opts.BindFlags(flag.CommandLine)
flag.Parse()
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
// 如果 enable-http2 标志为 false(默认值),则应禁用 http/2
// 由于其存在的漏洞。更具体地说,禁用 http/2 将
// 防止受到 HTTP/2 流取消和
// 快速重置 CVE 的漏洞影响。有关更多信息,请参见:
// - https://github.com/advisories/GHSA-qppj-fm5r-hxr3
// - https://github.com/advisories/GHSA-4374-p667-p6c8
disableHTTP2 := func(c *tls.Config) {
setupLog.Info("Disabling HTTP/2")
c.NextProtos = []string{"http/1.1"}
}
if !enableHTTP2 {
tlsOpts = append(tlsOpts, disableHTTP2)
}
// 初始Webhook TLS选项
webhookTLSOpts := tlsOpts
webhookServerOptions := webhook.Options{
TLSOpts: webhookTLSOpts,
Port: webhookPort,
}
if len(webhookCertPath) > 0 {
setupLog.Info("使用提供的证书初始化 Webhook 证书监视器",
"webhook-cert-path", webhookCertPath, "webhook-cert-name", webhookCertName, "webhook-cert-key", webhookCertKey)
webhookServerOptions.CertDir = webhookCertPath
webhookServerOptions.CertName = webhookCertName
webhookServerOptions.KeyName = webhookCertKey
}
webhookServer := webhook.NewServer(webhookServerOptions)
// Metrics 端点已在 'config/default/kustomization.yaml'. Metrics 选项用于配置服务器。
// 更多信息:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.25.0/pkg/metrics/server
// - https://book.kubebuilder.io/reference/metrics.html
metricsServerOptions := metricsserver.Options{
BindAddress: metricsAddr,
SecureServing: secureMetrics,
TLSOpts: tlsOpts,
}
if secureMetrics {
// FilterProvider 用于通过认证/鉴权保护 metrics 端点。
// These configurations ensure that only authorized users and service accounts
// can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info:
// https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.25.0/pkg/metrics/filters#WithAuthenticationAndAuthorization
metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization
}
// 如果未指定证书,controller-runtime 将自动
// 为指标服务器生成自签名证书。虽然对于开发和测试来说很方便,
// 但这种设置不建议用于生产环境。
//
// TODO(用户):如果您启用 certManager,请取消注释以下行:
// - [METRICS-WITH-CERTS] 在 config/default/kustomization.yaml 中生成并使用
// 由 cert-manager 管理的指标服务器证书。
// - [PROMETHEUS-WITH-CERTS] 在 config/prometheus/kustomization.yaml 中用于 TLS 认证。
if len(metricsCertPath) > 0 {
setupLog.Info("使用提供的证书初始化指标证书监视器",
"metrics-cert-path", metricsCertPath, "metrics-cert-name", metricsCertName, "metrics-cert-key", metricsCertKey)
metricsServerOptions.CertDir = metricsCertPath
metricsServerOptions.CertName = metricsCertName
metricsServerOptions.KeyName = metricsCertKey
}
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
Metrics: metricsServerOptions,
WebhookServer: webhookServer,
HealthProbeBindAddress: probeAddr,
LeaderElection: enableLeaderElection,
LeaderElectionID: "80807133.tutorial.kubebuilder.io",
// LeaderElectionReleaseOnCancel 定义了当管理器结束时,领导者是否应自愿辞职
// 当管理器停止时,这要求二进制文件立即结束,否则这个设置是不安全的。设置这个可以显著
// 加快自愿领导者的过渡,因为新的领导者不必首先等待
// LeaseDuration 时间。
//
// 在提供的默认脚手架中,程序会在
// 管理器停止后立即结束,因此启用此选项是可以的。不过,
// 如果您正在进行或打算在管理器停止后执行任何操作,如执行清理,
// 那么使用它可能是不安全的。
// LeaderElectionReleaseOnCancel: true,
})
if err != nil {
setupLog.Error(err, "Failed to start manager")
os.Exit(1)
}
if err := (&controller.CronJobReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "Failed to create controller", "controller", "cronjob")
os.Exit(1)
}
我们还将为类型设置 Webhook,下一节再谈。只需把它们加到 manager。由于可能想单独运行 Webhook,或在本地测试控制器时不运行它们,我们用环境变量控制。
我们只需确保在本地运行时设置 ENABLE_WEBHOOKS=false。
// nolint:goconst
if os.Getenv("ENABLE_WEBHOOKS") != "false" {
if err := webhookv1.SetupCronJobWebhookWithManager(mgr); err != nil {
setupLog.Error(err, "Failed to create webhook", "webhook", "CronJob")
os.Exit(1)
}
}
// +kubebuilder:scaffold:builder
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
setupLog.Error(err, "Failed to set up health check")
os.Exit(1)
}
if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
setupLog.Error(err, "Failed to set up ready check")
os.Exit(1)
}
setupLog.Info("Starting manager")
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
setupLog.Error(err, "Failed to run manager")
os.Exit(1)
}
}
_现在_我们可以实现我们的控制器。
实现默认值/验证的 Webhook
如果您想为您的 CRD 实现 admission webhooks ,您需要做的唯一一件事是实现 CustomDefaulter 和(或)CustomValidator 接口。
Kubebuilder 会为您处理其余的任务,例如:
- 创建 webhook 服务器。
- 确保服务器已在管理器中添加。
- 为您的 webhooks 创建处理程序。
- 在你的服务器中为每个处理程序注册路径。
首先,让我们为我们的自定义资源定义(CronJob)搭建 Webhook。我们需要使用 --defaulting 和 --programmatic-validation 标志运行以下命令(因为我们的测试项目将使用默认和验证 Webhook):
kubebuilder create webhook --group batch --version v1 --kind CronJob --defaulting --programmatic-validation
这将为您构建 webhook 函数,并在您的 main.go 中将 webhook 注册到管理器。
Custom webhook paths
你可以使用 --defaulting-path 和 --validation-path 标志为 Webhook 指定自定义 HTTP 路径:
# Custom path for defaulting webhook
kubebuilder create webhook --group batch --version v1 --kind CronJob --defaulting --defaulting-path=/my-custom-mutate-path
# Custom path for validation webhook
kubebuilder create webhook --group batch --version v1 --kind CronJob --programmatic-validation --validation-path=/my-custom-validate-path
# Both webhooks with different custom paths
kubebuilder create webhook --group batch --version v1 --kind CronJob --defaulting --programmatic-validation \
--defaulting-path=/custom-mutate --validation-path=/custom-validate
这会更改 Webhook 标记注解中的路径,但不会更改 Webhook 文件的脚手架位置。Webhook 文件仍会创建在 internal/webhook/v1/。
Apache License
Copyright 2026 The Kubernetes authors.
Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
Imports
package v1
import (
"context"
"github.com/robfig/cron"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime/schema"
validationutils "k8s.io/apimachinery/pkg/util/validation"
"k8s.io/apimachinery/pkg/util/validation/field"
ctrl "sigs.k8s.io/controller-runtime"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
batchv1 "tutorial.kubebuilder.io/project/api/v1"
)
接下来,我们为 Webhook 设置日志器。
var cronjoblog = logf.Log.WithName("cronjob-resource")
然后,我们向 manager 设置 Webhook。
// SetupCronJobWebhookWithManager registers the webhook for CronJob in the manager.
func SetupCronJobWebhookWithManager(mgr ctrl.Manager) error {
return ctrl.NewWebhookManagedBy(mgr, &batchv1.CronJob{}).
WithValidator(&CronJobValidator{}).
WithDefaulter(&CronJobDefaulter{
DefaultConcurrencyPolicy: batchv1.AllowConcurrent,
DefaultSuspend: false,
DefaultSuccessfulJobsHistoryLimit: 3,
DefaultFailedJobsHistoryLimit: 1,
}).
Complete()
}
注意我们使用 kubebuilder 标记生成 Webhook 清单。此标记负责生成变更 Webhook 清单。
各标记的含义见 此处。
此标记负责生成变更 Webhook 清单。
// +kubebuilder:webhook:path=/mutate-batch-tutorial-kubebuilder-io-v1-cronjob,mutating=true,failurePolicy=fail,sideEffects=None,groups=batch.tutorial.kubebuilder.io,resources=cronjobs,verbs=create;update,versions=v1,name=mcronjob-v1.kb.io,admissionReviewVersions=v1
// CronJobDefaulter struct is responsible for setting default values on the custom resource of the
// Kind CronJob when those are created or updated.
//
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
// as it is used only for temporary operations and does not need to be deeply copied.
type CronJobDefaulter struct {
// Default values for various CronJob fields
DefaultConcurrencyPolicy batchv1.ConcurrencyPolicy
DefaultSuspend bool
DefaultSuccessfulJobsHistoryLimit int32
DefaultFailedJobsHistoryLimit int32
}
我们使用 admission.Defaulter 接口为 CRD 设置默认值。将自动提供调用此默认值逻辑的 Webhook。
Default 方法应变更接收者并设置默认值。
// Default implements admission.Defaulter so a webhook will be registered for the Kind CronJob.
func (d *CronJobDefaulter) Default(_ context.Context, obj *batchv1.CronJob) error {
cronjoblog.Info("Defaulting for CronJob", "name", obj.GetName())
// Set default values
d.applyDefaults(obj)
return nil
}
// applyDefaults applies default values to CronJob fields.
func (d *CronJobDefaulter) applyDefaults(cronJob *batchv1.CronJob) {
if cronJob.Spec.ConcurrencyPolicy == "" {
cronJob.Spec.ConcurrencyPolicy = d.DefaultConcurrencyPolicy
}
if cronJob.Spec.Suspend == nil {
cronJob.Spec.Suspend = new(bool)
*cronJob.Spec.Suspend = d.DefaultSuspend
}
if cronJob.Spec.SuccessfulJobsHistoryLimit == nil {
cronJob.Spec.SuccessfulJobsHistoryLimit = new(int32)
*cronJob.Spec.SuccessfulJobsHistoryLimit = d.DefaultSuccessfulJobsHistoryLimit
}
if cronJob.Spec.FailedJobsHistoryLimit == nil {
cronJob.Spec.FailedJobsHistoryLimit = new(int32)
*cronJob.Spec.FailedJobsHistoryLimit = d.DefaultFailedJobsHistoryLimit
}
}
我们可以用超出声明式校验能力的方式校验 CRD。通常声明式校验已足够,但有时更高级的用例需要复杂校验。
例如,我们将在下面看到,我们用它校验格式正确的 cron schedule,而无需编写很长的正则表达式。
若实现了 admission.Validator 接口,将自动提供调用该校验的 Webhook。
ValidateCreate、ValidateUpdate 与 ValidateDelete 方法应分别在创建、更新与删除时校验其接收者。我们将 ValidateCreate 与 ValidateUpdate 分开,以允许诸如使某些字段不可变、仅能在创建时设置之类的行为。ValidateDelete 也与 ValidateUpdate 分开,以允许删除时不同的校验行为。但在这里,我们对 ValidateCreate 与 ValidateUpdate 使用相同的共享校验。而在 ValidateDelete 中什么也不做,因为删除时无需校验任何内容。
此标记负责生成校验 Webhook 清单。
// NOTE: If you want to customise the 'path', use the flags '--defaulting-path' or '--validation-path'.
// +kubebuilder:webhook:path=/validate-batch-tutorial-kubebuilder-io-v1-cronjob,mutating=false,failurePolicy=fail,sideEffects=None,groups=batch.tutorial.kubebuilder.io,resources=cronjobs,verbs=create;update,versions=v1,name=vcronjob-v1.kb.io,admissionReviewVersions=v1
// CronJobValidator struct is responsible for validating the CronJob resource
// when it is created, updated, or deleted.
//
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
// as this struct is used only for temporary operations and does not need to be deeply copied.
type CronJobValidator struct {
// TODO(user): Add more fields as needed for validation
}
// ValidateCreate implements admission.Validator so a webhook will be registered for the type CronJob.
func (v *CronJobValidator) ValidateCreate(_ context.Context, obj *batchv1.CronJob) (admission.Warnings, error) {
cronjoblog.Info("Validation for CronJob upon creation", "name", obj.GetName())
return nil, validateCronJob(obj)
}
// ValidateUpdate implements admission.Validator so a webhook will be registered for the type CronJob.
func (v *CronJobValidator) ValidateUpdate(_ context.Context, oldObj, newObj *batchv1.CronJob) (admission.Warnings, error) {
cronjoblog.Info("Validation for CronJob upon update", "name", newObj.GetName())
return nil, validateCronJob(newObj)
}
// ValidateDelete implements admission.Validator so a webhook will be registered for the type CronJob.
func (v *CronJobValidator) ValidateDelete(_ context.Context, obj *batchv1.CronJob) (admission.Warnings, error) {
cronjoblog.Info("Validation for CronJob upon deletion", "name", obj.GetName())
// TODO(user): fill in your validation logic upon object deletion.
return nil, nil
}
我们校验 CronJob 的名称与 spec。
// validateCronJob validates the fields of a CronJob object.
func validateCronJob(cronjob *batchv1.CronJob) error {
var allErrs field.ErrorList
if err := validateCronJobName(cronjob); err != nil {
allErrs = append(allErrs, err)
}
if err := validateCronJobSpec(cronjob); err != nil {
allErrs = append(allErrs, err)
}
if len(allErrs) == 0 {
return nil
}
return apierrors.NewInvalid(
schema.GroupKind{Group: "batch.tutorial.kubebuilder.io", Kind: "CronJob"},
cronjob.Name, allErrs)
}
一些字段由 OpenAPI schema 声明式校验。可在 设计 API 一节找到 kubebuilder 校验标记(前缀为 // +kubebuilder:validation)。运行 controller-gen crd -w 可查看所有支持声明校验的标记,或见 此处。
func validateCronJobSpec(cronjob *batchv1.CronJob) *field.Error {
// The field helpers from the kubernetes API machinery help us return nicely
// structured validation errors.
return validateScheduleFormat(
cronjob.Spec.Schedule,
field.NewPath("spec").Child("schedule"))
}
我们需要校验 cron schedule 格式正确。
func validateScheduleFormat(schedule string, fldPath *field.Path) *field.Error {
if _, err := cron.ParseStandard(schedule); err != nil {
return field.Invalid(fldPath, schedule, err.Error())
}
return nil
}
validateCronJobName() Code Implementation
字符串字段长度的校验可由校验 schema 以声明式完成。
但 ObjectMeta.Name 字段定义在 apimachinery 仓库的共享包中,因此无法使用校验 schema 对其进行声明式校验。
func validateCronJobName(cronjob *batchv1.CronJob) *field.Error {
if len(cronjob.Name) > validationutils.DNS1035LabelMaxLength-11 {
// The job name length is 63 characters like all Kubernetes objects
// (which must fit in a DNS subdomain). The cronjob controller appends
// a 11-character suffix to the cronjob (`-$TIMESTAMP`) when creating
// a job. The job name length limit is 63 characters. Therefore cronjob
// names must have length <= 63-11=52. If we don't validate this here,
// then job creation will fail later.
return field.Invalid(field.NewPath("metadata").Child("name"), cronjob.Name, "must be no more than 52 characters")
}
return nil
}
运行和部署控制器
Optional
如果选择对 API 定义进行任何更改,请在继续之前生成类似于 CR 或 CRD 的清单。
make manifests
为了测试控制器,我们可以在集群上本地运行它。不过,在此之前,我们需要安装我们的 CRD,按照 快速启动 的指南。这将自动使用 controller-tools 更新 YAML 清单(如果需要的话):
make install
安装 CRD 后,针对集群运行控制器。这使用你连接集群所用的凭据,因此暂时不必担心 RBAC。
在另一个终端中运行
export ENABLE_WEBHOOKS=错误
make run
您应该看到控制器关于启动的日志,但它暂时不会执行任何操作。
此时,我们需要一个 CronJob 进行测试。让我们在 config/samples/batch_v1_cronjob.yaml 中写一个示例,并使用它:
apiVersion: batch.tutorial.kubebuilder.io/v1
kind: CronJob
metadata:
labels:
app.kubernetes.io/name: project
app.kubernetes.io/managed-by: kustomize
name: cron作业示例
spec:
日程安排: "*/1 * * * *"
启动截止时间(秒): 60
并发策略: 允许 # 显式指定,但允许也是默认值。
工作模板:
spec:
template:
spec:
安全上下文:
以非root用户身份运行: true
以用户身份运行: 1000
seccompProfile:
type: RuntimeDefault
容器:
- name: 你好
image: busybox
args:
- /bin/sh
- -c
- 日期; 在Kubernetes集群中打招呼。
安全上下文:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: false
重启策略: 失败时
kubectl create -f config/samples/batch_v1_cronjob.yaml
此时,你应该会看到一阵忙碌的活动。如果你观察这些变化,你应该能看到你的定时任务在运行,并更新状态:
kubectl get cronjob.batch.tutorial.kubebuilder.io -o yaml
kubectl get job
现在我们知道它可以正常工作,我们可以在集群中运行它。停止 make run 调用,然后运行
make docker-build docker-push IMG=<some-registry>/<project-name>:tag
make deploy IMG=<some-registry>/<project-name>:tag
如果我们像之前一样再次列出定时任务,我们应该能看到控制器再次正常运行!
部署 cert-manager
使用 cert-manager 为 Webhook 服务器供应证书。只要把证书放到期望位置,其他方案也可行。
您可以按照 cert-manager 文档 来进行安装。
cert-manager 还有一个组件称为 CA Injector,它的责任是将 CA 包注入到 MutatingWebhookConfiguration / ValidatingWebhookConfiguration 中。
要实现这一点,您需要在 MutatingWebhookConfiguration / ValidatingWebhookConfiguration 对象中使用一个带有键 cert-manager.io/inject-ca-from 的注解。该注解的值应指向一个现有的 证书请求实例,格式为 <certificate-namespace>/<certificate-name>。
这是我们用于注解 MutatingWebhookConfiguration / ValidatingWebhookConfiguration 对象的 kustomize 补丁。
部署 Admission Webhook
cert-manager
按照 cert-manager 安装指南 安装 cert-manager 捆绑包。
构建你的形象
运行以下命令以在本地构建您的镜像。
make docker-build docker-push IMG=<some-registry>/<project-name>:tag
部署 Webhooks
您需要通过 kustomize 启用 webhook 和证书管理器配置。config/default/kustomization.yaml 现在应该如下所示:
Resources - 添加 webhook 与 cert-manager 资源:
# [WEBHOOK] 要启用 webhook,请取消注释所有带有 [WEBHOOK] 前缀的部分,包括在
# crd/kustomization.yaml中的那部分。
# crd/kustomization.yaml
- ../webhook
# [CERTMANAGER] 要启用 cert-manager,请取消注释所有带 'CERTMANAGER' 的部分。需要 'WEBHOOK' 组件。
- ../证书管理器
Patches - 添加 webhook manager 补丁:
# [WEBHOOK] 要启用 webhook,请取消注释所有带有 [WEBHOOK] 前缀的部分,包括在
# crd/kustomization.yaml中的那部分。
# crd/kustomization.yaml
- path: manager_webhook_patch.yaml
目标:
kind: 部署
Replacements - 添加 webhook 证书替换:
- source: # 如果您有任何 webhook,请取消注释以下代码块。: 服务
version: v1
name: Webhook 服务
字段路径: .metadata.name # 服务名称
目标:
- 选择:
kind: 证书
group: cert-manager.io
version: v1
name: 服务证书
字段路径:
- .spec.dnsNames.0
- .spec.dnsNames.1
选项:
分隔符: '.'
索引: 0
create: true
- source:
kind: 服务
version: v1
name: Webhook 服务
字段路径: .metadata.namespace # 服务的命名空间
targets:
- 选择:
kind: 证书
group: cert-manager.io
version: v1
name: 服务证书
字段路径:
- .spec.dnsNames.0
- .spec.dnsNames.1
选项:
分隔符: '.'
索引: 1
create: true
- source: # 如果您有一个有效的验证Webhook (--programmatic-validation),请取消注释以下代码块
kind: 证书
group: cert-manager.io
version: v1
name: serving-cert # 此名称应与 certificate.yaml 中的名称匹配
fieldPath: .metadata.namespace # 证书 CR 的命名空间
目标:
- 选择:
kind: 有效性Webhook配置
字段路径:
- .metadata.annotations.[cert-manager.io/inject-ca-from]
选项:
分隔符: '/'
索引: 0
create: true
- source:
kind: 证书
group: cert-manager.io
version: v1
name: 服务证书
字段路径: .metadata.name
目标:
- 选择:
kind: 有效性Webhook配置
字段路径:
- .metadata.annotations.[cert-manager.io/inject-ca-from]
选项:
分隔符: '/'
索引: 1
create: true
- source: # 如果你有一个默认的 Webhook (--defaulting),请取消注释以下代码块
类型: 证书
group: cert-manager.io
version: v1
name: 服务证书
字段路径: .metadata.namespace # 证书 CR 的命名空间
目标:
- 选择:
kind: 变更Webhook配置
字段路径:
- .metadata.annotations.[cert-manager.io/inject-ca-from]
选项:
分隔符: '/'
索引: 0
create: true
- source:
kind: 证书
group: cert-manager.io
version: v1
name: 服务证书
字段路径: .metadata.name
目标:
- 选择:
kind: 变更Webhook配置
字段路径:
- .metadata.annotations.[cert-manager.io/inject-ca-from]
选项:
分隔符: '/'
索引: 1
create: true
现在 config/crd/kustomization.yaml 应该如下所示:
# This kustomization.yaml is not intended to be run by itself,
# 因为它依赖于此 kustomize 包之外的 service 名称与命名空间。
# 应由 config/default 运行
resources:
- bases/batch.tutorial.kubebuilder.io_cronjobs.yaml
# +kubebuilder:scaffold:crdkustomizeresource
贴片:
# [WEBHOOK] 要启用 Webhook,请取消注释所有带 [WEBHOOK] 前缀的部分。
# 此处的补丁用于为每个 CRD 启用转换 Webhook
# +kubebuilder:scaffold:crdkustomizewebhookpatch
# [WEBHOOK] 要启用 Webhook,请取消注释以下部分
# 以下配置用于指导 kustomize 如何对 CRD 做 kustomization。
#configurations:
#- kustomizeconfig.yaml
现在您可以通过以下方式将其部署到您的集群中:
make deploy IMG=<some-registry>/<project-name>:tag
请稍等,直到 webhook pod 启动并且证书被配置。通常在 1 分钟内完成。
现在您可以创建一个有效的 CronJob 来测试您的 webhooks。创建过程应该成功完成。
kubectl create -f config/samples/batch_v1_cronjob.yaml
您还可以尝试创建一个无效的 CronJob(例如,使用格式不正确的调度字段)。您应该会看到创建失败并出现验证错误。
编写控制器测试
测试 Kubernetes 控制器是一个大主题,而 kubebuilder 为您生成的样板测试文件相对较少。
为了引导您了解用于 Kubebuilder 生成的控制器的集成测试模式,我们将重新审视在首个教程中构建的 CronJob,并为其编写一个简单的测试。
基本的方法是,在您生成的 suite_test.go 文件中,您将使用 envtest 创建一个本地 Kubernetes API 服务器,实例化并运行您的控制器,然后编写额外的 *_test.go 文件,通过 Ginkgo 来进行测试。
如果您想要调整您的 envtest 集群的配置,请参阅为集成测试配置 envtest一节,以及envtest 文档。
测试环境设置
Apache License
Copyright 2026 The Kubernetes authors.
Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
Imports
当我们在上一章用 kubebuilder create api 创建 CronJob API 时,Kubebuilder 已为你做了一些测试工作。Kubebuilder 脚手架生成了 internal/controller/suite_test.go,完成了测试环境的基础设置。
首先,它将包含必要的 import。
package controller
import (
"context"
"os"
"path/filepath"
"testing"
ctrl "sigs.k8s.io/controller-runtime"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/envtest"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
batchv1 "tutorial.kubebuilder.io/project/api/v1"
// +kubebuilder:scaffold:imports
)
// These tests use Ginkgo (BDD-style Go testing framework). Refer to
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
现在,我们来过一遍生成的代码。
var (
ctx context.Context
cancel context.CancelFunc
testEnv *envtest.Environment
cfg *rest.Config
k8sClient client.Client // You'll be using this client in your tests.
)
func TestControllers(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Controller Suite")
}
var _ = BeforeSuite(func() {
logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
ctx, cancel = context.WithCancel(context.TODO())
var err error
CronJob Kind 被加入测试环境使用的运行时 Scheme。这确保 CronJob API 已向 Scheme 注册,使测试控制器能够识别并与 CronJob 资源交互。
err = batchv1.AddToScheme(scheme.Scheme)
Expect(err).NotTo(HaveOccurred())
在 schemas 之后,你会看到以下标记。正是此标记允许在向项目添加新 API 时自动在此处加入新 schema。
// +kubebuilder:scaffold:scheme
envtest 环境配置为从指定目录加载自定义资源定义(CRD)。此设置使测试环境能够识别并与这些 CRD 定义的自定义资源交互。
By("bootstrapping test environment")
testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")},
ErrorIfCRDPathMissing: true,
}
// Retrieve the first found binary directory to allow running tests from IDEs
if getFirstFoundEnvTestBinaryDir() != "" {
testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir()
}
然后,我们启动 envtest 集群。
// cfg is defined in this file globally.
cfg, err = testEnv.Start()
Expect(err).NotTo(HaveOccurred())
Expect(cfg).NotTo(BeNil())
创建一个用于测试 CRUD 操作的客户端。
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
Expect(err).NotTo(HaveOccurred())
Expect(k8sClient).NotTo(BeNil())
不过,此自动生成文件缺少的一点是实际启动控制器的方式。上面的代码会设置用于与自定义 Kind 交互的客户端,但无法测试控制器行为。若要测试自定义控制器逻辑,需要在 BeforeSuite() 函数中加入一些眼熟的 manager 逻辑,以便把自定义控制器注册到此测试集群上运行。
你可能注意到下面的代码以与 CronJob 项目 main.go 几乎相同的逻辑运行控制器!唯一区别是 manager 在单独的 goroutine 中启动,以免在测试结束后阻塞 envtest 的清理。
注意我们同时设置了「实时」k8s 客户端与来自 manager 的单独客户端。这是因为在测试中做断言时,通常希望针对 API 服务器的实时状态断言。若使用 manager 的客户端(k8sManager.GetClient),会针对缓存内容断言,更慢且可能使测试不稳定。也可用 manager 的 APIReader 达到同样效果,但那样测试断言与设置中会有两个客户端(一个读、一个写),容易出错。
不过请注意,我们仍让 reconciler 针对 manager 的缓存客户端运行——我们希望控制器表现得像生产环境,并且使用了直接与 API 服务器通信时不可用的缓存功能(如索引)。
k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{
Scheme: scheme.Scheme,
})
Expect(err).ToNot(HaveOccurred())
err = (&CronJobReconciler{
Client: k8sManager.GetClient(),
Scheme: k8sManager.GetScheme(),
}).SetupWithManager(k8sManager)
Expect(err).ToNot(HaveOccurred())
go func() {
defer GinkgoRecover()
err = k8sManager.Start(ctx)
Expect(err).ToNot(HaveOccurred(), "failed to run manager")
}()
})
Kubebuilder 还会生成用于清理 envtest 以及在 controllers/ 目录实际运行测试文件的样板函数。你无需改动这些。
var _ = AfterSuite(func() {
By("tearing down the test environment")
cancel()
err := testEnv.Stop()
Expect(err).NotTo(HaveOccurred())
})
现在控制器已在测试集群上运行,且客户端已准备好对 CronJob 执行操作,我们可以开始编写集成测试!
// getFirstFoundEnvTestBinaryDir locates the first binary in the specified path.
// ENVTEST-based tests depend on specific binaries, usually located in paths set by
// controller-runtime. When running tests directly (e.g., via an IDE) without using
// Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured.
//
// This function streamlines the process by finding the required binaries, similar to
// setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are
// properly set up, run 'make setup-envtest' beforehand.
func getFirstFoundEnvTestBinaryDir() string {
basePath := filepath.Join("..", "..", "bin", "k8s")
entries, err := os.ReadDir(basePath)
if err != nil {
logf.Log.Error(err, "Failed to read directory", "path", basePath)
return ""
}
for _, entry := range entries {
if entry.IsDir() {
return filepath.Join(basePath, entry.Name())
}
}
return ""
}
测试您的控制器行为
Apache License
Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
理想情况下,应为每个脚手架生成并在 suite_test.go 中调用的控制器准备一个 <kind>_controller_test.go。因此,我们为 CronJob 控制器编写示例测试(cronjob_controller_test.go)。
Imports
照例,我们从必要的 import 开始。
package controller
import (
"context"
"reflect"
"time"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
batchv1 "k8s.io/api/batch/v1"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
cronjobv1 "tutorial.kubebuilder.io/project/api/v1"
)
编写简单集成测试的第一步是实际创建一个可对之运行测试的 CronJob 实例。注意要创建 CronJob,需要创建一个包含 CronJob 规格的桩 CronJob 结构体。
注意:创建桩 CronJob 时,CronJob 还需要其必需下游对象的桩。若没有下面的桩 Job 模板 spec 与 Pod 模板 spec,Kubernetes API 将无法创建 CronJob。
var _ = Describe("CronJob controller", func() {
Context("CronJob controller test", func() {
const (
CronjobName = "test-cronjob"
testContainerName = "test-container"
testContainerImage = "test-image"
)
ctx := context.Background()
namespace := &v1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: CronjobName,
Namespace: CronjobName,
},
}
typeNamespacedName := types.NamespacedName{
Name: CronjobName,
Namespace: CronjobName,
}
cronJob := &cronjobv1.CronJob{}
SetDefaultEventuallyTimeout(2 * time.Minute)
SetDefaultEventuallyPollingInterval(time.Second)
BeforeEach(func() {
By("Creating the Namespace to perform the tests")
err := k8sClient.Get(ctx, types.NamespacedName{Name: CronjobName}, &v1.Namespace{})
if err != nil && errors.IsNotFound(err) {
err = k8sClient.Create(ctx, namespace)
Expect(err).NotTo(HaveOccurred())
}
By("creating the custom resource for the Kind CronJob")
cronJob = &cronjobv1.CronJob{}
err = k8sClient.Get(ctx, typeNamespacedName, cronJob)
if err != nil && errors.IsNotFound(err) {
让我们以与从 config/samples 下的清单应用相同的方式 mock 自定义资源
cronJob = &cronjobv1.CronJob{
ObjectMeta: metav1.ObjectMeta{
Name: CronjobName,
Namespace: namespace.Name,
},
Spec: cronjobv1.CronJobSpec{
Schedule: "1 * * * *",
JobTemplate: batchv1.JobTemplateSpec{
Spec: batchv1.JobSpec{
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Name: testContainerName,
Image: testContainerImage,
},
},
RestartPolicy: v1.RestartPolicyOnFailure,
},
},
},
},
},
}
err = k8sClient.Create(ctx, cronJob)
Expect(err).NotTo(HaveOccurred())
}
})
每个测试后,清理上面创建的资源。
AfterEach(func() {
By("removing the custom resource for the Kind CronJob")
found := &cronjobv1.CronJob{}
err := k8sClient.Get(ctx, typeNamespacedName, found)
Expect(err).NotTo(HaveOccurred())
Eventually(func(g Gomega) {
g.Expect(k8sClient.Delete(context.TODO(), found)).To(Succeed())
}).Should(Succeed())
// TODO(user): Attention if you improve this code by adding other context test you MUST
// be aware of the current delete namespace limitations.
// More info: https://book.kubebuilder.io/reference/envtest.html#testing-considerations
By("Deleting the Namespace to perform the tests")
_ = k8sClient.Delete(ctx, namespace)
})
现在我们可以开始实现校验控制器协调行为的测试。
It("should successfully reconcile a custom resource for CronJob", func() {
By("Checking if the custom resource was successfully created")
Eventually(func(g Gomega) {
found := &cronjobv1.CronJob{}
g.Expect(k8sClient.Get(ctx, typeNamespacedName, found)).To(Succeed())
}).Should(Succeed())
创建此 CronJob 后,验证控制器是否正确初始化 status 条件。控制器在后台运行(在 suite_test.go 中启动),因此会自动检测我们的 CronJob 并设置初始条件。
By("Checking that status conditions are initialized")
Eventually(func(g Gomega) {
g.Expect(k8sClient.Get(ctx, typeNamespacedName, cronJob)).To(Succeed())
g.Expect(cronJob.Status.Conditions).NotTo(BeEmpty())
}).Should(Succeed())
现在验证 CronJob 最初没有活动 Job。这里使用 Gomega 的 Consistently() 检查以确保 status 保持稳定,确认控制器未过早创建 Job。
By("Checking that the CronJob has zero active Jobs")
Consistently(func(g Gomega) {
g.Expect(k8sClient.Get(ctx, typeNamespacedName, cronJob)).To(Succeed())
g.Expect(cronJob.Status.Active).To(BeEmpty())
}).WithTimeout(time.Second * 10).WithPolling(time.Millisecond * 250).Should(Succeed())
接下来,我们实际创建一个属于 CronJob 的桩 Job。将 Job 的 status Active 计数设为 2,以模拟正在运行两个 Pod,即 Job 处于活动运行中。
然后我们将 Job 的所有者引用指向测试 CronJob。这确保测试 Job 属于并被我们的测试 CronJob 跟踪。
By("Creating a new Job owned by the CronJob")
testJob := &batchv1.Job{
ObjectMeta: metav1.ObjectMeta{
Name: "test-job",
Namespace: namespace.Name,
},
Spec: batchv1.JobSpec{
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Name: testContainerName,
Image: testContainerImage,
},
},
RestartPolicy: v1.RestartPolicyOnFailure,
},
},
},
}
// Note that your CronJob’s GroupVersionKind is required to set up this owner reference.
kind := reflect.TypeFor[cronjobv1.CronJob]().Name()
gvk := cronjobv1.GroupVersion.WithKind(kind)
controllerRef := metav1.NewControllerRef(cronJob, gvk)
testJob.SetOwnerReferences([]metav1.OwnerReference{*controllerRef})
Expect(k8sClient.Create(ctx, testJob)).To(Succeed())
// Note that you can not manage the status values while creating the resource.
// The status field is managed separately to reflect the current state of the resource.
// Therefore, it should be updated using a PATCH or PUT operation after the resource has been created.
// Additionally, it is recommended to use StatusConditions to manage the status. For further information see:
// https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
testJob.Status.Active = 2
Expect(k8sClient.Status().Update(ctx, testJob)).To(Succeed())
将此 Job 添加到测试 CronJob 应触发控制器的 reconciler 逻辑。之后,我们可以验证控制器最终是否按预期更新了 CronJob 的 Status 字段!
By("Checking that the CronJob has one active Job in status")
Eventually(func(g Gomega) {
g.Expect(k8sClient.Get(ctx, typeNamespacedName, cronJob)).To(Succeed())
g.Expect(cronJob.Status.Active).To(HaveLen(1), "should have exactly one active job")
g.Expect(cronJob.Status.Active[0].Name).To(Equal("test-job"), "the active job name should match")
}).Should(Succeed())
最后,验证控制器是否正确设置了 status 条件。Status 条件是 Kubernetes API 约定的关键部分,让用户和其他控制器能够理解资源状态。
当有活动 Job 时,Available 条件应为 True,原因为 JobsActive。
By("Checking the latest Status Condition added to the CronJob instance")
Expect(k8sClient.Get(ctx, typeNamespacedName, cronJob)).To(Succeed())
var conditions []metav1.Condition
Expect(cronJob.Status.Conditions).To(ContainElement(
HaveField("Type", Equal("Available")), &conditions))
Expect(conditions).To(HaveLen(1), "should have one Available condition")
Expect(conditions[0].Status).To(Equal(metav1.ConditionTrue), "Available should be True")
Expect(conditions[0].Reason).To(Equal("JobsActive"), "reason should be JobsActive")
接下来,验证控制器是否根据历史限制裁剪旧的已完成 Job。
By("Checking that old finished Jobs are pruned by history limits")
failedJobsHistoryLimit := int32(1)
successfulJobsHistoryLimit := int32(1)
Eventually(func(g Gomega) {
g.Expect(k8sClient.Get(ctx, typeNamespacedName, cronJob)).To(Succeed())
cronJob.Spec.FailedJobsHistoryLimit = &failedJobsHistoryLimit
cronJob.Spec.SuccessfulJobsHistoryLimit = &successfulJobsHistoryLimit
g.Expect(k8sClient.Update(ctx, cronJob)).To(Succeed())
}).Should(Succeed())
createFinishedJob := func(name string, conditionType batchv1.JobConditionType, startTime *metav1.Time) *batchv1.Job {
finishedJob := &batchv1.Job{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace.Name,
},
Spec: batchv1.JobSpec{
Template: v1.PodTemplateSpec{
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Name: testContainerName,
Image: testContainerImage,
},
},
RestartPolicy: v1.RestartPolicyOnFailure,
},
},
},
}
finishedJob.SetOwnerReferences([]metav1.OwnerReference{*controllerRef})
Expect(k8sClient.Create(ctx, finishedJob)).To(Succeed())
finishedJob.Status.StartTime = startTime
finishedJob.Status.Conditions = []batchv1.JobCondition{{
Type: conditionType,
Status: v1.ConditionTrue,
}}
if conditionType == batchv1.JobFailed {
finishedJob.Status.Conditions = append([]batchv1.JobCondition{{
Type: batchv1.JobFailureTarget,
Status: v1.ConditionTrue,
}}, finishedJob.Status.Conditions...)
}
if conditionType == batchv1.JobComplete {
finishedJob.Status.CompletionTime = startTime
finishedJob.Status.Conditions = append([]batchv1.JobCondition{{
Type: batchv1.JobSuccessCriteriaMet,
Status: v1.ConditionTrue,
}}, finishedJob.Status.Conditions...)
}
Expect(k8sClient.Status().Update(ctx, finishedJob)).To(Succeed())
return finishedJob
}
baseTime := time.Now()
failedOldStartTime := metav1.NewTime(baseTime.Add(-time.Minute))
failedNewStartTime := metav1.NewTime(baseTime.Add(time.Second))
successfulOldStartTime := metav1.NewTime(baseTime.Add(-time.Minute))
successfulNewStartTime := metav1.NewTime(baseTime.Add(time.Second))
failedOldJob := createFinishedJob("test-failed-job-old", batchv1.JobFailed, &failedOldStartTime)
failedNewJob := createFinishedJob("test-failed-job-new", batchv1.JobFailed, &failedNewStartTime)
successfulOldJob := createFinishedJob("test-successful-job-old", batchv1.JobComplete, &successfulOldStartTime)
successfulNewJob := createFinishedJob("test-successful-job-new", batchv1.JobComplete, &successfulNewStartTime)
assertJobDeleted := func(g Gomega, job *batchv1.Job) {
found := &batchv1.Job{}
err := k8sClient.Get(ctx, types.NamespacedName{Name: job.Name, Namespace: job.Namespace}, found)
g.Expect(errors.IsNotFound(err)).To(BeTrue())
}
assertJobExists := func(g Gomega, job *batchv1.Job) {
found := &batchv1.Job{}
g.Expect(k8sClient.Get(ctx, types.NamespacedName{Name: job.Name, Namespace: job.Namespace}, found)).To(Succeed())
}
Eventually(func(g Gomega) {
assertJobDeleted(g, failedOldJob)
assertJobDeleted(g, successfulOldJob)
assertJobExists(g, failedNewJob)
assertJobExists(g, successfulNewJob)
}).Should(Succeed())
})
})
})
写完这些代码后,可以再次在 controllers/ 目录运行 make test 或 go test ./... 来运行新测试!
以上的状态更新示例展示了一种针对自定义 Kind 及其下游对象的一般测试策略。到目前为止,希望您已经学习了以下测试控制器行为的方法:
- 在 envtest 集群上设置您的控制器
- 为创建测试对象编写存根
- 将变化孤立到一个对象,以测试特定控制器的行为
教程:多版本 API
大多数项目开始时都有一个不断变化的 alpha API。然而,最终,大多数项目需要转向一个更稳定的 API。然而,一旦你的 API 稳定后,你就不能对其进行破坏性更改。这就是 API 版本派上用场的地方。
对 CronJob API spec 做一些更改,并确保 CronJob 项目支持所有不同版本。
如果您还没有,请确保您已经阅读过基础的 CronJob 教程。
接下来,让我们找出我们想要进行哪些更改…
改变现状
在Kubernetes API中,相对常见的变化是将一些以前是非结构化或以某种特殊字符串格式存储的数据,改变为结构化数据。我们的 schedule 字段恰好符合这一点 —— 目前在 v1 中,我们的计划看起来是这样的:
日程安排: "*/1 * * * *"
这是一个典型的特殊字符串格式示例(如果你不是一个 Unix 系统管理员,它也相当难以阅读)。
让它更结构化一些。根据 CronJob 代码,它支持「标准」Cron 格式。
在 Kubernetes 中,所有版本必须能安全地相互往返转换。这意味着若从版本 1 转到版本 2,再转回版本 1,不得丢失信息。因此,对 API 的任何变更都必须与 v1 所支持的内容兼容,并且要确保在 v2 添加的任何内容在 v1 中也受支持。有时这意味着需要向 v1 添加新字段,但本例不必,因为你没有添加新功能。
考虑到这些,让我们将上述示例稍微结构化一下:
日程安排:
分钟: */1
现在,我们至少有了每个字段的标签,但我们仍然可以轻松支持每个字段的不同语法。
此变更需要新的 API 版本。称之为 v2:
kubebuilder create api --group batch --version v2 --kind CronJob
按 y 以继续\
现在,让我们复制我们现有的类型,并进行更改:
Apache License
Copyright 2026 The Kubernetes authors.
Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
由于我们位于 v2 包中,controller-gen 会自动假定这是针对 v2 版本。我们可以用 +versionName 标记 覆盖。
package v2
Imports
import (
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
// 请编辑本文件!这是供您拥有的脚手架代码!
// 注意:需要 json 标签。新增字段必须带有 json 标签才能被序列化。
我们将大体保持 spec 不变,只是把 schedule 字段改为新类型。
// CronJobSpec 定义 CronJob 的期望状态
type CronJobSpec struct {
// Cron 格式的调度表达式,参见 https://en.wikipedia.org/wiki/Cron.
// +required
Schedule CronSchedule `json:"schedule"`
CronJobSpec Full Code
// startingDeadlineSeconds 定义若错过计划时间则启动 Job 的秒数截止
// time for any reason. Missed jobs executions will be counted as failed ones.
// +optional
// +kubebuilder:validation:Minimum=0
StartingDeadlineSeconds *int64 `json:"startingDeadlineSeconds,omitempty"`
// concurrencyPolicy defines how to treat concurrent executions of a Job.
// 有效值为:
// - "Allow"(默认):允许 CronJob 并发运行;
// - "Forbid":禁止并发,若上次运行未结束则跳过下次;
// - "Replace":取消当前运行的 Job 并用新 Job 替换
// +optional
// +kubebuilder:default:=Allow
ConcurrencyPolicy ConcurrencyPolicy `json:"concurrencyPolicy,omitempty"`
// suspend 告诉控制器暂停后续执行,它
// 不适用于已开始的执行。默认为 false。
// +optional
Suspend *bool `json:"suspend,omitempty"`
// jobTemplate 定义执行 CronJob 时将创建的 Job。
// +required
JobTemplate batchv1.JobTemplateSpec `json:"jobTemplate"`
// successfulJobsHistoryLimit 定义要保留的成功完成 Job 数量。
// 使用指针以区分显式零值与未指定。
// +optional
// +kubebuilder:validation:Minimum=0
SuccessfulJobsHistoryLimit *int32 `json:"successfulJobsHistoryLimit,omitempty"`
// failedJobsHistoryLimit 定义要保留的失败完成 Job 数量。
// 使用指针以区分显式零值与未指定。
// +optional
// +kubebuilder:validation:Minimum=0
FailedJobsHistoryLimit *int32 `json:"failedJobsHistoryLimit,omitempty"`
}
接下来,我们需要定义一个类型来保存 schedule。根据上面提议的 YAML,它会为每个对应的 Cron「字段」提供一个字段。
// describes a Cron schedule.
type CronSchedule struct {
// minute specifies the minutes during which the job executes.
// +optional
Minute *CronField `json:"minute,omitempty"`
// hour specifies the hour during which the job executes.
// +optional
Hour *CronField `json:"hour,omitempty"`
// dayOfMonth specifies the day of the month during which the job executes.
// +optional
DayOfMonth *CronField `json:"dayOfMonth,omitempty"`
// month specifies the month during which the job executes.
// +optional
Month *CronField `json:"month,omitempty"`
// dayOfWeek specifies the day of the week during which the job executes.
// +optional
DayOfWeek *CronField `json:"dayOfWeek,omitempty"`
}
最后,我们将定义一个包装类型来表示字段。我们可以为此字段附加额外校验,但目前仅用于文档目的。
// represents a Cron field specifier.
type CronField string
Other Types
所有其他类型将保持与之前相同。
// ConcurrencyPolicy 描述如何处理 Job。
// 下列并发策略只能指定其一。
// 若未指定下列任一策略,则默认
// 为 AllowConcurrent。
// +kubebuilder:validation:Enum=Allow;Forbid;Replace
type ConcurrencyPolicy string
const (
// AllowConcurrent 允许 CronJob 并发运行。
AllowConcurrent ConcurrencyPolicy = "Allow"
// ForbidConcurrent 禁止并发运行;若上次
// 尚未结束则跳过下次运行。
ForbidConcurrent ConcurrencyPolicy = "Forbid"
// ReplaceConcurrent 取消当前运行的 Job 并用新 Job 替换。
ReplaceConcurrent ConcurrencyPolicy = "Replace"
)
// CronJobStatus 定义 CronJob 的观测状态.
type CronJobStatus struct {
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
// Important: Run "make" to regenerate code after modifying this file
// active 定义指向当前运行 Job 的指针列表。
// +optional
// +listType=atomic
// +kubebuilder:validation:MinItems=1
// +kubebuilder:validation:MaxItems=10
Active []corev1.ObjectReference `json:"active,omitempty"`
// lastScheduleTime defines the information when was the last time the job was successfully scheduled.
// +optional
LastScheduleTime *metav1.Time `json:"lastScheduleTime,omitempty"`
// Kubernetes API 约定参见:
// https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties
// conditions 表示 CronJob 资源的当前状态。
// 每个 condition 有唯一类型,反映资源某一方面的状态。
//
// Standard condition types include:
// - "Available": the resource is fully functional
// - "Progressing": the resource is being created or updated
// - "Degraded": the resource failed to reach or maintain its desired state
//
// The status of each condition is one of True, False, or Unknown.
// +listType=map
// +listMapKey=type
// +optional
Conditions []metav1.Condition `json:"conditions,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +versionName=v2
// CronJob is the Schema for the cronjobs API
type CronJob struct {
metav1.TypeMeta `json:",inline"`
// metadata 是标准对象元数据
// +optional
metav1.ObjectMeta `json:"metadata,omitzero"`
// spec 定义 CronJob 的期望状态
// +required
Spec CronJobSpec `json:"spec"`
// status 定义 CronJob 的观测状态
// +optional
Status CronJobStatus `json:"status,omitzero"`
}
// +kubebuilder:object:root=true
// CronJobList 包含一个 CronJob 列表
type CronJobList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitzero"`
Items []CronJob `json:"items"`
}
func init() {
SchemeBuilder.Register(func(s *runtime.Scheme) error {
s.AddKnownTypes(SchemeGroupVersion, &CronJob{}, &CronJobList{})
return nil
})
}
存储版本
Apache License
Copyright 2026 The Kubernetes authors.
Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
package v1
Imports
import (
batchv1 "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
// 请编辑本文件!这是供您拥有的脚手架代码!
// 注意:需要 json 标签。新增字段必须带有 json 标签才能被序列化。
Remaining code from cronjob_types.go
// CronJobSpec 定义 CronJob 的期望状态
type CronJobSpec struct {
// Cron 格式的调度表达式,参见 https://en.wikipedia.org/wiki/Cron.
// +kubebuilder:validation:MinLength=0
// +required
Schedule string `json:"schedule"`
// startingDeadlineSeconds 定义若错过计划时间则启动 Job 的秒数截止
// time for any reason. Missed jobs executions will be counted as failed ones.
// +optional
// +kubebuilder:validation:Minimum=0
StartingDeadlineSeconds *int64 `json:"startingDeadlineSeconds,omitempty"`
// concurrencyPolicy 指定如何处理 Job 的并发执行。
// 有效值为:
// - "Allow"(默认):允许 CronJob 并发运行;
// - "Forbid":禁止并发,若上次运行未结束则跳过下次;
// - "Replace":取消当前运行的 Job 并用新 Job 替换
// +optional
// +kubebuilder:default:=Allow
ConcurrencyPolicy ConcurrencyPolicy `json:"concurrencyPolicy,omitempty"`
// suspend 告诉控制器暂停后续执行,它
// 不适用于已开始的执行。默认为 false。
// +optional
Suspend *bool `json:"suspend,omitempty"`
// jobTemplate 定义执行 CronJob 时将创建的 Job。
// +required
JobTemplate batchv1.JobTemplateSpec `json:"jobTemplate"`
// successfulJobsHistoryLimit 定义要保留的成功完成 Job 数量。
// 使用指针以区分显式零值与未指定。
// +optional
// +kubebuilder:validation:Minimum=0
SuccessfulJobsHistoryLimit *int32 `json:"successfulJobsHistoryLimit,omitempty"`
// failedJobsHistoryLimit 定义要保留的失败完成 Job 数量。
// 使用指针以区分显式零值与未指定。
// +optional
// +kubebuilder:validation:Minimum=0
FailedJobsHistoryLimit *int32 `json:"failedJobsHistoryLimit,omitempty"`
}
// ConcurrencyPolicy 描述如何处理 Job。
// 下列并发策略只能指定其一。
// 若未指定下列任一策略,则默认
// 为 AllowConcurrent。
// +kubebuilder:validation:Enum=Allow;Forbid;Replace
type ConcurrencyPolicy string
const (
// AllowConcurrent 允许 CronJob 并发运行。
AllowConcurrent ConcurrencyPolicy = "Allow"
// ForbidConcurrent 禁止并发运行;若上次
// 尚未结束则跳过下次运行。
ForbidConcurrent ConcurrencyPolicy = "Forbid"
// ReplaceConcurrent 取消当前运行的 Job 并用新 Job 替换。
ReplaceConcurrent ConcurrencyPolicy = "Replace"
)
// CronJobStatus 定义 CronJob 的观测状态.
type CronJobStatus struct {
// 插入额外的状态字段 - 定义集群的观察状态
// 重要:在修改此文件后,请运行 "make" 以重新生成代码
// active 定义指向当前运行 Job 的指针列表。
// +optional
// +listType=atomic
// +kubebuilder:validation:MinItems=1
// +kubebuilder:validation:MaxItems=10
Active []corev1.ObjectReference `json:"active,omitempty"`
// lastScheduleTime 定义 Job 上次成功调度的时间。
// +optional
LastScheduleTime *metav1.Time `json:"lastScheduleTime,omitempty"`
// Kubernetes API 约定参见:
// https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#typical-status-properties
// conditions 表示 CronJob 资源的当前状态。
// 每个 condition 有唯一类型,反映资源某一方面的状态。
//
// Standard condition types include:
// - "Available": the resource is fully functional
// - "Progressing": the resource is being created or updated
// - "Degraded": the resource failed to reach or maintain its desired state
//
// The status of each condition is one of True, False, or Unknown.
// +listType=map
// +listMapKey=type
// +optional
Conditions []metav1.Condition `json:"conditions,omitempty"`
}
由于将有多个版本,我们需要标记一个存储版本。这是 Kubernetes API 服务器用来存储数据的版本。本项目选择 v1 版本。
我们将使用 +kubebuilder:storageversion 来完成此事。
注意:若对象在存储版本变更前已写入,存储中可能仍存在多个版本——更改存储版本仅影响变更之后创建/更新对象的方式。
// +kubebuilder:object:root=true
// +kubebuilder:storageversion
// +kubebuilder:subresource:status
// +versionName=v1
// +kubebuilder:storageversion
// CronJob is the Schema for the cronjobs API
type CronJob struct {
metav1.TypeMeta `json:",inline"`
// metadata 是标准对象元数据
// +optional
metav1.ObjectMeta `json:"metadata,omitzero"`
// spec 定义 CronJob 的期望状态
// +required
Spec CronJobSpec `json:"spec"`
// status 定义 CronJob 的观测状态
// +optional
Status CronJobStatus `json:"status,omitzero"`
}
Remaining code from cronjob_types.go
// +kubebuilder:object:root=true
// CronJobList 包含一个 CronJob 列表
type CronJobList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitzero"`
Items []CronJob `json:"items"`
}
func init() {
SchemeBuilder.Register(func(s *runtime.Scheme) error {
s.AddKnownTypes(SchemeGroupVersion, &CronJob{}, &CronJobList{})
return nil
})
}
现在我们已经确定了类型,我们需要设置转换…
中心、辐条以及其他轮子隐喻
由于我们现在有两个不同的版本,并且用户可以请求任一版本,我们需要定义一种在我们的版本之间转换的方法。对于 CRD(自定义资源定义),这可以通过 webhook 实现,类似于我们在基本教程中定义的默认和验证 webhook。和之前一样,controller-runtime 将帮助我们将这些细节连接在一起,我们只需要实现实际的转换。
在我们进行之前,我们需要了解controller-runtime如何看待版本。具体而言:
完全图在航海方面不够充分。
定义转换的一个简单方法可能是定义转换函数,以在我们每个版本之间进行转换。然后,每当我们需要转换时,就查找适当的函数,并调用它来执行转换。
当我们只有两个版本时,这很好,但如果我们有 4 种类型呢?8 种类型呢?那将需要很多转换函数。
相反,controller-runtime 将转换建模为“中心与辐射“模型——我们将一个版本标记为“中心“,所有其他版本只定义与中心之间的转换:
然后,如果我们需要在两个非中心版本之间进行转换,我们首先转换为中心版本,然后再转换为我们所需的版本:
这减少了我们需要定义的转换函数的数量,并且是基于 Kubernetes 在内部的做法。
这与 Webhook 有什么关系?
当 API 客户端,如 kubectl 或者你的控制器,请求某个特定版本的资源时,Kubernetes API 服务器需要返回该版本的结果。然而,该版本可能与 API 服务器存储的版本不匹配。
在这种情况下,API 服务器需要知道如何在所需版本和存储版本之间进行转换。由于 CRD 并没有内置转换功能,Kubernetes API 服务器会调用一个 webhook 来执行转换。对于 Kubebuilder,这个 webhook 是由 controller-runtime 实现的,执行我们上面讨论的中心-辐射转换。
现在我们已经掌握了转换模型,我们可以实际执行我们的转换。
实施转换
转换模型就绪后,就可以实际实现转换函数了。为 CronJob API 版本 v1(Hub)到 Spoke 的 CronJob API 版本 v2 创建转换 Webhook,参见:
kubebuilder create webhook --group batch --version v1 --kind CronJob --conversion --spoke v2
上述命令将生成 cronjob_conversion.go 文件与我们的 cronjob_types.go 文件并排,以避免在主类型文件中混入额外的函数。
中心…
首先,我们将实现中心。我们将选择 v1 版本作为中心:
Apache License
Copyright 2026 The Kubernetes authors.
Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
package v1
实现 hub 方法相当简单——只需添加一个空方法 Hub() 作为 标记。也可以直接内联写在 cronjob_types.go 文件中。
// Hub marks this type as a conversion hub.
func (*CronJob) Hub() {}
… 和发言人
然后,我们将实现我们的分支,v2 版本:
Apache License
Copyright 2026 The Kubernetes authors.
Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
package v2
Imports
对于 import,我们需要 controller-runtime conversion 包,以及 hub 类型的 API 版本(v1),还有一些标准包。
import (
"fmt"
"strings"
"log"
"sigs.k8s.io/controller-runtime/pkg/conversion"
batchv1 "tutorial.kubebuilder.io/project/api/v1"
)
我们的「spoke」版本需要实现 Convertible 接口。即需要 ConvertTo() 与 ConvertFrom() 方法,以便与 hub 版本相互转换。
ConvertTo 应修改其参数以包含转换后的对象。除转换我们变更过的字段外,大部分转换只是直接复制。
// ConvertTo converts this CronJob (v2) to the Hub version (v1).
func (src *CronJob) ConvertTo(dstRaw conversion.Hub) error {
dst := dstRaw.(*batchv1.CronJob)
log.Printf("ConvertTo: Converting CronJob from Spoke version v2 to Hub version v1;"+
"source: %s/%s, target: %s/%s", src.Namespace, src.Name, dst.Namespace, dst.Name)
sched := src.Spec.Schedule
scheduleParts := []string{"*", "*", "*", "*", "*"}
if sched.Minute != nil {
scheduleParts[0] = string(*sched.Minute)
}
if sched.Hour != nil {
scheduleParts[1] = string(*sched.Hour)
}
if sched.DayOfMonth != nil {
scheduleParts[2] = string(*sched.DayOfMonth)
}
if sched.Month != nil {
scheduleParts[3] = string(*sched.Month)
}
if sched.DayOfWeek != nil {
scheduleParts[4] = string(*sched.DayOfWeek)
}
dst.Spec.Schedule = strings.Join(scheduleParts, " ")
rote conversion
其余转换工作相当机械。
// ObjectMeta
dst.ObjectMeta = src.ObjectMeta
// Spec
dst.Spec.StartingDeadlineSeconds = src.Spec.StartingDeadlineSeconds
dst.Spec.ConcurrencyPolicy = batchv1.ConcurrencyPolicy(src.Spec.ConcurrencyPolicy)
dst.Spec.Suspend = src.Spec.Suspend
dst.Spec.JobTemplate = src.Spec.JobTemplate
dst.Spec.SuccessfulJobsHistoryLimit = src.Spec.SuccessfulJobsHistoryLimit
dst.Spec.FailedJobsHistoryLimit = src.Spec.FailedJobsHistoryLimit
// Status
dst.Status.Active = src.Status.Active
dst.Status.LastScheduleTime = src.Status.LastScheduleTime
return nil
}
ConvertFrom 应修改其接收者以包含转换后的对象。除转换我们变更过的字段外,大部分转换只是直接复制。
// ConvertFrom converts the Hub version (v1) to this CronJob (v2).
func (dst *CronJob) ConvertFrom(srcRaw conversion.Hub) error {
src := srcRaw.(*batchv1.CronJob)
log.Printf("ConvertFrom: Converting CronJob from Hub version v1 to Spoke version v2;"+
"source: %s/%s, target: %s/%s", src.Namespace, src.Name, dst.Namespace, dst.Name)
schedParts := strings.Split(src.Spec.Schedule, " ")
if len(schedParts) != 5 {
return fmt.Errorf("invalid schedule: not a standard 5-field schedule")
}
partIfNeeded := func(raw string) *CronField {
if raw == "*" {
return nil
}
part := CronField(raw)
return &part
}
dst.Spec.Schedule.Minute = partIfNeeded(schedParts[0])
dst.Spec.Schedule.Hour = partIfNeeded(schedParts[1])
dst.Spec.Schedule.DayOfMonth = partIfNeeded(schedParts[2])
dst.Spec.Schedule.Month = partIfNeeded(schedParts[3])
dst.Spec.Schedule.DayOfWeek = partIfNeeded(schedParts[4])
rote conversion
其余转换工作相当机械。
// ObjectMeta
dst.ObjectMeta = src.ObjectMeta
// Spec
dst.Spec.StartingDeadlineSeconds = src.Spec.StartingDeadlineSeconds
dst.Spec.ConcurrencyPolicy = ConcurrencyPolicy(src.Spec.ConcurrencyPolicy)
dst.Spec.Suspend = src.Spec.Suspend
dst.Spec.JobTemplate = src.Spec.JobTemplate
dst.Spec.SuccessfulJobsHistoryLimit = src.Spec.SuccessfulJobsHistoryLimit
dst.Spec.FailedJobsHistoryLimit = src.Spec.FailedJobsHistoryLimit
// Status
dst.Status.Active = src.Status.Active
dst.Status.LastScheduleTime = src.Status.LastScheduleTime
return nil
}
现在我们已经设置好了转换,我们需要做的就是将主程序连接起来,以便提供 webhook!
设置网络hooks
我们的转换已经到位,因此剩下的就是告诉 controller-runtime 我们的转换。
v1 的 Webhook 设置
v1 Webhook 处理转换(作为 hub),并为基于字符串 schedule 的 v1 CronJob 格式提供校验/默认值:
Apache License
Copyright 2026 The Kubernetes authors.
Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
Imports
package v1
import (
"context"
"github.com/robfig/cron"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime/schema"
validationutils "k8s.io/apimachinery/pkg/util/validation"
"k8s.io/apimachinery/pkg/util/validation/field"
ctrl "sigs.k8s.io/controller-runtime"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
batchv1 "tutorial.kubebuilder.io/project/api/v1"
)
接下来,我们为 Webhook 设置日志器。
var cronjoblog = logf.Log.WithName("cronjob-resource")
此设置同时充当转换 Webhook 的设置:只要我们的类型实现了 Hub 与 Convertible 接口,就会注册转换 Webhook。
// SetupCronJobWebhookWithManager registers the webhook for CronJob in the manager.
func SetupCronJobWebhookWithManager(mgr ctrl.Manager) error {
return ctrl.NewWebhookManagedBy(mgr, &batchv1.CronJob{}).
WithValidator(&CronJobValidator{}).
WithDefaulter(&CronJobDefaulter{
DefaultConcurrencyPolicy: batchv1.AllowConcurrent,
DefaultSuspend: false,
DefaultSuccessfulJobsHistoryLimit: 3,
DefaultFailedJobsHistoryLimit: 1,
}).
Complete()
}
注意我们使用 kubebuilder 标记生成 Webhook 清单。此标记负责生成变更 Webhook 清单。
各标记的含义见 此处。
此标记负责生成变更 Webhook 清单。
// +kubebuilder:webhook:path=/mutate-batch-tutorial-kubebuilder-io-v1-cronjob,mutating=true,failurePolicy=fail,sideEffects=None,groups=batch.tutorial.kubebuilder.io,resources=cronjobs,verbs=create;update,versions=v1,name=mcronjob-v1.kb.io,admissionReviewVersions=v1
// CronJobDefaulter struct is responsible for setting default values on the custom resource of the
// Kind CronJob when those are created or updated.
//
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
// as it is used only for temporary operations and does not need to be deeply copied.
type CronJobDefaulter struct {
// Default values for various CronJob fields
DefaultConcurrencyPolicy batchv1.ConcurrencyPolicy
DefaultSuspend bool
DefaultSuccessfulJobsHistoryLimit int32
DefaultFailedJobsHistoryLimit int32
}
我们使用 admission.Defaulter 接口为 CRD 设置默认值。将自动提供调用此默认值逻辑的 Webhook。
Default 方法应变更接收者并设置默认值。
// Default implements admission.Defaulter so a webhook will be registered for the Kind CronJob.
func (d *CronJobDefaulter) Default(_ context.Context, obj *batchv1.CronJob) error {
cronjoblog.Info("Defaulting for CronJob", "name", obj.GetName())
// Set default values
d.applyDefaults(obj)
return nil
}
// applyDefaults applies default values to CronJob fields.
func (d *CronJobDefaulter) applyDefaults(cronJob *batchv1.CronJob) {
if cronJob.Spec.ConcurrencyPolicy == "" {
cronJob.Spec.ConcurrencyPolicy = d.DefaultConcurrencyPolicy
}
if cronJob.Spec.Suspend == nil {
cronJob.Spec.Suspend = new(bool)
*cronJob.Spec.Suspend = d.DefaultSuspend
}
if cronJob.Spec.SuccessfulJobsHistoryLimit == nil {
cronJob.Spec.SuccessfulJobsHistoryLimit = new(int32)
*cronJob.Spec.SuccessfulJobsHistoryLimit = d.DefaultSuccessfulJobsHistoryLimit
}
if cronJob.Spec.FailedJobsHistoryLimit == nil {
cronJob.Spec.FailedJobsHistoryLimit = new(int32)
*cronJob.Spec.FailedJobsHistoryLimit = d.DefaultFailedJobsHistoryLimit
}
}
我们可以用超出声明式校验能力的方式校验 CRD。通常声明式校验已足够,但有时更高级的用例需要复杂校验。
例如,我们将在下面看到,我们用它校验格式正确的 cron schedule,而无需编写很长的正则表达式。
若实现了 admission.Validator 接口,将自动提供调用该校验的 Webhook。
ValidateCreate、ValidateUpdate 与 ValidateDelete 方法应分别在创建、更新与删除时校验其接收者。我们将 ValidateCreate 与 ValidateUpdate 分开,以允许诸如使某些字段不可变、仅能在创建时设置之类的行为。ValidateDelete 也与 ValidateUpdate 分开,以允许删除时不同的校验行为。但在这里,我们对 ValidateCreate 与 ValidateUpdate 使用相同的共享校验。而在 ValidateDelete 中什么也不做,因为删除时无需校验任何内容。
Remaining Webhook Code
此标记负责生成校验 Webhook 清单。
// NOTE: If you want to customise the 'path', use the flags '--defaulting-path' or '--validation-path'.
// +kubebuilder:webhook:path=/validate-batch-tutorial-kubebuilder-io-v1-cronjob,mutating=false,failurePolicy=fail,sideEffects=None,groups=batch.tutorial.kubebuilder.io,resources=cronjobs,verbs=create;update,versions=v1,name=vcronjob-v1.kb.io,admissionReviewVersions=v1
// CronJobValidator struct is responsible for validating the CronJob resource
// when it is created, updated, or deleted.
//
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
// as this struct is used only for temporary operations and does not need to be deeply copied.
type CronJobValidator struct {
// TODO(user): Add more fields as needed for validation
}
// ValidateCreate implements admission.Validator so a webhook will be registered for the type CronJob.
func (v *CronJobValidator) ValidateCreate(_ context.Context, obj *batchv1.CronJob) (admission.Warnings, error) {
cronjoblog.Info("Validation for CronJob upon creation", "name", obj.GetName())
return nil, validateCronJob(obj)
}
// ValidateUpdate implements admission.Validator so a webhook will be registered for the type CronJob.
func (v *CronJobValidator) ValidateUpdate(_ context.Context, oldObj, newObj *batchv1.CronJob) (admission.Warnings, error) {
cronjoblog.Info("Validation for CronJob upon update", "name", newObj.GetName())
return nil, validateCronJob(newObj)
}
// ValidateDelete implements admission.Validator so a webhook will be registered for the type CronJob.
func (v *CronJobValidator) ValidateDelete(_ context.Context, obj *batchv1.CronJob) (admission.Warnings, error) {
cronjoblog.Info("Validation for CronJob upon deletion", "name", obj.GetName())
// TODO(user): fill in your validation logic upon object deletion.
return nil, nil
}
我们校验 CronJob 的名称与 spec。
// validateCronJob validates the fields of a CronJob object.
func validateCronJob(cronjob *batchv1.CronJob) error {
var allErrs field.ErrorList
if err := validateCronJobName(cronjob); err != nil {
allErrs = append(allErrs, err)
}
if err := validateCronJobSpec(cronjob); err != nil {
allErrs = append(allErrs, err)
}
if len(allErrs) == 0 {
return nil
}
return apierrors.NewInvalid(
schema.GroupKind{Group: "batch.tutorial.kubebuilder.io", Kind: "CronJob"},
cronjob.Name, allErrs)
}
一些字段由 OpenAPI schema 声明式校验。可在 设计 API 一节找到 kubebuilder 校验标记(前缀为 // +kubebuilder:validation)。运行 controller-gen crd -w 可查看所有支持声明校验的标记,或见 此处。
func validateCronJobSpec(cronjob *batchv1.CronJob) *field.Error {
// The field helpers from the kubernetes API machinery help us return nicely
// structured validation errors.
return validateScheduleFormat(
cronjob.Spec.Schedule,
field.NewPath("spec").Child("schedule"))
}
我们需要校验 cron schedule 格式正确。
func validateScheduleFormat(schedule string, fldPath *field.Path) *field.Error {
if _, err := cron.ParseStandard(schedule); err != nil {
return field.Invalid(fldPath, schedule, err.Error())
}
return nil
}
字符串字段长度的校验可由校验 schema 以声明式完成。
但 ObjectMeta.Name 字段定义在 apimachinery 仓库的共享包中,因此无法使用校验 schema 对其进行声明式校验。
func validateCronJobName(cronjob *batchv1.CronJob) *field.Error {
if len(cronjob.Name) > validationutils.DNS1035LabelMaxLength-11 {
// The job name length is 63 characters like all Kubernetes objects
// (which must fit in a DNS subdomain). The cronjob controller appends
// a 11-character suffix to the cronjob (`-$TIMESTAMP`) when creating
// a job. The job name length limit is 63 characters. Therefore cronjob
// names must have length <= 63-11=52. If we don't validate this here,
// then job creation will fail later.
return field.Invalid(field.NewPath("metadata").Child("name"), cronjob.Name, "must be no more than 52 characters")
}
return nil
}
v2 的 Webhook 设置
v2 Webhook 为带有结构化 CronSchedule 类型的 v2 CronJob 格式提供校验与默认值。注意校验逻辑与 v1 不同——它从各个 schedule 字段构建 cron 表达式:
Apache License
Copyright 2026 The Kubernetes authors.
Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
Imports
package v2
import (
"context"
"strings"
"github.com/robfig/cron"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime/schema"
validationutils "k8s.io/apimachinery/pkg/util/validation"
"k8s.io/apimachinery/pkg/util/validation/field"
ctrl "sigs.k8s.io/controller-runtime"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
batchv2 "tutorial.kubebuilder.io/project/api/v2"
)
Webhook Setup and Defaulting
// nolint:unused
// log is for logging in this package.
var cronjoblog = logf.Log.WithName("cronjob-resource")
// SetupCronJobWebhookWithManager registers the webhook for CronJob in the manager.
func SetupCronJobWebhookWithManager(mgr ctrl.Manager) error {
return ctrl.NewWebhookManagedBy(mgr, &batchv2.CronJob{}).
WithValidator(&CronJobValidator{}).
WithDefaulter(&CronJobDefaulter{
DefaultConcurrencyPolicy: batchv2.AllowConcurrent,
DefaultSuspend: false,
DefaultSuccessfulJobsHistoryLimit: 3,
DefaultFailedJobsHistoryLimit: 1,
}).
Complete()
}
// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
// +kubebuilder:webhook:path=/mutate-batch-tutorial-kubebuilder-io-v2-cronjob,mutating=true,failurePolicy=fail,sideEffects=None,groups=batch.tutorial.kubebuilder.io,resources=cronjobs,verbs=create;update,versions=v2,name=mcronjob-v2.kb.io,admissionReviewVersions=v1
// CronJobDefaulter struct is responsible for setting default values on the custom resource of the
// Kind CronJob when those are created or updated.
//
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
// as it is used only for temporary operations and does not need to be deeply copied.
type CronJobDefaulter struct {
// Default values for various CronJob fields
DefaultConcurrencyPolicy batchv2.ConcurrencyPolicy
DefaultSuspend bool
DefaultSuccessfulJobsHistoryLimit int32
DefaultFailedJobsHistoryLimit int32
}
// Default implements admission.Defaulter so a webhook will be registered for the Kind CronJob.
func (d *CronJobDefaulter) Default(_ context.Context, obj *batchv2.CronJob) error {
cronjoblog.Info("Defaulting for CronJob", "name", obj.GetName())
// Set default values
d.applyDefaults(obj)
return nil
}
// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.
// NOTE: If you want to customise the 'path', use the flags '--defaulting-path' or '--validation-path'.
// +kubebuilder:webhook:path=/validate-batch-tutorial-kubebuilder-io-v2-cronjob,mutating=false,failurePolicy=fail,sideEffects=None,groups=batch.tutorial.kubebuilder.io,resources=cronjobs,verbs=create;update,versions=v2,name=vcronjob-v2.kb.io,admissionReviewVersions=v1
// CronJobValidator struct is responsible for validating the CronJob resource
// when it is created, updated, or deleted.
//
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
// as this struct is used only for temporary operations and does not need to be deeply copied.
type CronJobValidator struct {
// TODO(user): Add more fields as needed for validation
}
// ValidateCreate implements admission.Validator so a webhook will be registered for the type CronJob.
func (v *CronJobValidator) ValidateCreate(_ context.Context, obj *batchv2.CronJob) (admission.Warnings, error) {
cronjoblog.Info("Validation for CronJob upon creation", "name", obj.GetName())
return nil, validateCronJob(obj)
}
// ValidateUpdate implements admission.Validator so a webhook will be registered for the type CronJob.
func (v *CronJobValidator) ValidateUpdate(_ context.Context, oldObj, newObj *batchv2.CronJob) (admission.Warnings, error) {
cronjoblog.Info("Validation for CronJob upon update", "name", newObj.GetName())
return nil, validateCronJob(newObj)
}
// ValidateDelete implements admission.Validator so a webhook will be registered for the type CronJob.
func (v *CronJobValidator) ValidateDelete(_ context.Context, obj *batchv2.CronJob) (admission.Warnings, error) {
cronjoblog.Info("Validation for CronJob upon deletion", "name", obj.GetName())
// TODO(user): fill in your validation logic upon object deletion.
return nil, nil
}
// applyDefaults applies default values to CronJob fields.
func (d *CronJobDefaulter) applyDefaults(cronJob *batchv2.CronJob) {
if cronJob.Spec.ConcurrencyPolicy == "" {
cronJob.Spec.ConcurrencyPolicy = d.DefaultConcurrencyPolicy
}
if cronJob.Spec.Suspend == nil {
cronJob.Spec.Suspend = new(bool)
*cronJob.Spec.Suspend = d.DefaultSuspend
}
if cronJob.Spec.SuccessfulJobsHistoryLimit == nil {
cronJob.Spec.SuccessfulJobsHistoryLimit = new(int32)
*cronJob.Spec.SuccessfulJobsHistoryLimit = d.DefaultSuccessfulJobsHistoryLimit
}
if cronJob.Spec.FailedJobsHistoryLimit == nil {
cronJob.Spec.FailedJobsHistoryLimit = new(int32)
*cronJob.Spec.FailedJobsHistoryLimit = d.DefaultFailedJobsHistoryLimit
}
}
// validateCronJob validates the fields of a CronJob object.
func validateCronJob(cronjob *batchv2.CronJob) error {
var allErrs field.ErrorList
if err := validateCronJobName(cronjob); err != nil {
allErrs = append(allErrs, err)
}
if err := validateCronJobSpec(cronjob); err != nil {
allErrs = append(allErrs, err)
}
if len(allErrs) == 0 {
return nil
}
return apierrors.NewInvalid(schema.GroupKind{Group: "batch.tutorial.kubebuilder.io", Kind: "CronJob"}, cronjob.Name, allErrs)
}
func validateCronJobName(cronjob *batchv2.CronJob) *field.Error {
if len(cronjob.Name) > validationutils.DNS1035LabelMaxLength-11 {
return field.Invalid(field.NewPath("metadata").Child("name"), cronjob.Name, "must be no more than 52 characters")
}
return nil
}
// validateCronJobSpec validates the schedule format of the custom CronSchedule type
func validateCronJobSpec(cronjob *batchv2.CronJob) *field.Error {
// Build cron expression from the parts
parts := []string{"*", "*", "*", "*", "*"} // default parts for minute, hour, day of month, month, day of week
if cronjob.Spec.Schedule.Minute != nil {
parts[0] = string(*cronjob.Spec.Schedule.Minute) // Directly cast CronField (which is an alias of string) to string
}
if cronjob.Spec.Schedule.Hour != nil {
parts[1] = string(*cronjob.Spec.Schedule.Hour)
}
if cronjob.Spec.Schedule.DayOfMonth != nil {
parts[2] = string(*cronjob.Spec.Schedule.DayOfMonth)
}
if cronjob.Spec.Schedule.Month != nil {
parts[3] = string(*cronjob.Spec.Schedule.Month)
}
if cronjob.Spec.Schedule.DayOfWeek != nil {
parts[4] = string(*cronjob.Spec.Schedule.DayOfWeek)
}
// Join parts to form the full cron expression
cronExpression := strings.Join(parts, " ")
return validateScheduleFormat(
cronExpression,
field.NewPath("spec").Child("schedule"))
}
func validateScheduleFormat(schedule string, fldPath *field.Path) *field.Error {
if _, err := cron.ParseStandard(schedule); err != nil {
return field.Invalid(fldPath, schedule, "invalid cron schedule format: "+err.Error())
}
return nil
}
…和 main.go
同样,我们现有的主文件是足够的:
Apache License
Copyright 2026 The Kubernetes authors.
Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
Imports
package main
import (
"crypto/tls"
"flag"
"os"
// 导入所有 Kubernetes 客户端身份验证插件(例如 Azure、GCP、OIDC 等)
// 确保 exec-entrypoint 和 run 可以使用它们。
_ "k8s.io/client-go/plugin/pkg/client/auth"
kbatchv1 "k8s.io/api/batch/v1"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/healthz"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
"sigs.k8s.io/controller-runtime/pkg/webhook"
batchv1 "tutorial.kubebuilder.io/project/api/v1"
batchv2 "tutorial.kubebuilder.io/project/api/v2"
"tutorial.kubebuilder.io/project/internal/controller"
webhookv1 "tutorial.kubebuilder.io/project/internal/webhook/v1"
webhookv2 "tutorial.kubebuilder.io/project/internal/webhook/v2"
// +kubebuilder:scaffold:imports
)
existing setup
var (
scheme = runtime.NewScheme()
setupLog = ctrl.Log.WithName("setup")
)
func init() {
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
utilruntime.Must(kbatchv1.AddToScheme(scheme)) // we've added this ourselves
utilruntime.Must(batchv1.AddToScheme(scheme))
utilruntime.Must(batchv2.AddToScheme(scheme))
// +kubebuilder:scaffold:scheme
}
// nolint:gocyclo
func main() {
Manager Setup
var metricsAddr string
var metricsCertPath, metricsCertName, metricsCertKey string
var webhookCertPath, webhookCertName, webhookCertKey string
var webhookPort int
var enableLeaderElection bool
var probeAddr string
var secureMetrics bool
var enableHTTP2 bool
var tlsOpts []func(*tls.Config)
flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+
"使用 :8443 进行 HTTPS,使用 :8080 进行 HTTP,或者保持为 0 以禁用指标服务。")
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
flag.BoolVar(&enableLeaderElection, "leader-elect", false,
"Enable leader election for controller manager. "+
"启用此功能将确保只有一个活动的控制管理器。")
flag.BoolVar(&secureMetrics, "metrics-secure", true,
"如果设置,指标端点将通过 HTTPS 安全提供。请使用 --metrics-secure=false 以改为使用 HTTP。")
flag.StringVar(&webhookCertPath, "webhook-cert-path", "", "The directory that contains the webhook certificate.")
flag.StringVar(&webhookCertName, "webhook-cert-name", "tls.crt", "The name of the webhook certificate file.")
flag.StringVar(&webhookCertKey, "webhook-cert-key", "tls.key", "The name of the webhook key file.")
flag.IntVar(&webhookPort, "webhook-port", 9443, "Port the webhook server listens on. "+
"Defaults to 9443. Set -1 to disable the webhook server.")
flag.StringVar(&metricsCertPath, "metrics-cert-path", "",
"The directory that contains the metrics server certificate.")
flag.StringVar(&metricsCertName, "metrics-cert-name", "tls.crt", "The name of the metrics server certificate file.")
flag.StringVar(&metricsCertKey, "metrics-cert-key", "tls.key", "The name of the metrics server key file.")
flag.BoolVar(&enableHTTP2, "enable-http2", false,
"If set, HTTP/2 will be enabled for the metrics and webhook servers")
opts := zap.Options{
Development: true,
}
opts.BindFlags(flag.CommandLine)
flag.Parse()
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
// 如果 enable-http2 标志为 false(默认值),则应禁用 http/2
// 由于其存在的漏洞。更具体地说,禁用 http/2 将
// 防止受到 HTTP/2 流取消和
// 快速重置 CVE 的漏洞影响。有关更多信息,请参见:
// - https://github.com/advisories/GHSA-qppj-fm5r-hxr3
// - https://github.com/advisories/GHSA-4374-p667-p6c8
disableHTTP2 := func(c *tls.Config) {
setupLog.Info("Disabling HTTP/2")
c.NextProtos = []string{"http/1.1"}
}
if !enableHTTP2 {
tlsOpts = append(tlsOpts, disableHTTP2)
}
// 初始Webhook TLS选项
webhookTLSOpts := tlsOpts
webhookServerOptions := webhook.Options{
TLSOpts: webhookTLSOpts,
Port: webhookPort,
}
if len(webhookCertPath) > 0 {
setupLog.Info("使用提供的证书初始化 Webhook 证书监视器",
"webhook-cert-path", webhookCertPath, "webhook-cert-name", webhookCertName, "webhook-cert-key", webhookCertKey)
webhookServerOptions.CertDir = webhookCertPath
webhookServerOptions.CertName = webhookCertName
webhookServerOptions.KeyName = webhookCertKey
}
webhookServer := webhook.NewServer(webhookServerOptions)
// Metrics 端点已在 'config/default/kustomization.yaml'. Metrics 选项用于配置服务器。
// 更多信息:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.25.0/pkg/metrics/server
// - https://book.kubebuilder.io/reference/metrics.html
metricsServerOptions := metricsserver.Options{
BindAddress: metricsAddr,
SecureServing: secureMetrics,
TLSOpts: tlsOpts,
}
if secureMetrics {
// FilterProvider 用于通过认证/鉴权保护 metrics 端点。
// These configurations ensure that only authorized users and service accounts
// can access the metrics endpoint. The RBAC are configured in 'config/rbac/kustomization.yaml'. More info:
// https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.25.0/pkg/metrics/filters#WithAuthenticationAndAuthorization
metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization
}
// 如果未指定证书,controller-runtime 将自动
// 为指标服务器生成自签名证书。虽然对于开发和测试来说很方便,
// 但这种设置不建议用于生产环境。
//
// TODO(用户):如果您启用 certManager,请取消注释以下行:
// - [METRICS-WITH-CERTS] 在 config/default/kustomization.yaml 中生成并使用
// 由 cert-manager 管理的指标服务器证书。
// - [PROMETHEUS-WITH-CERTS] 在 config/prometheus/kustomization.yaml 中用于 TLS 认证。
if len(metricsCertPath) > 0 {
setupLog.Info("使用提供的证书初始化指标证书监视器",
"metrics-cert-path", metricsCertPath, "metrics-cert-name", metricsCertName, "metrics-cert-key", metricsCertKey)
metricsServerOptions.CertDir = metricsCertPath
metricsServerOptions.CertName = metricsCertName
metricsServerOptions.KeyName = metricsCertKey
}
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
Metrics: metricsServerOptions,
WebhookServer: webhookServer,
HealthProbeBindAddress: probeAddr,
LeaderElection: enableLeaderElection,
LeaderElectionID: "80807133.tutorial.kubebuilder.io",
// LeaderElectionReleaseOnCancel 定义了当管理器结束时,领导者是否应自愿辞职
// 当管理器停止时,这要求二进制文件立即结束,否则这个设置是不安全的。设置这个可以显著
// 加快自愿领导者的过渡,因为新的领导者不必首先等待
// LeaseDuration 时间。
//
// 在提供的默认脚手架中,程序会在
// 管理器停止后立即结束,因此启用此选项是可以的。不过,
// 如果您正在进行或打算在管理器停止后执行任何操作,如执行清理,
// 那么使用它可能是不安全的。
// LeaderElectionReleaseOnCancel: true,
})
if err != nil {
setupLog.Error(err, "Failed to start manager")
os.Exit(1)
}
if err := (&controller.CronJobReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "Failed to create controller", "controller", "cronjob")
os.Exit(1)
}
现有的 SetupWebhookWithManager 调用也会向 manager 注册我们的转换 Webhook。
// nolint:goconst
if os.Getenv("ENABLE_WEBHOOKS") != "false" {
if err := webhookv1.SetupCronJobWebhookWithManager(mgr); err != nil {
setupLog.Error(err, "Failed to create webhook", "webhook", "CronJob")
os.Exit(1)
}
}
// nolint:goconst
if os.Getenv("ENABLE_WEBHOOKS") != "false" {
if err := webhookv2.SetupCronJobWebhookWithManager(mgr); err != nil {
setupLog.Error(err, "Failed to create webhook", "webhook", "CronJob")
os.Exit(1)
}
}
// +kubebuilder:scaffold:builder
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
setupLog.Error(err, "Failed to set up health check")
os.Exit(1)
}
if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
setupLog.Error(err, "Failed to set up ready check")
os.Exit(1)
}
setupLog.Info("Starting manager")
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
setupLog.Error(err, "Failed to run manager")
os.Exit(1)
}
}
一切都已经准备好!现在剩下的就是测试我们的网络钩子了。
部署与测试
在我们测试转换之前,我们需要在我们的 CRD 中启用它们:
Kubebuilder 在 config 目录下生成 Kubernetes 清单,默认情况下禁用了 webhook 部分。要启用它们,我们需要:
-
在
config/crd/kustomization.yaml文件中启用patches/webhook_in_<kind>.yaml和patches/cainjection_in_<kind>.yaml。 -
在
config/default/kustomization.yaml文件的bases部分启用../certmanager和../webhook目录。 -
启用
config/default/kustomization.yaml文件中CERTMANAGER部分下的所有变量。
另外,如果在我们的 Makefile 中存在,我们需要将 CRD_OPTIONS 变量设置为 "crd",移除 trivialVersions 选项(这确保我们实际上为每个版本[生成验证](/reference/generating-crd.md#multiple-versions “生成CRDs:多个版本”),而不是告诉 Kubernetes 它们是相同的):
CRD_OPTIONS ?= "crd"
现在我们已经完成了所有的代码更改和清单,接下来将其部署到集群并进行测试。
除非你有其他证书管理解决方案,否则你需要安装 cert-manager(版本 0.9.0+)。Kubebuilder 团队已经在 0.9.0-alpha.0 版本中测试了本教程中的指令。
一旦我们的证书准备齐全,我们可以像往常一样运行 make install deploy 来将所有组件(CRD 和控制器管理器部署)部署到集群上。
测试
一旦集群中所有位都启动并运行并启用了转换功能,我们就可以通过请求不同版本来测试我们的转换。
基于 v1 版本制作 v2 版本(放在 config/samples 下)
apiVersion: batch.tutorial.kubebuilder.io/v2
kind: CronJob
metadata:
labels:
app.kubernetes.io/name: project
app.kubernetes.io/managed-by: kustomize
name: cron作业示例
spec:
日程安排:
分钟: "*/1"
启动截止时间(秒): 60
并发策略: 允许 # 显式指定,但允许也是默认值。
工作模板:
spec:
template:
spec:
安全上下文:
以非root用户身份运行: true
以用户身份运行: 1000
seccompProfile:
type: RuntimeDefault
容器:
- name: 你好
image: busybox
args:
- /bin/sh
- -c
- 日期; 在Kubernetes集群中打招呼。
安全上下文:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: false
重启策略: 失败时
然后,我们可以在集群上创建它:
kubectl apply -f config/samples/batch_v2_cronjob.yaml
如果我们一切都做对了,它应该会成功创建,我们应该能够通过 v2 资源来获取它。
kubectl get cronjobs.v2.batch.tutorial.kubebuilder.io -o yaml
apiVersion: batch.tutorial.kubebuilder.io/v2
kind: CronJob
metadata:
labels:
app.kubernetes.io/name: project
app.kubernetes.io/managed-by: kustomize
name: cron作业示例
spec:
日程安排:
分钟: "*/1"
启动截止时间(秒): 60
并发策略: 允许 # 显式指定,但允许也是默认值。
工作模板:
spec:
template:
spec:
安全上下文:
以非root用户身份运行: true
以用户身份运行: 1000
seccompProfile:
type: RuntimeDefault
容器:
- name: 你好
image: busybox
args:
- /bin/sh
- -c
- 日期; 在Kubernetes集群中打招呼。
安全上下文:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: false
重启策略: 失败时
和 v1 资源
kubectl get cronjobs.v1.batch.tutorial.kubebuilder.io -o yaml
apiVersion: batch.tutorial.kubebuilder.io/v1
kind: CronJob
metadata:
labels:
app.kubernetes.io/name: project
app.kubernetes.io/managed-by: kustomize
name: cron作业示例
spec:
日程安排: "*/1 * * * *"
启动截止时间(秒): 60
并发策略: 允许 # 显式指定,但允许也是默认值。
工作模板:
spec:
template:
spec:
安全上下文:
以非root用户身份运行: true
以用户身份运行: 1000
seccompProfile:
type: RuntimeDefault
容器:
- name: 你好
image: busybox
args:
- /bin/sh
- -c
- 日期; 在Kubernetes集群中打招呼。
安全上下文:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: false
重启策略: 失败时
两者都应该填写,并且看起来与我们的 v2 和 v1 示例各自相当。请注意,每个都有不同的 API 版本。
最后,如果我们等一会儿,我们应该注意到我们的 CronJob 继续进行调整,尽管我们的控制器是针对我们的 v1 API 版本编写的。
迁移
将 Kubebuilder 项目升级到最新版本,可获得新功能、缺陷修复与生态改进。建议让项目与生态变更保持同步。
迁移可能包括升级到更新的插件版本(例如从 3.x 发行版的 go.kubebuilder.io/v3 到 4.x 的 go.kubebuilder.io/v4),或在同一插件下跨 CLI 发行版更新脚手架(例如从 v4.9.0 到 v4.10.1)。
Kubebuilder 提供多种迁移路径以适应不同工作流。请选择最适合您的方式。
迁移选项
[!TIP] 推荐: 启用 AutoUpdate 插件(GitHub Actions)以减少工作量。也可在本地运行 alpha update——两者使用相同的更新逻辑。其他选项主要用于
PROJECT文件中尚无cliVersion的旧项目,作为一次性步骤升级到受支持版本;之后再用上述工作流进行后续更新(更旧版本无法使用这些自动化功能)。
(推荐) AutoUpdate/GitHub Action:通过带有 PR 链接的 Issue 获知新的 Kubebuilder 发行版,以便审阅与升级
AutoUpdate 插件 会脚手架一个 Action:自动监控新的 Kubebuilder 发行版,并在有更新时打开带有 Pull Request 对比链接的 GitHub Issue。适合以最少人工工作保持项目最新。
该插件提供类似 GitHub Dependabot 的机制,为遵循标准脚手架的项目提供带 AI 辅助的持续更新。
kubebuilder edit --plugins="autoupdate/v1-alpha"
完整说明见 AutoUpdate 插件文档。
(推荐) 使用 alpha update 升级且不丢失自定义内容(即 AutoUpdate/GitHub Action 背后的逻辑)
若希望在本地运行更新而不依赖 GitHub Actions,可在命令行直接使用与 AutoUpdate 插件 相同的逻辑。
kubebuilder alpha update
该命令与 AutoUpdate 插件使用相同底层机制。您可以迁移项目、按需解决冲突,然后从本地环境推送 Pull Request。
全部选项与标志见 alpha update 命令参考。
借助工具重新生成并手动合并
kubebuilder alpha generate 会根据 PROJECT 文件配置重新脚手架整个项目,之后可手动对比并合并自定义代码。例如,可在升级 Kubebuilder CLI 后用它重新生成项目,再用 IDE 或 git diff 手动将变更合并回现有代码库,确保自定义内容落在新脚手架上。
该方法适用于大量自定义脚手架、或其他迁移方式不可用的项目。通常只需用一次以建立基线,供后续自动化更新使用。
kubebuilder alpha generate
完全手动迁移
若需要完全掌控,可手动迁移:用最新 Kubebuilder 创建新项目,再把代码移植过去。
该过程中,需从头运行全部命令创建新项目、API、控制器、Webhook 及其他资源,再把旧项目中的业务逻辑与自定义内容手动复制到新项目。
为简化这一次性迁移,已提供 AI 迁移助手 以自动化重复任务。
完整分步说明(含 AI 助手)见手动迁移流程指南。
手动迁移流程
请确保你已按照安装指南安装所需组件,并且所需版本的 Kubebuilder CLI 已位于你的 PATH 中。
本指南概述了将现有 Kubebuilder 项目迁移到较新版本 Kubebuilder 框架的手动步骤。此过程包括重新搭建项目脚手架,并手动移植你的自定义代码和配置。
从 Kubebuilder v3.0.0 起,Kubebuilder 使用的所有输入都会记录在 PROJECT 文件中。请检查当前项目中的该文件,以核对已记录的配置和元数据。请参阅 PROJECT 文件文档 以更好地理解。
另外,在开始之前,建议查阅基础项目包含什么?,以便更好地理解项目布局和结构。
阶段 1:Reorganize to New Layout (Required only for Legacy Layouts)
仅在以下任一条件为真时需要:
- 控制器不在
internal/controller/中 - Webhook 不在
internal/webhook/中 - main 不在
cmd/中
如果你的项目已使用 internal/controller/、internal/webhook/ 和 cmd/main.go,可跳过此阶段。
1.1 创建重组分支
git checkout -b reorganize
1.2 重组文件位置
将文件移到新布局:
# If you have controllers/ directory
mkdir -p internal/controller
mv controllers/* internal/controller/
rmdir controllers
# OR if you have pkg/controllers/ directory
mkdir -p internal/controller
mv pkg/controllers/* internal/controller/
# If you have webhooks in api/v1/ or apis/v1/
mkdir -p internal/webhook/v1
mv api/v1/*_webhook* internal/webhook/v1/ 2>/dev/null || mv apis/v1/*_webhook* internal/webhook/v1/ 2>/dev/null || echo "未找到可移动的 webhook 文件(如果项目没有 webhook,这是预期行为)"
# If main.go is in root
mkdir -p cmd
mv main.go cmd/
1.3 更新 package 声明
移动文件后,更新 package 声明:
控制器: 在所有 *_controller.go 和 *_controller_test.go 文件中,将 package controllers → package controller。
Webhook: 保持版本作为包名(例如,在 internal/webhook/v1/ 中 package v1 仍为 package v1)。
1.4 更新 import 路径
查找并更新所有 import:
grep -r "pkg/controllers\|/controllers\"" --include="*.go"
在找到的每个文件中更新:
- 导入:
<module>/controllers或<module>/pkg/controllers→<module>/internal/controller - 引用:
controllers.TypeName→controller.TypeName
1.5 更新 Dockerfile(如需要)
如果你的 Dockerfile 对已移动路径有显式 COPY 语句,请更新它们以反映新结构,或简化为 COPY . . 并使用 .dockerignore 排除不需要的文件。
1.6 验证并提交
构建并测试重组后的项目:
make generate manifests
make build && make test
如果成功,提交布局更改。你的项目现在使用新布局。继续进入阶段 2。
阶段 2:Migrate to Latest Version
步骤 1:Prepare Your Current Project
1.1 创建迁移分支
从当前代码库创建分支:
git checkout -b migration
1.2 创建备份
mkdir ../migration-backup
cp -r . ../migration-backup/
1.3 清理项目目录
删除除 .git 外的所有文件:
find . -not -path './.git*' -not -name '.' -not -name '..' -delete
步骤 2:Initialize the New Project
关于 PROJECT 文件: 从 v3.0.0+ 起,PROJECT 文件跟踪所有脚手架元数据。如果你有该文件且所有资源都通过 CLI 创建,请先尝试 kubebuilder alpha generate。否则,按下面的手动步骤识别并重新搭建所有资源的脚手架。
2.1 识别你的 module 与 domain
从备份中识别初始化所需的信息。
Module 路径 - 检查备份中的 go.mod 文件:
cat ../migration-backup/go.mod
查找 module 行:
module tutorial.kubebuilder.io/migration-project
Domain - 检查备份的 PROJECT 文件:
cat ../migration-backup/PROJECT
查找 domain 行:
domain: tutorial.kubebuilder.io
如果你没有 PROJECT 文件(版本 < v3.0.0),请检查 config/crd/bases/ 下的 CRD 文件,或查看 API group 名称。domain 是 API group 中 group 名称之后的部分。
2.2 初始化 Go module
使用原项目中相同的 module 路径初始化新的 Go module:
go mod init tutorial.kubebuilder.io/migration-project
将 tutorial.kubebuilder.io/migration-project 替换为你的实际 module 路径。
2.3 初始化 Kubebuilder 项目
使用 Kubebuilder 初始化项目:
kubebuilder init --domain tutorial.kubebuilder.io --repo tutorial.kubebuilder.io/migration-project
替换为你的实际 domain 和 repository(module 路径)。
2.4 启用多 group 支持(如需要)
多 group 项目将 API 组织到不同的 group 中,每个 group 位于自己的目录。当你有用于不同目的或领域的 API 时,这很有用。
通过检查备份的目录结构,确认项目是否使用多 group 布局:
-
单 group 布局: 所有 API 在一个 group 中
api/v1/cronjob_types.goapi/v1/job_types.goapi/v2/cronjob_types.go
-
多 group 布局: API 按 group 组织
api/batch/v1/cronjob_types.goapi/crew/v1/captain_types.goapi/sea/v1/ship_types.go
你也可以在备份的 PROJECT 文件中检查是否有 multigroup: true。
如果你的项目使用多 group 布局,请在创建 API 之前启用它:
kubebuilder edit --multigroup=true
按本指南操作时,你会自动获得新布局,因为你是用最新版本创建全新项目,并将代码移植进去。
步骤 3:Re-scaffold APIs and Controllers
对原项目中的每个 API 资源,在新项目中重新搭建脚手架。
3.1 识别你的全部 API
查看备份项目(../migration-backup/)以识别所有 API。无论是否有 PROJECT 文件,都建议检查备份目录,因为并非所有资源都可能通过 CLI 创建。
检查备份中的目录结构,确保不会遗漏任何手动创建的资源:
-
在
api/目录(或较旧 Kubebuilder 版本生成的项目中的apis/)中查找*_types.go文件:- 单 group:
api/v1/cronjob_types.go- 提取:versionv1,kindCronJob,group 来自导入 - 多 group:
api/batch/v1/cronjob_types.go- 提取:groupbatch,versionv1,kindCronJob
- 单 group:
-
在这些位置检查控制器:
- 当前:
internal/controller/cronjob_controller.go或internal/controller/<group>/cronjob_controller.go - 旧版:
controllers/cronjob_controller.go或pkg/controllers/cronjob_controller.go
- 当前:
如果你从 Kubebuilder v3.0.0+ 起使用 CLI 创建了所有 API,它们应出现在 PROJECT 文件的 resources 部分,例如:
resources:
- api:
crdVersion: v1
namespaced: true
controller: true
group: batch
kind: CronJob
version: v1
3.2 创建每个 API 和控制器
对步骤 3.1 中识别的每个 API,重新搭建脚手架:
kubebuilder create api --group batch --version v1 --kind CronJob
出现提示时:
- 对 “Create Resource [y/n]” 回答 yes 以生成 API 类型
- 如果你的原项目对此 API 有控制器,对 “Create Controller [y/n]” 回答 yes
创建每个 API 后,更新生成的清单和代码:
make manifests # Generate CRD, RBAC, and other config files
make generate # Generate code (e.g., DeepCopy methods)
然后验证一切可以编译:
make build
这些步骤确保新搭建的 API 正确集成。有关 API 创建工作流的详细演练,请参见快速开始指南。
对项目中的所有 API 重复此过程。
创建所有资源后,重新生成清单:
make manifests
make generate
3.3 重新脚手架生成 Webhook(如适用)
如果原项目有 webhook,你需要重新搭建它们的脚手架。
识别备份项目中的 Webhook:
-
从目录结构查找 webhook 文件:
- 旧位置(v3 及更早):
api/v1/<kind>_webhook.go或api/<group>/<version>/<kind>_webhook.go - 当前位置(单 group):
internal/webhook/<version>/<kind>_webhook.go - 当前位置(多 group):
internal/webhook/<group>/<version>/<kind>_webhook.go
- 旧位置(v3 及更早):
-
从
PROJECT文件(如可用)检查每个资源的 webhooks 部分:
resources:
- api:
...
webhooks:
defaulting: true
validation: true
webhookVersion: v1
重新脚手架生成 Webhook:
对每个带有 Webhook 的资源运行:
kubebuilder create webhook --group batch --version v1 --kind CronJob --defaulting --programmatic-validation
Webhook 选项:
--defaulting- 创建默认值 Webhook(设置默认值)--programmatic-validation- 创建校验 Webhook(校验创建/更新/删除操作)--conversion- 创建转换 Webhook(用于多版本 API,见下一节)
3.4 重新脚手架生成转换 Webhook(如适用)
如果你的项目有带 conversion webhook 的多版本 API,需要设置 hub-spoke 转换模式。
设置转换 Webhook:
为 hub 版本创建 conversion webhook,并使用 --spoke 标志指定 spoke 版本。
注意: 下面示例中使用 v1 作为 hub 仅作说明。请选择项目中应作为中心转换点的版本——通常是功能最完整且稳定的存储版本,不一定是最旧或最新的版本。
kubebuilder create webhook --group batch --version v1 --kind CronJob --conversion --spoke v2
该命令:
- 为作为 hub 版本的
v1创建 conversion webhook - 将
v2配置为与 hubv1相互转换的 spoke - 生成带有转换方法存根的
*_conversion.go文件
对于多个 spoke,将它们指定为逗号分隔的列表:
kubebuilder create webhook --group batch --version v1 --kind CronJob --conversion --spoke v2,v1alpha1
这将 v1 设为 hub,并将 v2 和 v1alpha1 都设为 spoke。
你需要实现的内容:
该命令会生成方法存根,你将在步骤 4 中填写:
- Hub 版本:实现
Hub()方法(通常只是一个标记) - Spoke 版本:实现带有转换逻辑的
ConvertTo(hub)和ConvertFrom(hub)方法
有关实现转换逻辑的全面指导,请参见多版本教程。
搭建所有 webhook 脚手架后,验证一切可以编译:
make manifests && make build
步骤 4:Port Your Custom Code
将自定义业务逻辑和配置从备份手动移植到新项目。
4.1 移植 API 定义
比较并合并备份项目中的自定义 API 字段和标记。
需要对比的文件:
- 单 group:
api/v1/<kind>_types.go - 多 group:
api/<group>/<version>/<kind>_types.go
需要移植的内容:
- Spec 与 Status 结构体中的自定义字段
- 验证标记 - 例如
+kubebuilder:validation:Minimum=0、+kubebuilder:validation:Pattern=... - CRD 生成标记 - 例如
+kubebuilder:printcolumn、+kubebuilder:resource:scope=Cluster - 子资源(SubResources) - 例如
+kubebuilder:subresource:status、+kubebuilder:subresource:scale - 文档注释 - 用于 CRD 描述
有关所有可用标记,请参见 CRD 生成、CRD 验证 和标记。
如果你的 API 引用了父包(例如 scheduling.GroupName),请移植它:
mkdir -p api/<group>/
cp ../migration-backup/apis/<group>/groupversion_info.go api/<group>/
移植 API 定义后,重新生成并验证:
make manifests # Generate CRD manifests from your types
make generate # Generate DeepCopy methods
这可确保在继续之前正确生成 API 类型和 CRD 清单。
4.2 移植控制器逻辑
需要对比的文件:
- 当前单 group:
internal/controller/<kind>_controller.go - 当前多 group:
internal/controller/<group>/<kind>_controller.go
需要移植的内容:
- Reconcile 函数实现 - 你的核心业务逻辑
- 辅助函数 - 控制器文件中的任何额外函数
- RBAC 标记 -
+kubebuilder:rbac:groups=...,resources=...,verbs=... - 额外的 watch -
SetupWithManager中的自定义 watch 配置 - 导入 - 控制器需要的任何额外包
- 结构体字段 - 添加到 Reconciler 结构体的自定义字段
有关权限标记的详细信息,请参见 RBAC 标记。
移植控制器逻辑后,重新生成清单并验证编译:
make generate
make manifests
make build
4.3 移植 Webhook 实现
Webhook 在不同 Kubebuilder 版本之间位置有变化。请注意路径差异:
旧版 webhook 位置(Kubebuilder v3 及更早):
api/v1/<kind>_webhook.goapi/<group>/<version>/<kind>_webhook.go
当前 Webhook 位置:
- 单 group:
internal/webhook/<version>/<kind>_webhook.go - 多 group:
internal/webhook/<group>/<version>/<kind>_webhook.go
需要移植的内容:
- Defaulting webhook -
Default()方法实现 - Validation webhook -
ValidateCreate()、ValidateUpdate()、ValidateDelete()方法 - Conversion webhook -
ConvertTo()和ConvertFrom()方法(用于多版本 API) - 辅助函数 - 任何验证或默认值辅助函数
- Webhook 标记 - 通常自动生成,但请验证它们符合你的需求
有关详细信息,请参见 Webhook 概览、准入 Webhook 和多版本教程。
针对转换 Webhook:
如果你有 conversion webhook,请确保在步骤 3.4 中使用了 create webhook --conversion --spoke <version> 命令。这会自动设置 hub-spoke 基础设施。你只需在 spoke 版本的 ConvertTo() 和 ConvertFrom() 方法以及 hub 版本的 Hub() 方法中填写转换逻辑。
该命令会创建所有必要的样板代码——你只需实现版本之间字段转换的业务逻辑。
移植 Webhook 后,重新生成并验证:
make generate
make manifests
make build
4.4 移植 main.go 自定义内容(如有)
文件: cmd/main.go
大多数项目不需要自定义 main.go,因为 Kubebuilder 会自动处理所有标准设置(注册 API、设置控制器和 webhook、Manager 初始化、metrics 等)。
仅移植不属于标准脚手架的自定义内容。将备份的 main.go 与新搭建的文件比较,识别你添加的任何自定义逻辑。
4.5 配置 Kustomize 清单
config/ 目录包含用于部署 operator 的 Kustomize 清单。与备份比较,确保所有配置都已正确设置。
审查并更新这些目录:
-
config/default/kustomization.yaml- 主 kustomization 文件- 如果你有 webhook,确保启用了 webhook 配置(取消注释与 webhook 相关的 patches)
- 如果使用 webhook,确保启用了 cert-manager(取消注释 certmanager 资源)
- 根据原始配置启用或禁用 metrics 端点
- 审查命名空间与名称前缀设置
-
config/manager/- 控制器 Manager 部署- 除非有自定义,通常无需更改。如有自定义,请将资源 limits 和 requests 与备份比较,并检查环境变量
-
config/rbac/- RBAC 配置- 通常由标记自动生成——无需手动更改
- 仅在有标记未覆盖的自定义 role binding 或 service account 配置时检查
-
config/webhook/- Webhook 配置(如适用)- 通常自动生成 - 无需手动更改
- 仅在有自定义 webhook service 或证书配置时检查
-
config/samples/- 示例 CR 清单- 从备份复制你的示例资源
配置 Kustomize 后,验证清单能正确构建:
make all
make build-installer
4.6 移植额外自定义内容
从备份移植任何额外的包、依赖和自定义内容:
额外包(例如 pkg/util):
cp -r ../migration-backup/pkg/<package-name> pkg/
# 更新导入路径(适用于 macOS 和 Linux)
find pkg/ -name "*.go" -exec sed -i.bak 's|<module>/apis/|<module>/api/|g' {} \;
find pkg/ -name "*.go.bak" -delete
对于依赖,运行 go mod tidy,或对复杂项目从备份复制 go.mod/go.sum。
检查其他自定义内容(Makefile、Dockerfile、测试文件)。使用 diff 工具与备份比较,识别遗漏的文件。
移植所有自定义内容后,验证一切可以构建:
make all
步骤 5:Test and Verify
与备份比较,确保所有自定义内容都已正确移植,例如:
diff -r --brief ../migration-backup/ . | grep "Only in ../migration-backup"
运行测试并验证功能:
make test && make lint-fix
部署到测试集群(例如 kind)并验证更改(即验证预期行为、运行回归检查、确认完整 CI 流水线仍能通过,并执行 e2e 测试)。
其他资源
- 迁移概览 - 所有迁移选项的概览
- PROJECT 文件参考 - 理解 PROJECT 文件
- 基础项目包含什么? - 理解项目结构
- Alpha Generate 命令 - 自动重新搭建脚手架
- Alpha Update 命令 - 自动迁移
- 使用外部类型 - 针对非本项目定义类型的控制器
- CRD 生成 - 从 Go 类型生成 CRD
- CRD 验证 - 为 API 添加验证
- 标记 - 代码生成的所有可用标记
- RBAC 标记 - 生成 RBAC 清单
- Webhook 概览 - 理解 webhook
- 准入 Webhook - 实现准入 webhook
- 多版本教程 - 处理多个 API 版本
- 部署 cert-manager - webhook 所需
- 配置 EnvTest - 使用 EnvTest 进行测试
使用 AI 将项目从任意版本迁移到最新版本
AI 可以通过减少重复工作并帮助解决破坏性变更来协助手动迁移。它不会取代手动迁移流程,但可以帮助减少工作量并达成目标。
工作流与 AI 辅助步骤
步骤 1:重组到新布局(仅旧布局需要)
AI 帮助确保项目按新布局组织(main.go 在 cmd/ 下,控制器和 webhook 在 internal/ 内)。审阅并验证重组,然后运行 make build 确保仍能编译。
步骤 2:用于重新脚手架的发现 CLI 命令
AI 分析你的项目并生成使用最新版本完全重新搭建脚手架所需的所有 Kubebuilder CLI 命令。创建备份(mkdir ../migration-backup && cp -r . ../migration-backup/),然后执行生成的命令以搭建全新项目。
步骤 3:移植自定义代码
AI 帮助将自定义代码从备份移植到新搭建的项目。仔细审阅所有更改,确保业务逻辑正确转移。
参见 步骤 3:移植自定义代码
步骤 4:验证
运行 make generate && make manifests && make build,然后运行 make test 验证所有测试通过。部署到测试集群并验证你的方案仍能完成相同的工作。
完整详情请参见手动迁移流程。
步骤 1:Reorganize to New Layout (Required only for Legacy Layouts)
如果你的项目是用 Kubebuilder v3.0.0 之前的版本构建的,你可能需要此步骤。
重组文件以匹配新的目录布局。
检查是否需要此步骤(若以下任一为真,则需要):
- 控制器不在
internal/controller/中 - Webhook 不在
internal/webhook/中 - main 不在
cmd/中
如果全部已在新布局中,跳到步骤 2
提供给 AI 助手的说明
将这些说明复制并粘贴给你的 AI 助手:
Reorganize Kubebuilder project files to match new directory layout.
CONTEXT:
- Project location: . (current directory - your existing project)
- Goal: Move files to new layout WITHOUT changing code or versions
- Keep project functional after reorganization
STEP 1 - Check which files need to move:
- Controllers in controllers/ or pkg/controllers/: needs move
- Controllers in internal/controller/ or internal/controller/<group>/ (multi-group): already correct
- Webhooks in api/v1/ or apis/<group>/v1/: needs move
- Webhooks in internal/webhook/v1/ or internal/webhook/<group>/v1/ (multi-group): already correct
- Main in root (main.go): needs move
- Main in cmd/ (cmd/main.go or cmd/*/main.go): already correct
STEP 2 - Reorganize file locations:
a. Move controllers if needed:
- If controllers/ directory exists:
mkdir -p internal/controller
mv controllers/* internal/controller/
rmdir controllers
- If pkg/controllers/ directory exists:
mkdir -p internal/controller
mv pkg/controllers/* internal/controller/
b. Move webhooks if needed:
- If api/v1/ or apis/v1/ contains *_webhook.go files:
mkdir -p internal/webhook/v1
mv api/v1/*_webhook* internal/webhook/v1/ 2>/dev/null || mv apis/v1/*_webhook* internal/webhook/v1/ 2>/dev/null || true
- If api/<group>/v1/ or apis/<group>/v1/ contains webhooks (multi-group):
mkdir -p internal/webhook/<group>/v1
mv api/<group>/v1/*_webhook* internal/webhook/<group>/v1/ 2>/dev/null || mv apis/<group>/v1/*_webhook* internal/webhook/<group>/v1/ 2>/dev/null || true
c. Move main.go if needed:
- If main.go exists in root:
mkdir -p cmd
mv main.go cmd/
STEP 3 - Update import paths in ALL files:
After moving files, imports will break. Fix them systematically:
a. In cmd/main.go (or cmd/*/main.go, cmd/*/*.go):
- Find: import "your-module/controllers"
- Replace with: import "your-module/internal/controller"
- Find: import "your-module/pkg/controllers"
- Replace with: import "your-module/internal/controller"
- Find: &controllers.SomeReconciler or controllers.NewController
- Replace with: &controller.SomeReconciler or controller.NewController
- API imports (api/v1, apis/v1alpha1) - NO CHANGE needed
b. In internal/controller/*.go files:
- Check package declaration is still: package controller (not controllers)
- API imports stay same - NO CHANGE needed
- If you had controller-to-controller imports, update paths
c. In internal/webhook/v1/*.go files:
- Check package declaration: should be package v1
- API imports stay same - NO CHANGE needed
- Webhook imports in main.go may need updating
STEP 4 - Update Dockerfile (if using explicit COPY):
Check Dockerfile for explicit COPY statements. If found, update:
Old pattern:
COPY cmd/main.go cmd/main.go
COPY api/ api/
COPY internal/controller/ internal/controller/
Option 1 - Simplify (recommended):
COPY . .
Ensure .dockerignore has:
**
!go.mod
!go.sum
!**/*.go
**/*_test.go
Note: Podman does not honor the !**/*.go re-include
(https://github.com/containers/buildah/issues/6417). If the project is built
with Podman, also re-include the source directories by name:
!cmd
!api
!internal
Option 2 - Update explicit paths:
COPY cmd/ cmd/
COPY api/ api/
COPY internal/ internal/
STEP 5 - Verify reorganization:
- Run: go mod tidy
- Run: make generate
- Run: make manifests
- Run: make build
- Run: make test
If errors, fix import paths.
Success: new layout, make build succeeds, make test passes, project functional
作用说明
AI 将会:
- 移动文件到新布局(controllers/ 到 internal/controller/,webhooks 到 internal/webhook/,main.go 到 cmd/)
- 移动后在所有文件中修复 import 路径
- 验证重组后的项目能够构建且测试通过
完成此步骤后,你的项目使用新布局(相同代码,新位置),使迁移简单得多!
后续步骤
AI 重组之后:
- 验证:
make build && make test(在当前项目中) - 如果成功,备份并继续到步骤 2:发现 CLI 命令
- 如有错误,先审查并修复再继续
步骤 2:Discovery CLI Commands
使用 AI 分析你(现已重组)的 Kubebuilder 项目,并生成用最新版本重建它所需的所有 CLI 命令。
提供给 AI 助手的说明
将这些说明复制并粘贴给你的 AI 助手(Cursor、Claude、GitHub Copilot 等):
Analyze this Kubebuilder project and generate all CLI commands to recreate it.
CONTEXT:
Kubebuilder projects have these components:
APIs (Custom Resources):
- Location: api/ or apis/ directory
- Recognition: Look for Go structs with marker: // +kubebuilder:object:root=true
- Pattern: type <Name> struct with metav1.TypeMeta and metav1.ObjectMeta fields
- Example: type Captain struct { metav1.TypeMeta; metav1.ObjectMeta; Spec CaptainSpec; Status CaptainStatus }
Controllers:
- Location: controllers/, internal/controller/, or pkg/controllers/
- Recognition: Look for Reconcile() function signature
- Pattern: func (r *<Name>Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error)
- Struct embeds: client.Client
- Has function: SetupWithManager(mgr ctrl.Manager) error
Webhooks:
- Location: api/v1/ or internal/webhook/v1/
- Recognition: Look for webhook method signatures
- Defaulting pattern: func Default() or func Default(ctx context.Context, obj *<Type>) error
- Validation pattern: func ValidateCreate() error or func ValidateCreate(ctx context.Context, obj *<Type>) (admission.Warnings, error)
- Conversion pattern: func Hub() or func ConvertTo() or func ConvertFrom()
CLI Command Formats:
- kubebuilder init --domain <domain> --repo <module>
- kubebuilder edit --multigroup=true (if multi-group layout)
- kubebuilder create api --group <group> --version <version> --kind <Kind> --controller=<bool> --resource=<bool>
* --controller=true: create controller
* --resource=true: create API definition
* --resource=false: controller only (for external types like Deployment, Pod)
- kubebuilder create webhook --group <group> --version <version> --kind <Kind> [flags]
* --defaulting: sets default values
* --programmatic-validation: validates create/update/delete
* --conversion --spoke <versions>: for multi-version APIs (hub-spoke pattern)
- Hub version: Usually oldest stable version (e.g., v1) - command runs on this version
- Spoke versions: Newer versions that convert to/from hub (e.g., v2, v3) - specified with --spoke
- Example: --group crew --version v1 --kind Captain --conversion --spoke v2
(v1 is hub, v2 is spoke)
- External types (k8s.io/api/*): use --resource=false --controller=true
Project structure patterns:
- Single-group: api/v1/, api/v2/ (versions directly under api/)
- Multi-group: api/<group>/v1/, api/<group>/v2/ (group subdirectories)
- Multi-group detection: Check PROJECT file for "multigroup: true" OR check if api/ has group subdirectories
Files to IGNORE:
- zz_generated.*.go (auto-generated code)
- groupversion_info.go (just group registration)
- config/crd/bases/*.yaml (auto-generated from code)
- config/rbac/*.yaml (auto-generated from markers)
References:
- Kubebuilder Book: ../introduction.md
- controller-runtime: https://github.com/kubernetes-sigs/controller-runtime
- controller-tools: https://github.com/kubernetes-sigs/controller-tools
ANALYZE PROJECT:
1. Extract module path from go.mod (line 1: "module <path>")
2. Extract domain from PROJECT file (domain: <value>) OR api/*/groupversion_info.go (// +groupName=<group>.<domain>)
3. Detect multi-group: api/ has api/<group>/v1/ structure? (yes/no)
4. Scan api/ or apis/ directory - Find ALL your own APIs:
- Find all *_types.go files OR types.go (exclude groupversion_info.go, zz_generated.deepcopy.go)
- For each file, find: type <Kind> struct with // +kubebuilder:object:root=true above it
- Extract: Kind name, group (from groupversion_info.go +groupName comment), version (from directory)
- Check controller: look for controllers/<lowercaseKind>_controller.go OR internal/controller/<lowercaseKind>_controller.go OR pkg/controllers/<lowercaseKind>_controller.go
- Check webhooks: look for api/v1/<lowercaseKind>_webhook.go OR internal/webhook/v1/<lowercaseKind>_webhook.go
- If webhook file found, scan for methods:
* "func (r *<Kind>) Default()": has --defaulting
* "func (r *<Kind>) ValidateCreate()": has --programmatic-validation
* "func (*<Kind>) Hub()": this version is conversion hub
* "func (r *<Kind>) ConvertTo(": this version is a spoke
5. Scan internal/controller/, controllers/, or pkg/controllers/ - Find controllers for external types:
- For each *_controller.go file, check imports NOT from your module
- Look for: k8s.io/api/apps/v1, k8s.io/api/core/v1, github.com/cert-manager/cert-manager/pkg/apis/*
- Extract type from: type <Kind>Reconciler struct OR Reconcile signature
- This is a controller-only resource (use --controller=true --resource=false)
6. Scan internal/webhook/ - Find webhooks for external types:
- For each *_webhook.go file in internal/webhook/v1/ (or other versions)
- Check if the Kind type is imported (not defined in your api/)
- If imported from k8s.io/api/* or external package: external type webhook
- Scan for Default() and ValidateCreate() methods to determine flags
OUTPUT FORMAT (bash script):
#!/bin/bash
# Module: <module-path>
# Domain: <domain>
# Multi-group: <yes/no>
set -e
kubebuilder init --domain <domain> --repo <module-path>
kubebuilder edit --multigroup=true # only if multi-group
# External type controllers (--resource=false)
kubebuilder create api --group cert-manager --version v1 --kind Certificate \
--controller=true --resource=false \
--external-api-path=<path> --external-api-domain=<domain> --external-api-module=<module>
# Your own APIs (--resource=true)
kubebuilder create api --group crew --version v1 --kind Captain --controller=true --resource=true
kubebuilder create api --group crew --version v2 --kind FirstMate --controller=false --resource=true
# Webhooks for your own APIs
kubebuilder create webhook --group crew --version v1 --kind Captain --defaulting --programmatic-validation
kubebuilder create webhook --group crew --version v1 --kind FirstMate --conversion --spoke v2
# Webhooks for external/core types (no create api needed)
kubebuilder create webhook --group apps --version v1 --kind Deployment --defaulting --programmatic-validation
kubebuilder create webhook --group core --version v1 --kind Pod --defaulting
make manifests && make generate && make build
RULES:
- Combine ALL webhook types in ONE command: --defaulting --programmatic-validation together
- Conversion webhooks: use hub version and list ALL spokes: --conversion --spoke v2,v3
- List EVERY Kind found in source code, not just what is in PROJECT file
- External type controllers: use --controller=true --resource=false
- Webhooks for external/core types: just create webhook (no create api needed)
- Order: external controllers first, then your APIs, then all webhooks
理解输出
AI 将分析你的项目并输出一个 bash 脚本。脚本将按此顺序包含命令:
kubebuilder init- 初始化项目kubebuilder edit --multigroup=true- If multi-group detectedkubebuilder create api- 用于外部类型控制器(带--resource=false)kubebuilder create api- 用于你自己的 API(带--resource=true)kubebuilder create webhook- 用于所有 Webhookmake manifests && make generate && make build- Verify
示例输出
以下是 AI 说明生成内容的真实示例:
示例 1:简单多 group 项目
已分析:kubernetes-sigs/scheduler-plugins
#!/bin/bash
# Module: sigs.k8s.io/scheduler-plugins
# Domain: scheduling.x-k8s.io
# Multi-group: YES
set -e
kubebuilder init --domain scheduling.x-k8s.io --repo sigs.k8s.io/scheduler-plugins
kubebuilder edit --multigroup=true
kubebuilder create api --group scheduling --version v1alpha1 --kind ElasticQuota --controller=true --resource=true
kubebuilder create api --group scheduling --version v1alpha1 --kind PodGroup --controller=true --resource=true
make manifests && make generate && make build
已发现: 2 个 API,多 group,无 Webhook
示例 2:带 webhook 的单 group(go/v3 迁移)
已分析:project-v3
#!/bin/bash
# Module: sigs.k8s.io/kubebuilder/testdata/project-v3
# Domain: testproject.org
# Multi-group: NO
set -e
kubebuilder init --domain testproject.org --repo sigs.k8s.io/kubebuilder/testdata/project-v3
kubebuilder create api --group crew --version v1 --kind Captain --controller=true --resource=true
kubebuilder create api --group crew --version v1 --kind FirstMate --controller=true --resource=true
kubebuilder create api --group crew --version v1 --kind Admiral --controller=true --resource=true
kubebuilder create webhook --group crew --version v1 --kind Captain --defaulting --programmatic-validation
kubebuilder create webhook --group crew --version v1 --kind Admiral --defaulting
make manifests && make generate && make build
已发现: 3 个 API,单 group,带 defaulting 和 validation 的 webhook
示例 3:带外部类型的复杂多 group
已分析:testdata/project-v4-multigroup
#!/bin/bash
# Module: sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup
# Domain: testproject.org
# Multi-group: YES
set -e
kubebuilder init --domain testproject.org --repo sigs.k8s.io/kubebuilder/testdata/project-v4-multigroup
kubebuilder edit --multigroup=true
# External type controllers
kubebuilder create api --group cert-manager --version v1 --kind Certificate \
--controller=true --resource=false \
--external-api-path=github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 \
--external-api-domain=io \
--external-api-module=github.com/cert-manager/cert-manager@v1.19.2
kubebuilder create api --group apps --version v1 --kind Deployment --controller=true --resource=false
# APIs - Group: crew
kubebuilder create api --group crew --version v1 --kind Captain --controller=true --resource=true
# APIs - Group: ship
kubebuilder create api --group ship --version v1beta1 --kind Frigate --controller=true --resource=true
kubebuilder create api --group ship --version v1 --kind Destroyer --controller=true --resource=true
kubebuilder create api --group ship --version v2alpha1 --kind Cruiser --controller=true --resource=true
# APIs - Group: sea-creatures
kubebuilder create api --group sea-creatures --version v1beta1 --kind Kraken --controller=true --resource=true
kubebuilder create api --group sea-creatures --version v1beta2 --kind Leviathan --controller=true --resource=true
# APIs - Group: foo.policy
kubebuilder create api --group foo.policy --version v1 --kind HealthCheckPolicy --controller=true --resource=true
# APIs - Group: foo
kubebuilder create api --group foo --version v1 --kind Bar --controller=true --resource=true
# APIs - Group: fiz
kubebuilder create api --group fiz --version v1 --kind Bar --controller=true --resource=true
# APIs - Group: example.com
kubebuilder create api --group example.com --version v1alpha1 --kind Memcached --controller=true --resource=true
kubebuilder create api --group example.com --version v1alpha1 --kind Busybox --controller=true --resource=true
kubebuilder create api --group example.com --version v1 --kind Wordpress --controller=true --resource=true
kubebuilder create api --group example.com --version v2 --kind Wordpress --controller=false --resource=true
# Webhooks for your APIs
kubebuilder create webhook --group crew --version v1 --kind Captain --defaulting --programmatic-validation
kubebuilder create webhook --group ship --version v1 --kind Destroyer --defaulting
kubebuilder create webhook --group ship --version v2alpha1 --kind Cruiser --programmatic-validation
kubebuilder create webhook --group example.com --version v1alpha1 --kind Memcached --programmatic-validation
kubebuilder create webhook --group example.com --version v1 --kind Wordpress --conversion --spoke v2
# Webhooks for external types
kubebuilder create webhook --group cert-manager --version v1 --kind Issuer --defaulting
kubebuilder create webhook --group core --version v1 --kind Pod --programmatic-validation
kubebuilder create webhook --group apps --version v1 --kind Deployment --defaulting --programmatic-validation
make manifests && make generate && make build
已发现: 跨 6 个 group 的 12 个 API、conversion webhook、外部控制器、外部 webhook
接下来做什么
- 仔细审阅生成的脚本,确保它与你的项目结构匹配。
- 将其保存为
migration-commands.sh并使其可执行:chmod +x migration-commands.sh - 按手动迁移流程操作:
- 将项目备份到其他位置
- 在项目根目录为空时执行此脚本中的命令
- 在获得完全重新搭建脚手架的项目后,你需要把所有代码重新加回去
- 移植你的自定义代码
步骤 3:Port Custom Code
在重组项目(步骤 1)并执行发现步骤中的脚手架命令(步骤 2)之后,使用 AI 将自定义代码移植到新项目。
提供给 AI 助手的说明
将这些说明复制并粘贴给你的 AI 助手:
Port custom code from Kubebuilder project backup to new scaffolded project.
CONTEXT:
What is scaffold vs custom:
- Scaffold: Auto-generated boilerplate by Kubebuilder (has "// TODO(user):" comments)
- Custom: Your business logic that replaces TODOs
Backup location: ../migration-backup/ (your old project with custom code)
New project: . (newly scaffolded project with TODOs to replace)
How to recognize each file type (by content, not just name):
API files (typically *_types.go):
- Have marker: // +kubebuilder:object:root=true
- Have structs: type <Name> struct with metav1.TypeMeta, metav1.ObjectMeta
- Have: <Name>Spec struct (desired state)
- Have: <Name>Status struct (observed state)
- Markers like: // +kubebuilder:validation:...
Controller files (typically *_controller.go):
- Have struct: type <Name>Reconciler struct { client.Client; Scheme *runtime.Scheme }
- Have function: func (r *<Name>Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error)
- Have function: func (r *<Name>Reconciler) SetupWithManager(mgr ctrl.Manager) error
- May have: // +kubebuilder:rbac markers before Reconcile
Webhook files (typically *_webhook.go):
- OLD pattern: func (r *<Name>) Default(), func (r *<Name>) ValidateCreate() error
- NEW pattern: type <Name>Defaulter struct, func (d *<Name>Defaulter) Default(ctx context.Context, obj *<Name>) error
- Conversion: func (*<Name>) Hub(), func (r *<Name>) ConvertTo(...), func (r *<Name>) ConvertFrom(...)
Main file:
- Has: func main()
- Has: ctrl.NewManager(...)
- Registers controllers and webhooks
File paths after Step 1:
- APIs in: api/v1/ or api/<group>/v1/
- Controllers in: internal/controller/ or internal/controller/<group>/
- Webhooks in: internal/webhook/v1/ or internal/webhook/<group>/v1/
- Main: cmd/main.go
Files to NEVER edit (auto-generated):
- config/crd/bases/*.yaml (generated from make manifests)
- config/rbac/role.yaml (generated from make manifests)
- config/webhook/manifests.yaml (generated from make manifests)
- **/zz_generated.*.go (generated from make generate)
- PROJECT file (managed by CLI)
Critical markers to NEVER remove:
- // +kubebuilder:scaffold:* (Kubebuilder injects code here)
Make command sequence:
- After editing APIs or markers: make generate && make manifests
- After editing Go code: make build
- After all changes: make lint-fix && make generate && make manifests && make all && make test
Common markers in API files:
- // +kubebuilder:validation:Required
- // +kubebuilder:validation:Minimum=1
- // +kubebuilder:validation:Pattern="^[a-z]+$"
- // +kubebuilder:printcolumn:name="Status",type=string,JSONPath=...
RBAC markers in controller files:
- // +kubebuilder:rbac:groups=<group>,resources=<resource>,verbs=get;list;watch;create;update;patch;delete
- // +kubebuilder:rbac:groups=<group>,resources=<resource>/status,verbs=get;update;patch
- // +kubebuilder:rbac:groups=<group>,resources=<resource>/finalizers,verbs=update
References:
- Kubebuilder Book: ../introduction.md
- Markers Reference: ../reference/markers.md
- controller-runtime: https://github.com/kubernetes-sigs/controller-runtime
- controller-tools: https://github.com/kubernetes-sigs/controller-tools
PORT CUSTOM CODE (in this order):
1. Port go.mod dependencies FIRST:
Compare ../migration-backup/go.mod with current go.mod
a. For packages in backup but NOT in new (exclude k8s.io/*, sigs.k8s.io/controller-*):
- Run: go get <package>@<version>
b. For packages in BOTH with different versions:
- Keep the HIGHER (newer) version
- If backup has newer version: go get <package>@<newer-version>
- If new scaffold has newer version: keep it (do not downgrade)
- NOTE: Old projects can have newer versions than scaffold
After ALL: run go mod tidy
2. Port API type definitions:
For each *_types.go in backup to new (paths match after Step 1):
Backup: ../migration-backup/api/v1/<kind>_types.go
New: api/v1/<kind>_types.go
Port:
- Custom fields in Spec and Status structs
- ALL +kubebuilder markers (validation, printcolumn, resource, etc.)
- Documentation comments
- Custom types (enums, type aliases)
- REMOVE "// TODO(user):" comments when adding fields
NEVER remove: // +kubebuilder:scaffold:* or // +kubebuilder:object:root=true
After each: go mod tidy && make generate && make manifests
3. Port controller implementations:
For each controller (paths match after Step 1):
Backup: ../migration-backup/internal/controller/<kind>_controller.go
New: internal/controller/<kind>_controller.go
Port in order:
a. Additional imports (ADD to existing)
b. Custom constants, variables, types, interfaces (before Reconciler struct)
c. Custom fields in <Kind>Reconciler struct
d. ALL +kubebuilder:rbac markers (place before Reconcile)
e. Reconcile() body (REMOVE "// TODO(user):" and paste custom logic)
f. ALL helper functions (closures and standalone)
g. SetupWithManager customizations (if any beyond default .For().Named().Complete())
After each: go mod tidy && make generate && make manifests && make build
4. Port webhooks:
CRITICAL: Code pattern depends on controller-runtime version!
Webhooks (paths match after Step 1):
Backup: ../migration-backup/internal/webhook/v1/<kind>_webhook.go
New: internal/webhook/v1/<kind>_webhook.go
Detect pattern by reading backup file:
- Has "func (r *<Kind>) Default() {": OLD pattern (needs adaptation)
- Has "func (d *<Kind>Defaulter) Default(ctx": NEW pattern (direct copy)
IF OLD pattern - ADAPT:
- Default(): Extract logic, paste after type assertion, change 'r.' to '<kind>.', add return nil, REMOVE TODO
- Validate*(): Extract logic, paste after assertion, change 'r.' to '<kind>.', change return types, REMOVE TODO
- Conversion: Copy Hub/ConvertTo/ConvertFrom directly (no change needed)
IF NEW pattern - DIRECT COPY:
- Copy Defaulter/Validator structs and all methods
- Copy helper functions and imports
After each: go mod tidy && make manifests && make build
5. Port main.go customizations:
Backup: ../migration-backup/cmd/main.go
New: cmd/main.go
Compare and port ONLY custom additions:
- Custom manager options
- Custom command-line flags
- Custom initialization before mgr.Start()
- Additional scheme registrations
DO NOT port standard scaffold (controller/webhook setup, manager config)
After: make build
6. Port config settings (ADAPT, do not copy):
a. config/default/kustomization.yaml - Compare and adapt:
- Uncomment webhook/certmanager if you have webhooks
- Update namespace/namePrefix if custom
- Match metrics configuration
- Add custom patches/resources
DO NOT copy entire file
b. Other config/*/kustomization.yaml - Check for custom patches, adapt if needed
c. Custom config dirs - Copy any additional dirs: config/dev/, config/prod/, etc.
After: make build-installer
7. Port config samples and customizations:
- Sample CRs: Copy ../migration-backup/config/samples/*.yaml to config/samples/
- Makefile: Copy custom targets from backup (preserve scaffolded targets)
- Dockerfile: Apply custom build steps from backup
8. Port ALL tests:
- Controller tests: Copy *_controller_test.go from backup
- Webhook tests: Copy *_webhook_test.go (adapt if pattern changed)
- E2E tests: Copy test/e2e/* if exist
- Integration tests: Copy test/integration/* if exist
9. Port additional files:
- README: Port custom sections (do not replace entire file)
- Additional dirs: Copy docs/, scripts/, examples/, charts/, testdata/ if exist
- Root files: Copy .env, VERSION, CHANGELOG.md, CONTRIBUTING.md if exist
- .github workflows: Copy custom workflows
DO NOT port: dist/, bin/, vendor/
10. Verify nothing missed:
- Run: diff -r --brief ../migration-backup/ . | grep "Only in ../migration-backup"
- Port any custom files found (ignore: .git/, bin/, vendor/, dist/, zz_generated.*, go.sum, auto-gen configs)
- Verify key files have custom code (APIs, controllers, webhooks)
11. Final verification:
- Run: go mod tidy
- Run: make lint-fix
- Run: make generate
- Run: make manifests
- Run: make build
- Run: make build-installer
- Run: make test
Success: no errors, tests pass, functionally identical to backup
IMPORTANT REMINDERS:
- NEVER edit auto-generated files (already listed in CONTEXT above)
- NEVER remove // +kubebuilder:scaffold:* comments
- REMOVE "// TODO(user):" when replacing with custom code
- ADAPT config YAML files, do not copy entire files
- Port EVERYTHING except: .git/, bin/, vendor/, dist/, zz_generated.*, go.sum
- Follow make command sequence from CONTEXT above
AI 将做什么
AI 将会:
- 检测布局 — 比较新旧项目结构
- 移植 API 定义 — 自定义字段、markers、文档
- 移植 controller 逻辑 — Imports、类型、Reconcile()、helpers、RBAC、SetupWithManager
- 适配 webhooks — 必要时处理模式变更,移植全部逻辑与 helpers
- 移植 main.go — 仅自定义初始化、flags 与 manager 选项
- 移植配置 — kustomization.yaml、samples、Makefile、Dockerfile
- 移植依赖 — 向 go.mod 添加包,运行 go mod tidy
- 移植测试 — Controller 测试、webhook 测试、e2e 测试、集成测试
- 移植其他文件 — README、docs/、scripts/、.github/ 及任何自定义目录
- 完整验证 — 运行 lint-fix、generate、manifests、build、test
AI 完成后
关键:请仔细审查!
示例:会移植哪些内容
API 自定义字段
来自备份(api/v1/captain_types.go):
type CaptainSpec struct {
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:Maximum=100
Replicas int32 `json:"replicas"`
// +kubebuilder:validation:Pattern=`^[a-z]+$`
Name string `json:"name"`
}
到新项目(TODO 已移除,自定义字段已添加):
type CaptainSpec struct {
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:Maximum=100
Replicas int32 `json:"replicas"`
// +kubebuilder:validation:Pattern=`^[a-z]+$`
Name string `json:"name"`
}
控制器协调逻辑
来自备份(Reconcile 函数体):
func (r *CaptainReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := log.FromContext(ctx)
// Your custom reconciliation logic here
var captain crewv1.Captain
if err := r.Get(ctx, req.NamespacedName, &captain); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// Custom business logic...
return ctrl.Result{}, nil
}
到新项目(TODO 已移除,自定义逻辑已添加):
func (r *CaptainReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := log.FromContext(ctx)
// Custom reconciliation logic from backup
var captain crewv1.Captain
if err := r.Get(ctx, req.NamespacedName, &captain); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// Custom business logic...
return ctrl.Result{}, nil
}
Webhook 适配(v3 到 v4)
来自 go/v3 备份:
func (r *Captain) Default() {
if r.Spec.Replicas == 0 {
r.Spec.Replicas = 1
}
}
到 go/v4 新项目:
func (d *CaptainDefaulter) Default(ctx context.Context, obj *crewv1.Captain) error {
// 已移植并适配的逻辑(obj 是类型安全的,无需断言):
if obj.Spec.Replicas == 0 {
obj.Spec.Replicas = 1
}
return nil
}
后续步骤
AI 移植你的代码后:
- 检查是否有遗漏、损坏或错误移植
- 部署到测试集群 - 验证行为
单组到多组
Kubebuilder 默认脚手架单 group 项目以保持简单,因为多数项目不需要多个 API groups。但需要时,可将现有单 group 项目转换为多 group 布局。这会将 API 与 controllers 重组到按 group 划分的目录中。
此设计决策的理由见设计文档。
理解布局
从单 group 到多 group 时会发生这些变化:
单 group 布局(默认):
api/<version>/*_types.go All your CRD schemas in one place
internal/controller/* All your controllers together
internal/webhook/<version>/* Webhooks organized by version (if you have any)
多 group 布局:
api/<group>/<version>/*_types.go CRD schemas organized by group
internal/controller/<group>/* Controllers organized by group
internal/webhook/<group>/<version>/* Webhooks organized by group and version (if you have any)
可通过检查 PROJECT 文件中的 multigroup: true 判断当前布局。
迁移
下列步骤将 CronJob 示例 从单 group 迁移到多 group 布局。
步骤 1:Enable multi-group mode
首先,告诉 Kubebuilder 你要使用多 group 布局:
kubebuilder edit --multigroup=true
该命令通过添加 multigroup: true 更新 PROJECT 文件。此变更之后:
- 你创建的新 API 将自动使用多 group 结构(
api/<group>/<version>/) - 现有 API 仍在当前位置,必须手动迁移(见下面步骤 3–9)
步骤 2:Identify your group name
查看 api/v1/groupversion_info.go 以找到 group 名称:
// +组名=batch.tutorial.kubebuilder.io
package v1
Group 名称是点号前的第一部分(本例中为 batch)。
步骤 3:Move your APIs
为 group 创建目录并移动 version 目录:
mkdir -p api/batch
mv api/v1 api/batch/
若有多个版本(如 v1、v2 等),全部移动:
mv api/v2 api/batch/
步骤 4:Move your controllers
创建 group 目录并移动所有 controller 文件:
mkdir -p internal/controller/batch
mv internal/controller/*.go internal/controller/batch/
这会将包括 suite_test.go 在内的所有 controller 文件移入 group 目录。每个 group 需要自己的测试套件。
步骤 5:Move your webhooks (if you have any)
若项目有 webhooks(检查是否有 internal/webhook/ 目录),添加 group 目录:
mkdir -p internal/webhook/batch
mv internal/webhook/v1 internal/webhook/batch/
mv internal/webhook/v2 internal/webhook/batch/ # if v2 exists
如果您没有 webhook,可以跳过这一部分。
步骤 6:Update import paths
将所有 import 语句更新为指向新位置。
以前如下所示:
import (
batchv1 "tutorial.kubebuilder.io/project/api/v1"
"tutorial.kubebuilder.io/project/internal/controller"
)
现在应如下所示:
import (
batchv1 "tutorial.kubebuilder.io/project/api/batch/v1"
batchcontroller "tutorial.kubebuilder.io/project/internal/controller/batch"
)
若有 webhooks,还需要更新这些 imports:
// Before
webhookv1 "tutorial.kubebuilder.io/project/internal/webhook/v1"
// After
webhookbatchv1 "tutorial.kubebuilder.io/project/internal/webhook/batch/v1"
需要检查并更新的文件:
cmd/main.gointernal/controller/batch/*.gointernal/webhook/batch/v1/*.go(若有 Webhook)api/batch/v1/*_test.go
提示:在整个项目中使用 IDE 的 “Find and Replace” 功能。
步骤 7:Update the PROJECT file
kubebuilder edit --multigroup=true 命令会在 PROJECT 文件中设置 multigroup: true,但不会更新现有 API 的路径。你需要手动更新每个资源的 path 字段。
验证你的 PROJECT 文件包含这些变更:
- 检查已设置
multigroup: true(在顶层):
layout:
- go.kubebuilder.io/v4
多组: true # Must be true
projectName: project
- 为每个资源更新
path字段:
Before:
resources:
- api:
crdVersion: v1
namespaced: true
controller: true
group: batch
kind: CronJob
path: tutorial.kubebuilder.io/project/api/v1 # 旧路径
version: v1
After:
resources:
- api:
crdVersion: v1
namespaced: true
controller: true
group: batch
kind: CronJob
path: tutorial.kubebuilder.io/project/api/batch/v1 # 带 group 的新路径
version: v1
对 PROJECT 文件中的所有资源重复此操作。
步骤 8:Update test suite CRD paths
更新测试套件中的 CRD 目录路径。由于文件下移了一层,路径需再加一层 ".."。
在 internal/controller/batch/suite_test.go 中:
之前(位于 internal/controller/suite_test.go):
testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")},
}
之后(现位于 internal/controller/batch/suite_test.go):
testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")},
}
若有 webhooks,更新 internal/webhook/batch/v1/webhook_suite_test.go:
之前(位于 internal/webhook/v1/webhook_suite_test.go):
testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")},
}
之后(现位于 internal/webhook/batch/v1/webhook_suite_test.go):
testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "..", "config", "crd", "bases")},
}
步骤 9:Verify the migration
运行下列命令验证一切正常:
make manifests # Regenerate CRDs and RBAC
make generate # Regenerate code
make test # Run tests
make build # Build the project
AI 辅助迁移
若使用 AI 编码助手(Cursor、GitHub Copilot 等),可自动化大部分迁移步骤。
迁移到命名空间作用域 Manager
本指南介绍如何将现有集群作用域项目转换为 Namespace 作用域部署。
默认情况下,Kubebuilder 脚手架集群作用域的 managers,可监视并管理所有 namespaces 中的资源。本指南说明如何将现有集群作用域项目转换为 Namespace 作用域部署,使 manager 仅监视特定 namespace(s)。
何时使用命名空间作用域
在以下情况使用命名空间作用域:
- 在多租户集群中构建租户专用 managers
- 安全策略要求最小权限(无集群范围权限)
- 需要在不同 namespaces 中运行多个 manager 实例
- 仅管理 Namespace 作用域资源(Deployments、Services、ConfigMaps 等)
在以下情况使用集群作用域(默认):
- 管理集群作用域资源(Nodes、ClusterRoles、Namespaces 等)
- 单个 manager 实例管理所有 namespaces 中的资源
迁移
快速摘要:
- 运行
kubebuilder edit --namespaced --force— 脚手架 Role/RoleBinding 并更新 manager.yaml - 更新 cmd/main.go 以配置 Namespace 作用域缓存
- 在现有 controller 文件的 RBAC markers 中添加
namespace=参数 - 运行
make manifests— 根据更新后的 markers 重新生成 RBAC - 验证并部署
详细步骤
1. 启用命名空间作用域模式
kubebuilder edit --namespaced --force
该命令会自动:
- 在 PROJECT 文件中设置
namespaced: true - 脚手架
kind: Role(Namespace 作用域)的config/rbac/role.yaml - 脚手架
kind: RoleBinding的config/rbac/role_binding.yaml - 重新生成带有 WATCH_NAMESPACE 环境变量的
config/manager/manager.yaml - 为所有现有 API 重新生成
kind: Role(Namespace 作用域)的 admin/editor/viewer roles
注意: --force 标志会重新生成 config/manager/manager.yaml。不带 --force 时,必须手动添加 WATCH_NAMESPACE(见下文)。
2. 更新 cmd/main.go(必需的手动步骤)
edit 命令无法自动更新 cmd/main.go。你必须手动添加 Namespace 作用域配置。
a. 添加 import:
import (
// ... 现有导入 ...
"sigs.k8s.io/controller-runtime/pkg/cache"
)
b. 添加辅助函数(在 init() 之后、main() 之前):
// getWatchNamespace 返回 Manager 应监视变更的命名空间。
// 它从 WATCH_NAMESPACE 环境变量读取该值。
func getWatchNamespace() (string, error) {
watchNamespaceEnvVar := "WATCH_NAMESPACE"
ns, found := os.LookupEnv(watchNamespaceEnvVar)
if !found {
return "", fmt.Errorf("%s must be set", watchNamespaceEnvVar)
}
return ns, nil
}
// setupCacheNamespaces 配置缓存以监视特定命名空间。
func setupCacheNamespaces(namespaces string) cache.Options {
defaultNamespaces := make(map[string]cache.Config)
for _, ns := range strings.Split(namespaces, ",") {
defaultNamespaces[strings.TrimSpace(ns)] = cache.Config{}
}
return cache.Options{
DefaultNamespaces: defaultNamespaces,
}
}
c. 在 main() 函数中,于 ctrl.NewManager() 之前添加:
// 从 WATCH_NAMESPACE 环境变量获取命名空间作用域模式的命名空间。
watchNamespace, err := getWatchNamespace()
if err != nil {
setupLog.Error(err, "Unable to get WATCH_NAMESPACE")
os.Exit(1)
}
d. 更新 manager 创建以使用 Namespace 作用域缓存:
mgrOptions := ctrl.Options{
Scheme: scheme,
Metrics: metricsServerOptions,
WebhookServer: webhookServer,
HealthProbeBindAddress: probeAddr,
LeaderElection: enableLeaderElection,
LeaderElectionID: "your-leader-election-id",
// ... other existing options ...
}
// 配置缓存以监视 WATCH_NAMESPACE 中指定的命名空间
mgrOptions.Cache = setupCacheNamespaces(watchNamespace)
setupLog.Info("Watching namespace(s)", "namespaces", watchNamespace)
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), mgrOptions)
if err != nil {
setupLog.Error(err, "Failed to start manager")
os.Exit(1)
}
3. 更新现有控制器中的 RBAC 标记
对每个现有 controller 文件,在 RBAC markers 中添加 namespace= 参数。
查找控制器文件:
- 查找包含
func (r *SomeReconciler) Reconcile(的文件 - 常见位置:
internal/controller/*_controller.go
在 internal/controller/cronjob_controller.go 中:
之前(集群作用域):
// +kubebuilder:rbac:groups=batch.tutorial.kubebuilder.io,resources=cronjobs,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=batch.tutorial.kubebuilder.io,resources=cronjobs/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=batch.tutorial.kubebuilder.io,resources=cronjobs/finalizers,verbs=update
// Reconcile 是主 Kubernetes 协调循环的一部分
func (r *CronJobReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
之后(命名空间作用域):
// +kubebuilder:rbac:groups=batch.tutorial.kubebuilder.io,namespace=<project-name>-system,resources=cronjobs,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=batch.tutorial.kubebuilder.io,namespace=<project-name>-system,resources=cronjobs/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=batch.tutorial.kubebuilder.io,namespace=<project-name>-system,resources=cronjobs/finalizers,verbs=update
// Reconcile 是主 Kubernetes 协调循环的一部分
func (r *CronJobReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
将 project-system 替换为你的 namespace(在 config/default/kustomization.yaml 的 namespace: 字段中)。
4. 重新生成 RBAC 清单
在步骤 3 更新 RBAC markers 后,重新生成 RBAC manifests:
make manifests # Regenerate RBAC from updated controller markers
确认生成的文件显示 kind: Role 而非 kind: ClusterRole:
config/rbac/role.yaml:
kind: Role
metadata:
name: manager-role
# 注意:命名空间由 kustomize 在构建时添加,不在源文件中
**config/rbac/*_editor_role.yaml, _viewer_role.yaml, _admin_role.yaml:
kind: Role
metadata:
name: cronjob-editor-role
# 注意:命名空间由 kustomize 在构建时添加,不在源文件中
5. 验证并部署
运行测试以验证一切正常:
make generate # Regenerate code
make test # Run tests
部署并验证:
make deploy IMG=<your-image>
# Verify RBAC is namespace-scoped (not cluster-scoped)
kubectl get role,rolebinding -n <manager-namespace>
# Test: Create a resource in the manager's 命名空间 - 应被协调
kubectl apply -f config/samples/ -n <manager-namespace>
# 测试:在不同命名空间中创建资源 - 不应被协调
kubectl apply -f config/samples/ -n other-namespace
AI 辅助迁移
若使用 AI 编码助手(Cursor、GitHub Copilot 等),可自动化手动迁移步骤。
多命名空间支持
WATCH_NAMESPACE 环境变量支持逗号分隔的值,以监视多个特定 namespaces:
env:
- name: WATCH_NAMESPACE
value: "namespace-1,namespace-2,namespace-3"
注意:需要在每个 namespace 中创建 Role/RoleBinding 才能有正确的 RBAC。
恢复为集群作用域
恢复为集群作用域:
kubebuilder edit --namespaced=false --force
该命令会自动:
- 在 PROJECT 文件中设置
namespaced: false - 脚手架
kind: ClusterRole的config/rbac/role.yaml - 脚手架
kind: ClusterRoleBinding的config/rbac/role_binding.yaml - 使用
--force:重新生成不含 WATCH_NAMESPACE 环境变量的config/manager/manager.yaml
需要手动执行的步骤:
- 从所有 controller 文件的 RBAC markers 中移除
namespace=参数 - 运行
make manifests重新生成集群作用域 RBAC - 从
cmd/main.go中移除命名空间作用域代码:- 移除
getWatchNamespace()函数 - 移除
setupCacheNamespaces()函数 - 移除命名空间获取与缓存配置
- 若别处未使用,移除新增的 imports(
fmt、strings、cache)
- 移除
- 若未使用
--force,请手动从config/manager/manager.yaml移除WATCH_NAMESPACE
重要说明
- 仅 controllers 需要更新 RBAC:只更新 controller 文件(含
Reconcile函数的文件)中的+kubebuilder:rbacmarkers。Webhook 文件不使用 RBAC markers——webhooks 使用基于证书的认证与 API server 通信。 - RBAC markers 控制作用域:controller RBAC markers 中的
namespace=参数决定 controller-gen 生成Role(Namespace 作用域)还是ClusterRole(集群作用域)。没有namespace=参数时,controller-gen 始终生成ClusterRole。 - Controller-gen 会重新生成 role.yaml:运行
make manifests后,controller-gen 会根据 controller RBAC markers 重新生成config/rbac/role.yaml。kubebuilder edit --namespaced=true初始脚手架的Role作为模板,但实际内容由 controller-gen 管理。 - Namespace 参数格式:在 controller RBAC markers 中使用
namespace=<your-namespace>,通常为namespace=<project-name>-system以匹配部署 namespace。 - Metrics auth role 保持集群作用域:
metrics-auth-role使用集群作用域 API(TokenReview、SubjectAccessReview),正确地保持为无 namespace 参数的 ClusterRole。 - Webhooks 需要手动配置:目前 controller-gen 不支持 webhooks 的
namespaceSelector或objectSelectormarkers。详见上文 webhook 部分。
另见
- Manager 作用域 — Manager 作用域概念详解
- 项目配置 — PROJECT 文件配置参考
Alpha 命令
Kubebuilder 提供实验性的 alpha 命令,用于协助项目迁移和脚手架重新生成等高级操作。
这些命令通过自动化或部分自动化流程,简化了以往需要手动且易出错的任务。
目前可用的 alpha 命令如下:
alpha generate— 使用已安装的 CLI 版本重新脚手架生成项目alpha update— 利用脚手架快照通过三方合并自动化迁移过程
更多信息见各命令的专用文档。
使用(alpha generate)重新生成你的项目
概述
kubebuilder alpha generate 命令使用当前已安装的 CLI 和插件版本重新生成项目脚手架。
它根据 PROJECT 文件中的配置重新生成完整脚手架。这样你可以应用较新 Kubebuilder 版本引入的最新布局变更、插件特性以及代码生成改进。
你可以选择就地重新生成项目(覆盖现有文件),或输出到单独目录以便基于 diff 检查并手动集成。
什么时候使用它?
当 Kubebuilder 引入新变更时,你可以使用 kubebuilder alpha generate 升级项目脚手架。这包括插件更新(例如 go.kubebuilder.io/v3 → go.kubebuilder.io/v4)或 CLI 版本更新(例如 4.3.1 → latest)。
在以下情况此命令很有用:
- 更新项目以使用最新布局或插件版本
- 重新生成项目脚手架以包含近期变更
- 将当前脚手架与最新版本对比并手动应用更新
- 创建干净脚手架以审查或测试变更
当你希望完全掌控升级过程时使用此命令。若项目由较旧 CLI 版本创建且不支持 alpha update,此命令也很有用。
这种方法可以让你对比当前分支与上游脚手架更新(例如来自主分支)之间的差异,并帮助你将自定义代码叠加上新脚手架。
如何使用它?
将当前项目升级到已安装的 CLI 版本(即最新脚手架)
kubebuilder alpha generate
运行此命令后,项目会就地重新生成脚手架。然后你可以对比本地变更与主分支,查看更新内容,并按需重新叠加自定义代码。
将脚手架生成到新目录
使用 --input-dir 和 --output-dir 标志指定输入与输出路径。
kubebuilder alpha generate \
--input-dir=/path/to/existing/project \
--output-dir=/path/to/new/project
运行命令后,可在指定输出目录中检查生成的脚手架。
旗帜
| Flag | 描述 |
|---|---|
--input-dir | 包含 PROJECT 文件的目录路径。默认为当前工作目录。 |
--output-dir | 写入新脚手架的目录。若未设置,则在输入目录中就地重新生成;输入目录会先被清理:除 .git 外的所有文件都会被删除。 |
--plugins | 此次生成要使用的插件键。 |
-h, --help | 显示此命令的帮助。 |
进一步的资源
使用(alpha update)更新你的项目
概述
kubebuilder alpha update 使用三方 Git 合并将项目脚手架升级到更新的 Kubebuilder 发行版。它为旧版与新版重建干净脚手架,将当前代码合并到新脚手架,并给出可审查的输出分支。它处理繁重工作,让你专注于审查与解决冲突,而非重新应用代码。
默认情况下,最终结果会在专用输出分支上压缩为单个提交。若希望保留完整历史(不 squash),请使用 --show-commits。
何时使用它
在以下情况使用此命令:
- 希望迁移到更新的 Kubebuilder 版本或插件布局
- 希望在单独分支上审查脚手架变更
- 希望专注于解决合并冲突(而非重新应用自定义代码)
它是如何工作的
你告知工具新版本以及项目所在分支。它会重建两套脚手架,用三方合并将你的代码合并到新脚手架中,并给出可安全审查与合并的输出分支。你可以选择单个干净提交、完整历史,或自动推送到远程。
步骤 1:Detect versions
- 它会查看你的
PROJECT文件或你传入的标志。 - 通过读取
PROJECT文件中的cliVersion字段(若可用)确定你来自哪个旧版本。 - 确定你想要的新版本(默认为最新发行版)。
- 选择包含当前代码的分支(默认为
main)。
步骤 2:Create scaffolds
该命令会创建三个临时分支:
- 祖先:来自旧版本的干净项目脚手架。
- 原始:你当前代码的快照。
- 升级:来自新版本的干净脚手架。
步骤 3:Do a 3-way merge
- 使用 Git 的三方合并将原始(你的代码)合并到升级(新脚手架)中。
- 在引入上游变更的同时保留你的自定义内容。
- 如果发生冲突:
- 默认 → 停止并让你手动解决。
- 使用
--force→ 即使有冲突标记也继续并提交。(适合自动化)
- 运行
make manifests generate fmt vet lint-fix进行整理。
步骤 4:Write the output branch
- 默认情况下,所有内容会在安全输出分支上压缩为单个提交:
kubebuilder-update-from-<from-version>-to-<to-version>。 - 你可以更改行为:
--show-commits:保留完整历史。--restore-path:在压缩模式下,从基线分支恢复特定文件(如 CI 配置)。--output-branch:选择自定义分支名。--merge-message:自定义干净合并时的提交信息。--conflict-message:自定义发生冲突时的提交信息。--push:自动将结果推送到origin。--git-config: sets git configurations.--open-gh-issue:创建带有清单与对比链接的 GitHub issue(需要gh)。
步骤 5:Cleanup
- 输出分支就绪后,所有临时工作分支将被删除。
- 最终得到一个干净分支,可测试、审查并合并回主分支。
如何使用(命令)
在项目根目录运行:
kubebuilder alpha update
固定版本与基线分支:
kubebuilder alpha update \
--from-version v4.5.2 \
--to-version v4.6.0 \
--from-branch main
适合自动化(即使有冲突也继续):
kubebuilder alpha update --force
保留完整历史而非压缩:
kubebuilder alpha update --from-version v4.5.0 --to-version v4.7.0 --force --show-commits
默认压缩,但保留基线分支的 CI/工作流:
kubebuilder alpha update --force \
--restore-path .github/workflows \
--restore-path docs
使用自定义输出分支名:
kubebuilder alpha update --force \
--output-branch upgrade/kb-to-v4.7.0
运行更新并将结果推送到 origin:
kubebuilder alpha update --from-version v4.6.0 --to-version v4.7.0 --force --push
自定义提交信息:
kubebuilder alpha update --force \
--merge-message "chore: upgrade kubebuilder scaffold" \
--conflict-message "chore: upgrade with conflicts - manual review needed"
处理冲突(--force 与默认)
使用 --force 时,即使有冲突 Git 也会完成合并。提交将包含如下标记:
<<<<<<< HEAD
Your changes
=======
Incoming changes
>>>>>>> (original)
这允许你在 CI 或 cron 任务中运行该命令而无需人工干预。
- 不使用
--force:命令停在 merge 分支并打印指引;不创建提交。 - 使用
--force:合并会被提交(merge 或 output 分支)并包含标记。
修复冲突后,始终运行:
make manifests generate fmt vet lint-fix
# or
make all
与 GitHub Issues 一起使用(--open-gh-issue)
传入 --open-gh-issue 让命令在仓库中创建 GitHub Issue 以协助更新。
Examples
创建带有对比链接的 Issue:
kubebuilder alpha update --open-gh-issue
你将看到
该命令会打开一个链接到 diff 的 Issue,以便你创建 PR 并审查,例如:
Automation
这能与自动化良好集成。autoupdate.kubebuilder.io/v1-alpha 插件可生成按计划(例如每周)运行该命令的 GitHub Actions 工作流。当有新的 Kubebuilder 版本可用时,它会打开带有对比链接的 Issue,便于你创建 PR 并审查。
仅在运行期间更改额外 Git 配置(不修改你的 ~/.gitconfig)
默认情况下,kubebuilder alpha update 会应用安全的 Git 配置:merge.renameLimit=999999、diff.renameLimit=999999、merge.conflictStyle=merge。你可以追加更多配置,或禁用它们。
- 在默认值之上追加更多
kubebuilder alpha update \
--git-config rerere.enabled=true
- 完全禁用默认值
kubebuilder alpha update --git-config disable
- 禁用默认值并设置你自己的
kubebuilder alpha update \
--git-config disable \
--git-config rerere.enabled=true
旗帜
| Flag | 描述 |
|---|---|
--conflict-message | 存在冲突时合并的自定义提交信息。默认为 :warning: chore(kubebuilder): update scaffold (manual conflict resolution) <from> -> <to>。 |
--force | 即使发生合并冲突也继续。冲突文件会带着冲突标记被提交(适合 CI/cron)。 |
--from-branch | 保存当前项目代码的 Git 分支。默认为 main。 |
--from-version | 要更新自的 Kubebuilder 发行版(例如 v4.6.0)。未设置时尽可能从 PROJECT 文件读取。 |
--git-config | 可重复。以 -c key=value 传入每次调用的 Git 配置。默认(若省略):-c merge.renameLimit=999999 -c diff.renameLimit=999999。你的配置会叠加应用。要禁用默认配置,请包含 --git-config disable。 |
--merge-message | 成功合并(无冲突)的自定义提交信息。默认为 chore(kubebuilder): update scaffold <from> -> <to>。 |
--open-gh-issue | 更新完成后创建带有预填清单与对比链接的 GitHub issue(需要 gh)。 |
--output-branch | 输出分支名称。默认:kubebuilder-update-from-<from-version>-to-<to-version>。 |
--push | 更新完成后将输出分支推送到 origin 远程。 |
--restore-path | 可重复。压缩时要从基础分支保留的路径(例如 .github/workflows)。与 --show-commits 不兼容。 |
--show-commits | 保留完整历史(不压缩)。与 --restore-path 不兼容。 |
--to-version | 要更新到的 Kubebuilder 发行版(例如 v4.7.0)。未设置时默认为最新可用发行版。 |
-h, --help | 显示此命令的帮助。 |
Demonstration
进一步的资源
参考
-
使用最终器 最终器是一种机制,用于在资源从 Kubernetes 集群中删除之前执行与该资源相关的任何自定义逻辑。
-
监控资源 监控 Kubernetes 集群中的资源,以便获取信息并对变更采取行动。
-
什么是webhook? Webhooks是HTTP回调,在k8s中有三种类型的webhook:1)准入webhook 2)CRD转换webhook 3)授权webhook
- 接收Webhook 接收Webhook是在API服务器接收资源之前,对其进行变更或验证的HTTP回调。
-
您接受的训练数据截止至2023年10月。
生成 CRD(自定义资源定义)
Kubebuilder 使用一个名为 controller-gen 的工具来生成实用代码和 Kubernetes 对象的 YAML,比如自定义资源定义(CustomResourceDefinitions)。
为此,它使用特殊的“标记注释“(以 // + 开头的注释)来指示有关字段、类型和包的附加信息。在自定义资源定义(CRD)的情况下,这些信息通常来自于你的 _types.go 文件。有关标记的更多信息,请参见 标记参考文档。
Kubebuilder 提供了一个 make 目标来运行 controller-gen 并生成 CRD:make manifests。
当你运行 make manifests 时,应该可以在 config/crd/bases 目录下看到生成的 CRD。make manifests 还可以生成许多其他的构件—更多细节请查看 标记参考文档。
验证
CRD 支持使用 validation 部分中的 OpenAPI v3 架构 进行 声明式验证.
一般来说,验证标记可以附加到字段或类型上。如果您在定义复杂的验证、需要重用验证或需要验证切片元素,通常最好定义一个新类型来描述您的验证。
例如:
type ToySpec struct {
// +kubebuilder:validation:MaxLength=15
// +kubebuilder:validation:MinLength=1
Name string `json:"name,omitempty"`
// +kubebuilder:validation:MaxItems=500
// +kubebuilder:validation:MinItems=1
// +kubebuilder:validation:UniqueItems=true
Knights []string `json:"knights,omitempty"`
Alias Alias `json:"alias,omitempty"`
Rank Rank `json:"rank"`
}
// +kubebuilder:validation:Enum=狮子;狼;龙
type Alias string
// +kubebuilder:validation:最小值=1
// +kubebuilder:validation:最大值=3
// +kubebuilder:validation:排除最大值=false
type Rank int32
附加打印机列
从 Kubernetes 1.11 开始,kubectl get 可以向服务器请求显示哪些列。对于自定义资源定义(CRD),这可以用于提供有用的特定类型信息,类似于内置类型所提供的信息。
显示的信息可以通过您的 CRD 上的 additionalPrinterColumns 字段 进行控制,该字段由 CRD 的 Go 类型上的 +kubebuilder:printcolumn 标记控制。
例如,在以下示例中,我们添加字段以显示有关骑士的信息,以及来自验证示例的等级和别名字段:
// +kubebuilder:printcolumn:name="别名",type=string,JSONPath=`.spec.alias`
// +kubebuilder:printcolumn:name="等级",type=integer,JSONPath=`.spec.rank`
// +kubebuilder:printcolumn:name="勇敢逃跑",type=boolean,JSONPath=`.spec.knights[?(@ == "罗宾爵士")]`,description="当危险抬起丑陋的头颅时,他勇敢地转身逃跑",priority=10
// +kubebuilder:printcolumn:name="年龄",type="日期",JSONPath=".metadata.creationTimestamp"
type Toy struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec ToySpec `json:"spec,omitempty"`
Status ToyStatus `json:"status,omitempty"`
}
子资源
从 Kubernetes 1.13 开始,CRDs 可以选择实现 /status 和 /scale 子资源。
一般建议您在所有具有状态字段的资源中使用 /status 子资源。
两个子资源都有相应的 标记.
状态
通过 +kubebuilder:subresource:status 启用 status 子资源。启用后,对主资源的更新不会更改 status。同样,对 status 子资源的更新只能更改 status 字段。
例如:
// +kubebuilder:subresource:status
type Toy struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec ToySpec `json:"spec,omitempty"`
Status ToyStatus `json:"status,omitempty"`
}
规模
通过 +kubebuilder:subresource:scale 启用 scale 子资源。启用后,用户可以对你的资源使用 kubectl scale。若 selectorpath 参数指向标签选择器的字符串形式,HorizontalPodAutoscaler 即可自动扩缩你的资源。
例如:
type CustomSetSpec struct {
Replicas *int32 `json:"replicas"`
}
type CustomSetStatus struct {
Replicas int32 `json:"replicas"`
Selector string `json:"selector"` // 这必须是选择器的字符串形式
}
// +kubebuilder:subresource:status
// +kubebuilder:subresource:scale:specpath=.spec.replicas,statuspath=.status.replicas,selectorpath=.status.selector
type CustomSet struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec CustomSetSpec `json:"spec,omitempty"`
Status CustomSetStatus `json:"status,omitempty"`
}
多个版本
从 Kubernetes 1.13 开始,您可以在 CRD 中定义多个版本的 Kind,并使用一个 webhook 在它们之间进行转换。
有关此过程的更多详细信息,请参见 多版本教程。
默认情况下,Kubebuilder 禁用了为 CRD 中的 Kind 不同版本生成不同的验证,以便与旧版本的 Kubernetes 兼容。
如果使用 v1beta CRD,您需要将 makefile 中的 CRD_OPTIONS ?= "crd:trivialVersions=true,preserveUnknownFields=false 这一行更改为 CRD_OPTIONS ?= crd:preserveUnknownFields=false;如果使用 v1(推荐),则更改为 CRD_OPTIONS ?= crd。
然后,您可以使用 +kubebuilder:storageversion 标记 来指示 API 服务器应该使用的用于存储数据的 GVK。
在内部实现上
Kubebuilder 自动生成用于运行 controller-gen 的 make 规则。该规则会使用 Go 模块通过 go install 自动安装 controller-gen,如果它不在您的路径中。
如果你想查看 controller-gen 的具体操作,也可以直接运行它。
每个 controller-gen 的“生成器“都由 controller-gen 的一个选项控制,使用与标记相同的语法。controller-gen 还支持不同的输出“规则“,以控制输出的方式和去向。请注意 manifests 制作规则(略微简化,仅生成 CRD):
# 为 CRD 生成清单
manifests: controller-gen
$(CONTROLLER_GEN) rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
它使用 output:crd:artifacts 输出规则来指示与 CRD 相关的配置(非代码)工件应最终放在 config/crd/bases 中,而不是 config/crd。
要查看包括生成器在内的所有 controller-gen 选项,请运行
$ controller-gen -h
或者,欲了解更多详情:
$ controller-gen -hhh
使用终结器
Finalizers 允许控制器实现异步删除前钩子。若为 API 类型的每个对象创建外部资源(如存储桶),并希望在从 Kubernetes 删除对象时删除关联的外部资源,请使用 Finalizer。
您可以在Kubernetes参考文档中阅读更多关于最终器的信息。下面的部分演示了如何在控制器的Reconcile方法中注册和触发预删除钩子。
需要注意的关键点是,最终处理器导致对象的“删除“变成“更新“,以设置删除时间戳。对象上存在删除时间戳表明该对象正在被删除。否则,如果没有最终处理器,删除会表现为一个调整过程,其中对象在缓存中丢失。
亮点:
- 如果对象没有被删除且没有注册终结器,则在 Kubernetes 中添加终结器并更新该对象。
- 如果对象正在被删除,并且最终器仍然在最终器列表中,那么请执行预删除逻辑,移除最终器并更新对象。
- 确保预删除逻辑是幂等的。
Apache License
Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
Imports
先从标准导入开始。与之前一样,需要核心 controller-runtime 库、client 包,以及 API 类型包。
package controllers
import (
"context"
"k8s.io/kubernetes/pkg/apis/batch"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
batchv1 "tutorial.kubebuilder.io/project/api/v1"
)
默认情况下,kubebuilder 会包含更新 CronJob Finalizer 所需的 RBAC 规则。
// +kubebuilder:rbac:groups=batch.tutorial.kubebuilder.io,resources=cronjobs,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=batch.tutorial.kubebuilder.io,resources=cronjobs/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=batch.tutorial.kubebuilder.io,resources=cronjobs/finalizers,verbs=update
下面的代码片段展示了实现 Finalizer 的骨架代码。
func (r *CronJobReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := r.Log.WithValues("cronjob", req.NamespacedName)
cronJob := &batchv1.CronJob{}
if err := r.Get(ctx, req.NamespacedName, cronJob); err != nil {
log.Error(err, "unable to fetch CronJob")
// we'll ignore not-found errors, since they can't be fixed by an immediate
// requeue (we'll need to wait for a new notification), and we can get them
// on deleted requests.
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// name of our custom finalizer
myFinalizerName := "batch.tutorial.kubebuilder.io/finalizer"
// examine DeletionTimestamp to determine if object is under deletion
if cronJob.ObjectMeta.DeletionTimestamp.IsZero() {
// The object is not being deleted, so if it does not have our finalizer,
// then let's add the finalizer and update the object. This is equivalent
// to registering our finalizer.
if !controllerutil.ContainsFinalizer(cronJob, myFinalizerName) {
controllerutil.AddFinalizer(cronJob, myFinalizerName)
if err := r.Update(ctx, cronJob); err != nil {
return ctrl.Result{}, err
}
}
} else {
// The object is being deleted
if controllerutil.ContainsFinalizer(cronJob, myFinalizerName) {
// our finalizer is present, so let's handle any external dependency
if err := r.deleteExternalResources(cronJob); err != nil {
// if fail to delete the external dependency here, return with error
// so that it can be retried.
return ctrl.Result{}, err
}
// remove our finalizer from the list and update it.
controllerutil.RemoveFinalizer(cronJob, myFinalizerName)
if err := r.Update(ctx, cronJob); err != nil {
return ctrl.Result{}, err
}
}
// Stop reconciliation as the item is being deleted
return ctrl.Result{}, nil
}
// Your reconcile logic
return ctrl.Result{}, nil
}
func (r *Reconciler) deleteExternalResources(cronJob *batch.CronJob) error {
//
// delete any external resources associated with the cronJob
//
// Ensure that delete implementation is idempotent and safe to invoke
// multiple times for same object.
}
良好实践
Operator 中的“协调(reconciliation)”是什么?
当你使用 Kubebuilder 创建一个项目时,可以查看在 cmd/main.go 中生成的代码。这段代码初始化了一个 Manager,项目依赖于 controller-runtime 框架。这个 Manager 管理着 Controllers,提供了一种调整功能,用于在集群内同步资源直到达到所需状态。
调整是一个持续的循环,执行必要的操作以保持期望的状态,遵循Kubernetes的原则,如控制循环。有关更多信息,请查阅Kubernetes的Operator模式文档,以更好地理解这些概念。
为什么重调应该是幂等的?
在开发运算符时,控制器的对账循环需要是幂等的。通过遵循 运算符模式,我们创建 控制器,这些控制器提供一个对账函数,负责在集群上同步资源,直到达到期望状态。开发幂等的解决方案将允许对账器正确响应通用或意外事件,轻松处理应用程序的启动或升级。更多相关解释可以参考 这里。
根据特定事件编写调整逻辑,会违反 Operator 模式的建议,并且违背了controller-runtime的设计原则。这可能导致不可预见的后果,例如资源被卡住,需要人工干预。
理解 Kubernetes API 和遵循 API 约定
构建操作器通常涉及扩展 Kubernetes API 本身。了解自定义资源定义(CRDs)如何与 Kubernetes API 交互是很有帮助的。此外,关于分组、版本和种类的 Kubebuilder 文档 也可能有助于更好地理解这些与操作器相关的概念。
此外,我们建议您查看 Kubernetes 上的 Operator 模式 文档,以更好地理解使用 KubeBuilder 构建的标准解决方案的目的。
为什么您应该遵循 Kubernetes API 的约定和标准
遵循 Kubernetes API 规范和标准 对于最大化应用程序和部署的潜力至关重要。通过遵循这些既定实践,您可以在多个方面受益。
首先,遵循标准确保了Kubernetes生态系统内的无缝互操作性。遵循约定使您的应用程序能够与其他组件和谐工作,从而减少兼容性问题并促进一致的用户体验。
其次,遵循API标准可以提高应用程序的可维护性和故障排除能力。采用熟悉的模式和结构使得调试和支持您的部署变得更加容易,从而提高了运营效率和快速问题解决的能力。
此外,利用Kubernetes API的惯例可以让您充分发挥平台的全部功能。通过在定义的框架内工作,您可以利用Kubernetes提供的丰富特性和资源,从而实现可扩展性、性能优化和弹性。
最后,遵循这些标准可以使您的本土解决方案具备未来适应能力。通过与不断发展的Kubernetes生态系统保持一致,您可以确保与未来更新、新功能以及活跃的Kubernetes社区引入的增强功能保持兼容。
总之,通过遵循Kubernetes API的约定和标准,您可以实现无缝集成、简化维护、优化性能和为未来做好准备的潜力,这些都为您的应用程序和部署的成功做出了贡献。
为什么应避免由单个 controller 负责管理多个 CRD(Custom Resource Definitions)的系统设计(例如 ‘install_all_controller.go’)?
避免采用一种设计方案,其中同一个控制器对账多个 Kind。将多个 Kind(如 CRD)都由同一个控制器管理,通常与 controller-runtime 提出的设计相悖。此外,这可能会损害封装性、单一职责原则和内聚性等概念。损害这些概念可能会导致意想不到的副作用,并增加扩展、重用或维护操作器的难度。在一个操作器中,由一个控制器管理多个自定义资源(CRs)可能会导致几个问题:
- 复杂性:单个控制器管理多个自定义资源(CR)可能会增加代码的复杂性,从而使其更难理解、维护和调试。
- 可扩展性:每个控制器通常管理一种类型的自定义资源(CR)以实现可扩展性。如果一个控制器处理多个CR,它可能成为瓶颈,从而降低系统的整体效率和响应速度。
- 单一职责原则:遵循软件工程中的这一原则,理想情况下每个控制器应该只有一个职责。这种方法简化了开发和调试,并使系统更加健壮。
- 错误隔离:如果一个控制器管理多个自定义资源(CR),并且发生错误,可能会影响它管理的所有 CR。为每个 CR 配置一个单独的控制器可以确保一个控制器或 CR 的问题不会直接影响其他资源。
- 并发与同步:一个单一的控制器管理多个自定义资源(CR)可能导致竞争条件,并需要复杂的同步,特别是当这些CR之间存在相互依赖时。
总之,虽然让一个控制器管理多个CR看起来效率较高,但这通常会导致更高的复杂性、较低的可扩展性以及潜在的稳定性问题。通常遵循单一职责原则更为合适,每个CR由其自己的控制器管理。
为什么你应该采用状态条件
按照 K8s API 约定 使用 Status Conditionals 管理你的方案,因为:
- 标准化:条件提供了一种标准化的方式来表示操作员自定义资源的状态,使用户和工具更容易理解和解释资源的状态。
- 可读性:通过使用多个条件的组合,条件可以清晰地表达复杂的状态,使用户更容易理解资源的当前状态和进展。
- 可扩展性:当新的功能或状态被添加到您的 Operator 时,可以轻松扩展条件以表示这些新状态,而无需对现有的 API 或结构进行重大更改。
- 可观察性:集群管理员和外部监控工具可以监视和跟踪状态条件,从而更好地了解由操作员管理的自定义资源的状态。
- 兼容性:通过采用在 Kubernetes API 中使用条件的常见模式,Operator 作者确保他们的自定义资源与更广泛的生态系统保持一致,这有助于用户在与集群中的多个 Operator 和资源交互时获得一致的体验。
你应采用 Kubernetes 的可观测与埋点约定
正确的日志记录对 Kubernetes 原生应用的可观测性至关重要。但需要根据代码上下文理解应采用哪套日志约定。
理解 Go 与 Kubernetes 日志约定的差异
使用 Go 开发时,你可能熟悉 Go Code Review Comments 指南:错误字符串不应大写开头,也不应以标点结尾。这些约定面向常被组合进更大上下文的错误消息:
// Go conventions (for general Go code, libraries, CLI tools)
return fmt.Errorf("something bad happened") // lowercase, no period
log.Printf("failed to connect: %v", err) // lowercase
但是,在开发运行于集群上的 Kubernetes 原生方案(controller、operator、webhook)时,应遵循 Kubernetes 日志约定,以获得更好的可观测性并与 Kubernetes 生态保持一致。
Kubernetes 日志约定
对于控制器、Operator 和 Webhook,遵循以下指南:
- 以大写字母开头。
- 不要以句号结束消息。
- 使用主动语态。有动作主体时使用完整句子(「A could not do B」);若主体就是程序本身则可省略主体(「Could not do B」)。
- 使用过去时(用 “Could not delete B” 而非 “Cannot delete B”)
- 提及对象时,说明对象类型。(用 “Deleted Pod” 而非 “Deleted”)
- 使用带有成对键值的结构化日志。
Examples:
// Kubernetes conventions (for controllers, operators, webhooks)
log.Info("Starting reconciliation") // Capital letter, no period
log.Info("Creating Deployment", "name", name, "namespace", ns) // Specify object type, structured logging
log.Info("Created Deployment", "name", deploy.Name) // Past tense, specify type
log.Error(err, "Failed to create Pod", "name", name) // Past tense, specify type
log.Info("Deployment could not create Pod", "deployment", name) // Acting subject
log.Info("Could not delete Pod", "name", name) // Subject is the program itself
为何使用不同约定?
- Go 约定针对会被组合进更大上下文、并与其他文本内联显示的错误消息进行了优化
- Kubernetes 约定针对分布式系统中的结构化日志进行了优化,其中日志:
- 从集群中的多个组件聚合而来
- 由日志收集器解析(Fluentd、Fluentbit、Loki 等)
- 显示在监控仪表盘与 UI 中
- 用于生产环境中的告警与故障排查
遵循这些约定可确保日志与 Kubernetes 可观测性工具无缝集成,并为集群运维人员和 SRE 提供清晰、可操作的信息。
(Alpha)Server-Side Apply
--ssa 标志会脚手架生成带 Server-Side Apply 支持的 API,使多个参与者修改同一资源时字段管理更安全。
这将添加:
- 用于生成 ApplyConfiguration 的 API 标记(
+genclient、+kubebuilder:ac:generate=true) - Makefile 集成,在生成 CRD 与 RBAC 清单的同时生成类型安全的 ApplyConfiguration 类型
何时使用它
在以下情况使用 Server-Side Apply:
- 多个控制器或用户管理同一资源
- 用户用标签、注解或你的控制器不应覆盖的 spec 字段自定义 CR
- 你需要声明式字段所有权跟踪
- 其他 Operator 将管理你的 CR 实例(它们可导入你生成的 ApplyConfiguration 类型)
它是如何工作的
传统的 Update() 会覆盖整个对象。使用客户端 Apply 方法的 Server-Side Apply 只管理你指定的字段。
运行 make manifests 后,ApplyConfiguration 类型创建于:
api/<version>/applyconfiguration/api/<version>/ (single group)
api/<group>/<version>/applyconfiguration/<group>/<version>/ (multi-group)
导入方式如下(多 group 项目使用 example.com/myproject/api/<group>/v1/applyconfiguration/<group>/v1):
appsv1apply "example.com/myproject/api/v1/applyconfiguration/api/v1"
混合使用 SSA 与非 SSA API
groupversion_info.go 中的 +kubebuilder:ac:generate=true marker 是包级的:它为该 group/version 启用 ApplyConfiguration 生成。每个 kind 再用自己的 +kubebuilder:ac:generate marker 选择加入或排除:用 --ssa 脚手架的 kind 为 =true,未使用的为 =false。当你用 --ssa 创建 API 时,该 group/version 中此前脚手架且没有此 marker 的 kind 会自动获得它。
要更改生成哪些 kind:
- 在 kind 上方设置
+kubebuilder:ac:generate=true以包含它 - 在 kind 上方设置
+kubebuilder:ac:generate=false以排除它
Usage
使用 --ssa 标志创建 API:
kubebuilder create api --group apps --version v1 --kind Application --ssa
在控制器中实现 Server-Side Apply:
import (
"context"
metav1apply "k8s.io/client-go/applyconfigurations/meta/v1"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
appsv1apply "example.com/myproject/api/v1/applyconfiguration/api/v1"
)
func (r *ApplicationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
// Build desired state - specify only fields you manage
app := appsv1apply.Application(req.Name, req.Namespace).
WithStatus(appsv1apply.ApplicationStatus().
WithConditions(metav1apply.Condition().
WithType("Available").
WithStatus("True").
WithReason("Reconciled")))
// Apply status, forcing ownership of the fields this controller manages
if err := r.SubResource("status").Apply(ctx, app,
client.FieldOwner("application-controller"), client.ForceOwnership); err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
然后运行:
make generate manifests
最佳实践
- 始终指定
client.FieldOwner("controller-name")以标识你的控制器 - Controller 应对所管理的字段传入
client.ForceOwnership;Kubernetes 建议 controller 始终强制解决冲突 - 使用 builder 模式仅指定你的控制器所管理的字段
其他资源
许可证头
这是什么?
Kubebuilder 会在 hack/boilerplate.go.txt 创建包含许可证头的文件。该头会自动添加到所有生成的 Go 文件中。
样板文件由以下使用:
- Kubebuilder 在创建新文件时(
create api、create webhook等) controller-genwhen generating code (DeepCopy methods, etc.)make generateandmake manifestscommands
默认情况下,Kubebuilder 使用 Apache 2.0 许可证。
如何自定义
选项 1:Use built-in licenses
在初始化时或更新现有项目时使用 --license 标志:
# During initialization
kubebuilder init --domain example.com --license apache2 --owner "Your Name"
# After initialization (update existing project)
kubebuilder edit --license apache2 --owner "Your Company"
可用的许可证值:
apache2- Apache 2.0 License (default)copyright- Copyright notice only (no license text)none- No license header
选项 2:Use a custom license file
从文件提供你自己的许可证头。Kubebuilder 会读取文件内容并复制到 hack/boilerplate.go.txt:
# During initialization
kubebuilder init --domain example.com --license-file ./my-header.txt
# After initialization
kubebuilder edit --license-file ./my-header.txt
工作原理:
- Kubebuilder 读取你的自定义许可证文件(可为任意路径)
- 将内容复制到
hack/boilerplate.go.txt
在以下情况使用:
- 内置模板中没有的许可证
- 特定公司许可证格式
- 自定义版权声明
选项 3:Edit the file directly
初始化后,你可以直接编辑 hack/boilerplate.go.txt:
vim hack/boilerplate.go.txt
自定义许可证文件格式
你的自定义许可证文件必须包含 Go 注释定界符(/* 和 */)。
示例(my-license-header.txt):
/*
Copyright YEAR Your Company Name.
Licensed under the MIT License.
See LICENSE file in the project root for full license text.
*/
将许可证变更应用到现有文件
更新 hack/boilerplate.go.txt 后,选择如何应用变更:
重新生成已生成的代码(推荐):
make generate # Update DeepCopy methods
make manifests # Update CRDs, RBAC, webhooks
这会更新生成的代码,但保持手写文件不变。
重新生成整个项目:
kubebuilder alpha generate
仅新文件: 新文件将自动使用更新后的模板:
kubebuilder create api --group myapp --version v1 --kind MyNewKind
手动更新: 对于手写文件,以 hack/boilerplate.go.txt 为参考手动更新页眉。
如何使用
生成或重新生成文件时会使用 hack/boilerplate.go.txt:
kubebuilder create api与create webhook- 新文件make generate- DeepCopy methods, etc.make manifests- CRDs, RBAC, webhookskubebuilder alpha generate- entire project
所有工具自动引用 hack/boilerplate.go.txt。Makefile 已配置好——无需更改。
Examples
示例 1:Apache 2.0 许可证(默认)
使用内置的 Apache 2.0 许可证:
kubebuilder init --domain example.com --license apache2 --owner "Your Company"
这将生成:
/*
版权 2025 The Kubernetes authors.
根据 Apache 许可证,版本 2.0("许可证")授权;
您只能在遵守该许可证的情况下使用此文件。
您可以在以下地址获得该许可证的副本:
http://www.apache.org/licenses/LICENSE-2.0
除非适用法律要求或书面同意,否则根据该许可证分发的软件
是按"原样"基础分发的,
不提供任何形式的担保或条件,无论是明示还是否明示。
有关许可证下的具体权限和限制,请参见许可证。
*/
示例 2:仅版权声明
使用内置的 copyright 许可证仅生成版权声明:
kubebuilder init --domain example.com --license copyright --owner "Your Company"
这将生成:
/*
Copyright YEAR Your Company.
*/
示例 3:自定义许可证(MIT)
对于非内置许可证,创建自定义文件 mit-header.txt:
/*
Copyright YEAR Your Name.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
然后初始化你的项目:
kubebuilder init --domain example.com --license-file ./mit-header.txt
会发生什么:
- Kubebuilder 从
./mit-header.txt读取内容 - 用该内容创建
hack/boilerplate.go.txt hack/boilerplate.go.txt成为所有生成文件的权威来源
Related
- 良好实践 - 项目结构最佳实践
- 生成 CRD - 如何带页眉生成 CRD
- controller-gen CLI - 代码生成详情
创建活动
在控制器的 Reconcile 函数中发布 Event 对象通常是非常有用的,因为它们允许用户或任何自动化过程了解特定对象的状态并做出相应的响应。
可以通过运行 $ kubectl describe <资源类型> <资源名称> 来查看对象的最近事件。也可以通过运行 $ kubectl get events 来检查事件。
观看资源
在扩展 Kubernetes API 时,我们的目标是确保我们的解决方案与 Kubernetes 本身的行为一致。例如,考虑一个由控制器管理的 Deployment 资源。这个控制器负责响应集群中的变化——例如,当 Deployment 被创建、更新或删除时——通过触发调整以确保资源的状态与期望状态相匹配。
同样,在开发 controller 时,请监视对你的方案至关重要的资源上的相关变更。这些变更——无论是创建、更新还是删除——都应触发对账循环以采取适当操作,并在集群中保持一致性。
controller-runtime 库提供了多种方式来监视和管理资源。
主要资源
主要资源是你的控制器负责管理的资源。例如,如果你为 MyApp 创建了一个自定义资源定义(CRD),那么相应的控制器负责管理 MyApp 的实例。
在这种情况下,MyApp 是该控制器的 主资源,您的控制器的调整循环专注于确保这些主资源的期望状态得以维护。
当你使用 Kubebuilder 创建一个新的 API 时,以下默认代码会被生成,确保控制器监视所有相关事件——例如创建、更新和删除——以便于新的 API。
此设置确保每当你创建、更新或删除 API 实例时,系统都会触发对账循环:
// 监视主要资源(例如,MyApp)的创建、更新、删除事件
if err := ctrl.NewControllerManagedBy(mgr).
For(&<YourAPISpec>{}). <-- See there that the Controller is For this API
Complete(r); err != nil {
return err
}
次要资源
您的控制器可能还需要管理辅助资源,这是集群中支持主要资源所需的资源。
对这些次级资源的更改可能会直接影响主要资源,因此控制者必须相应地监控和调整这些资源。
由控制者拥有的。
这些辅助资源,例如 Services、ConfigMaps 或 Deployments,在被控制器拥有时,由特定控制器创建和管理,并通过 OwnerReferences 与主资源相关联。
例如,如果我们在集群中有一个控制器来管理 MyApp 类型的自定义资源(CR),这代表了我们的应用解决方案,那么确保 MyApp 以所需实例数正常运行所需的所有资源将被称为 次要资源。负责创建、删除和更新这些资源的代码将是 MyApp 控制器的一部分。我们将使用 controllerutil.SetControllerReference 函数添加适当的 OwnerReferences 以指示这些资源由相同的控制器所拥有,该控制器负责管理 MyApp 实例,这些实例将由 MyAppReconciler 进行调整。
此外,如果删除了主要资源,Kubernetes 的垃圾回收机制将确保所有关联的次要资源以层级方式自动删除。
哪些不是由控制器“拥有“的?
请注意,辅助资源可以是您项目中定义的 API/CRD,也可以是与 主要资源 相关的其他项目中的 API/CRD,但特定控制器并不负责创建或管理这些资源。
例如,如果我们有一个表示备份解决方案(即 MyBackup)的自定义资源定义(CRD),用于我们的 MyApp,它可能需要监视 MyApp 资源的变化,以便在 MyBackup 中触发对账,确保达到期望的状态。同样,MyApp 的行为也可能受到其他项目中定义的 CRD/API 的影响。
在这两种情况下,这些资源都被视为次级资源,即使它们不是由MyAppController“拥有”(即没有被创建或管理)。
在 Kubebuilder 中,未在项目中定义且不是 核心类型(即未在 Kubernetes API 中定义的类型)的资源称为 外部类型。
外部类型是指在您的项目中未定义但您需要监视和响应的资源。例如,如果操作员 A管理一个用于应用程序部署的 MyApp CRD,而操作员 B处理备份,那么操作员 B可以将 MyApp CRD 视为外部类型,以便根据 MyApp 的变化触发备份操作。
在此场景中,Operator B 可以定义依赖 MyApp 状态的 BackupConfig CRD。通过将 MyApp 视为次要资源(Secondary Resource),Operator B 可以监视并对账 Operator A 的 MyApp 变更,确保你在更新或扩缩 MyApp 时 controller 启动备份流程。
观看资源的一般概念
无论资源是在您的项目内定义的还是来自外部项目,主要资源和次要资源的概念始终是相同的:
- 主要资源是控制器主要负责管理的资源。
- 次级资源是指为了确保主要资源按预期工作而必需的资源。
因此,无论资源由你的项目还是其他项目定义,你的 controller 都可以按需监视、对账并管理这些资源的变更。
观看二级资源有什么重要性?
在构建 Kubernetes 控制器时,关注 主要资源 是至关重要的,但同样重要的是监控 次要资源。未能跟踪这些资源可能导致您的控制器行为和整个集群状态的不一致。
次级资源可能并不直接由您的控制器管理,但对这些资源的更改仍然会对主资源和您的控制器的功能产生重大影响。以下是关注这些资源的重要原因:
-
确保一致性:
- 二级资源(例如,子对象或外部依赖)可能与其预期状态发生偏离。例如,二级资源可能被修改或删除,从而导致系统不同步。
- 观察辅助资源确保任何变化都能立即被检测到,这使得控制器能够对账并恢复所需状态。
-
避免随机自我修复:
- 在没有查看二级资源的情况下,控制器可能只在重启后或在触发特定事件时“自我恢复“。这可能导致对问题的反应不可预测或延迟。
- 监控二级资源可以确保及时处理不一致性,而不是等待控制器重启或外部事件来触发调整。
-
有效的生命周期管理:
- 次要资源可能并不直接由控制者拥有,但它们的状态仍然会影响主要资源的行为。如果不对这些进行监控,就可能导致孤儿资源或过时资源的存在。
- 观看非拥有的二级资源使控制器能够对可能影响主资源的生命周期事件(创建、更新、删除)作出响应,从而确保系统行为的一致性。
请参阅观看未拥有的次级资源以获取示例。
为什么不在所有场景中都使用 RequeueAfter X 而不是监听资源呢?
Kubernetes 控制器本质上是 事件驱动 的。在创建控制器时,调整循环 通常是由对资源的 create、update 或 delete 操作等 事件 触发的。这种事件驱动的方法相比于不断重新排队或轮询资源使用 RequeueAfter 更加高效和响应迅速。这确保了系统只在必要时采取行动,从而保持性能和效率。
在许多情况下,监视资源是确保Kubernetes资源保持在所需状态的首选方法。它更高效、更灵敏,并且与Kubernetes的事件驱动架构相一致。然而,在某些场景中,RequeueAfter是合适且必要的,特别是在管理不发出事件的外部系统或处理需要时间才能收敛的资源时,例如长时间运行的过程。仅依赖RequeueAfter处理所有场景可能导致不必要的开销和反应延迟。因此,至关重要的是优先考虑事件驱动的调整,通过在可能的情况下配置控制器以监视资源,并将RequeueAfter保留用于需要定期检查的情况。
RequeueAfter X 何时有用
虽然 RequeueAfter 不是触发调整的主要方法,但在某些特定情况下是必要的,例如:
- 观察外部系统:在处理不生成事件的外部资源(例如,外部数据库或第三方服务)时,
RequeueAfter允许控制器定期检查这些资源的状态。 - 基于时间的操作:一些任务,例如旋转密钥或更新证书,必须在特定的时间间隔内执行。
RequeueAfter确保这些操作按计划进行,即使没有其他更改发生。 - 处理错误或延迟:在管理遇到错误或需要时间自我修复的资源时,
RequeueAfter确保控制器在再次检查资源状态之前等待指定的时间,从而避免不断尝试进行调整。
谓词的使用
对于更复杂的使用场景,可以使用 谓词 来精确调整您的控制器何时应该触发状态同步。谓词允许您根据特定条件筛选事件,例如对特定字段、标签或注释的更改,从而确保您的控制器仅对相关事件做出响应,并高效地运行。
查看控制器“拥有“的二级资源
在 Kubernetes 控制器中,通常会同时管理 主资源 和 次资源。主资源 是控制器负责的主要资源,而 次资源 是由控制器创建和管理,以支持 主资源。
本节说明如何管理由 controller Owned 的次要资源(Secondary Resources)。本示例展示如何:
- 在主要资源(
Busybox)和次要资源(Deployment)之间设置所有者引用,以确保正确的生命周期管理。 - 将控制器配置为在
SetupWithManager()中使用Owns()监视次要资源。可以看到Deployment由Busybox控制器拥有,因为它将由该控制器创建和管理。
设置拥有者引用
要将次级资源(Deployment)的生命周期与主资源(Busybox)连接起来,我们需要在次级资源上设置一个 所有者引用。这确保了 Kubernetes 会自动处理层叠删除:如果主资源被删除,次级资源也会被删除。
Controller-runtime 提供了 controllerutil.SetControllerReference 函数,您可以使用它来设置资源之间的关系。
设置拥有者引用
下面,我们创建 Deployment 并使用 controllerutil.SetControllerReference() 在 Busybox 自定义资源和 Deployment 之间设置 Owner 引用。
// deploymentForBusybox 返回一个 Busybox 的 Deployment 对象
func (r *BusyboxReconciler) deploymentForBusybox(busybox *examplecomv1alpha1.Busybox) *appsv1.Deployment {
replicas := busybox.Spec.Size
dep := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: busybox.Name,
Namespace: busybox.Namespace,
},
Spec: appsv1.DeploymentSpec{
Replicas: &replicas,
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{"app": busybox.Name},
},
Template: metav1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{"app": busybox.Name},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "busybox",
Image: "busybox:latest",
},
},
},
},
},
}
// 设置 Deployment 的 ownerRef,确保在删除 Busybox CR 时,Deployment 也会被删除。
controllerutil.SetControllerReference(busybox, dep, r.Scheme)
return dep
}
Explanation
通过设置 OwnerReference,如果 Busybox 资源被删除,Kubernetes 会自动删除 Deployment。这还允许控制器监视 Deployment 的变化,并确保维护所需的状态(例如副本数)。
例如,如果有人修改了 Deployment,将副本数更改为 3,而 Busybox CR 定义的期望状态为 1 个副本,控制器将进行调整,确保 Deployment 缩减回 1 个副本。
对账函数示例
// Reconcile 处理 Busybox 和 Deployment 的主要调整循环。
func (r *BusyboxReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := logf.FromContext(ctx)
// 获取 Busybox 实例
busybox := &examplecomv1alpha1.Busybox{}
if err := r.Get(ctx, req.NamespacedName, busybox); err != nil {
if apierrors.IsNotFound(err) {
log.Info("Busybox resource not found. Ignoring since it must be deleted")
return ctrl.Result{}, nil
}
log.Error(err, "Failed to get Busybox")
return ctrl.Result{}, err
}
// 检查部署是否已经存在,如果不存在则创建一个新的。
found := &appsv1.Deployment{}
err := r.Get(ctx, types.NamespacedName{Name: busybox.Name, Namespace: busybox.Namespace}, found)
if err != nil && apierrors.IsNotFound(err) {
// 定义一个新的部署
dep := r.deploymentForBusybox(busybox)
log.Info("Creating a new Deployment", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
if err := r.Create(ctx, dep); err != nil {
log.Error(err, "无法创建新的部署", "Deployment.Namespace", dep.Namespace, "Deployment.Name", dep.Name)
return ctrl.Result{}, err
}
// 重新排队请求以确保创建部署
return ctrl.Result{RequeueAfter: time.Minute}, nil
} else if err != nil {
log.Error(err, "Failed to get Deployment")
return ctrl.Result{}, err
}
// 确保部署大小与期望状态匹配
size := busybox.Spec.Size
if *found.Spec.Replicas != size {
found.Spec.Replicas = &size
if err := r.Update(ctx, found); err != nil {
log.Error(err, "Failed to update Deployment size", "Deployment.Namespace", found.Namespace, "Deployment.Name", found.Name)
return ctrl.Result{}, err
}
// 重新排队请求以确保达到正确的状态
return ctrl.Result{RequeueAfter: time.Minute}, nil
}
// 更新 Busybox 状态以反映该部署可用
busybox.Status.AvailableReplicas = found.Status.AvailableReplicas
if err := r.Status().Update(ctx, busybox); err != nil {
log.Error(err, "Failed to update Busybox status")
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
观看辅助资源
为了确保对次要资源(例如 Deployment)的更改能够触发对主要资源(Busybox)的对账,我们配置控制器以监视这两个资源。
Owns() 方法允许您指定控制器应该监控的辅助资源。这样,控制器将在辅助资源发生变化(例如,被更新或删除)时自动调整主资源。
示例:配置 SetupWithManager 以监视辅助资源
// SetupWithManager 使用 Manager 设置控制器。
// 控制器将同时监视 Busybox 主资源和 Deployment 次资源。
func (r *BusyboxReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&examplecomv1alpha1.Busybox{}). // 观看主要资源
Owns(&appsv1.Deployment{}). // 监视次要资源(部署)
Complete(r)
}
确保正确的权限
Kubebuilder 使用 标记 来定义控制器所需的 RBAC 权限。为了使控制器能够正确监视和管理主资源(Busybox)和次资源(Deployment),必须授予其适当的权限;即对这些资源具有 watch、get、list、create、update 和 delete 权限。
示例:RBAC 标记
在 Reconcile 方法之前,我们需要定义适当的 RBAC 标记。这些标记将由 controller-gen 在您运行 make manifests 时,用于生成所需的角色和权限。
// +kubebuilder:rbac:groups=example.com,resources=busyboxes,verbs=获取;列表;观察;创建;更新;补丁;删除
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=获取;列表;观察;创建;更新;补丁;删除
- 第一个标记授予控制器管理
Busybox自定义资源(主要资源)的权限。 - 第二个标记授予控制器管理
Deployment资源(次要资源)的权限。
请注意,我们正在授予对资源的 watch 权限。
观看未“拥有“的辅助资源
在某些情况下,控制器可能需要监视并响应它并不“拥有“的资源的变化,这意味着这些资源是由另一个控制器创建和管理的。
以下示例演示了控制器如何监控和对账它不直接管理的资源。这适用于任何不是由控制器“拥有“的资源,包括由其他控制器或项目管理的核心类型或自定义资源,并在单独的流程中进行对账。
例如,考虑两个自定义资源——Busybox 和 BackupBusybox。如果对 Busybox 的更改应该触发 BackupBusybox 控制器的调整,我们可以配置 BackupBusybox 控制器来监视 Busybox 的更新。
示例:监视未拥有的 Busybox 资源以重新调整 BackupBusybox
考虑一个控制器,它管理一个自定义资源 BackupBusybox,同时需要监控整个集群中 Busybox 资源的变化。我们只希望在 Busybox 实例启用备份功能时触发对其的调整。
- 为什么观看二次资源?
BackupBusybox控制器并不负责创建或拥有Busybox资源,但这些资源的变化(例如更新或删除)会直接影响主要资源(BackupBusybox)。- 通过监视具有特定标签的
Busybox实例,控制器确保只有相关资源触发必要的操作(例如备份)。
配置示例
以下是如何配置 BackupBusyboxReconciler 以监视 Busybox 资源的更改并触发 BackupBusybox 的对账:
// SetupWithManager 用于设置控制器与管理器。
// 控制器将监视 BackupBusybox 主要资源和 Busybox 资源。
func (r *BackupBusyboxReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&examplecomv1alpha1.BackupBusybox{}). // 观察主要资源(BackupBusybox)
Watches(
&examplecomv1alpha1.Busybox{}, // 观察 Busybox CR
handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request {
// 在同一命名空间中触发 BackupBusybox 的同步
return []reconcile.Request{
{
NamespacedName: types.NamespacedName{
Name: "backupbusybox", // 对关联的 BackupBusybox 资源进行调整
Namespace: obj.GetNamespace(), // 使用已更改的 Busybox 命名空间
},
},
}
}),
). // 当 Busybox 资源发生变化时触发调整。
Complete(r)
}
以下是我们如何配置控制器,以仅过滤和监视具有特定标签的 Busybox 资源的变化:
// SetupWithManager 用于将控制器与管理器进行设置。
// 控制器将监视 BackupBusybox 主要资源和 Busybox 资源,并按标签进行过滤。
func (r *BackupBusyboxReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&examplecomv1alpha1.BackupBusybox{}). // 观察主要资源(BackupBusybox)
Watches(
&examplecomv1alpha1.Busybox{}, // 观察 Busybox CR
handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request {
// 检查 Busybox 资源是否有标签 'backup-needed: "true"'
if val, ok := obj.GetLabels()["backup-enable"]; ok && val == "true" {
// 如果标签存在并设置为"true",则触发 BackupBusybox 的对账
return []reconcile.Request{
{
NamespacedName: types.NamespacedName{
Name: "backupbusybox", // 对关联的 BackupBusybox 资源进行调整
Namespace: obj.GetNamespace(), // 使用已更改的 Busybox 命名空间
},
},
}
}
// 如果标签不存在或不匹配,则不触发和解
return []reconcile.Request{}
}),
). // 当标记的 Busybox 资源发生变化时触发重对账
Complete(r)
}
使用谓词来优化手表
在使用控制器时,使用 谓词 来过滤事件和控制何时触发调整循环通常是有益的。
谓词 允许您基于事件(例如创建、更新或删除)和资源字段(例如标签、注释或状态字段)定义条件。通过使用 谓词,您可以细化控制器的行为,使其仅对所观察资源的特定变化做出响应。
这在你想要细化哪些资源的变化应该触发对账时尤其有用。通过使用谓词,你可以避免不必要的对账,并确保控制器仅对相关变化做出反应。
何时使用谓词
谓词在以下情况下很有用:
- 您想忽略某些更改,例如那些不影响控制器关注的字段的更新。
- 您只想对具有特定标签或注释的资源触发调整。
- 您希望监视外部资源,并仅对特定更改做出反应。
示例:使用谓词过滤更新事件
假设 BackupBusybox controller 仅应在 Busybox 资源的某些字段变更时对账,例如 spec.size 字段变更,而应忽略所有其他变更(如 status 更新)。
定义谓词
在下面的示例中,我们定义了一个谓词,仅在对 Busybox 资源有重要更新时才允许对账:
import (
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/event"
)
// 仅在 Busybox 规格的大小变化时触发重新调整的谓词
updatePred := predicate.Funcs{
// 仅在 Busybox 资源的 spec.size 发生变化时允许更新
UpdateFunc: func(e event.UpdateEvent) bool {
oldObj := e.ObjectOld.(*examplecomv1alpha1.Busybox)
newObj := e.ObjectNew.(*examplecomv1alpha1.Busybox)
// 仅在 spec.size 字段发生变化时触发重新对账
return oldObj.Spec.Size != newObj.Spec.Size
},
// 允许创建事件
CreateFunc: func(e event.CreateEvent) bool {
return true
},
// 允许删除事件
DeleteFunc: func(e event.DeleteEvent) bool {
return true
},
// 允许通用事件(例如,外部触发)
GenericFunc: func(e event.GenericEvent) bool {
return true
},
}
Explanation
在这个例子中:
UpdateFunc仅在spec.size字段在旧对象和新对象之间发生变化时返回true,这意味着在spec中的其他所有更改(例如注释或其他字段)将被忽略。CreateFunc、DeleteFunc和GenericFunc返回true,意味着创建、删除和通用事件仍然被处理,从而允许这些事件类型的调整发生。
这确保了只有在特定字段 spec.size 被修改时,控制器才会进行调整,而忽略 spec 中与您的逻辑无关的其他修改。
示例:在Watches中使用谓词
现在,我们将这个谓词应用在 BackupBusyboxReconciler 的 Watches() 方法中,以便仅针对相关事件触发重新对账:
// SetupWithManager 使用 Manager 设置控制器。
// 控制器将同时监视 BackupBusybox 主要资源和 Busybox 资源,并使用谓词。
func (r *BackupBusyboxReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&examplecomv1alpha1.BackupBusybox{}). // 观察主要资源(BackupBusybox)
Watches(
&examplecomv1alpha1.Busybox{}, // 观察 Busybox CR
handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request {
return []reconcile.Request{
{
NamespacedName: types.NamespacedName{
Name: "backupbusybox", // 对关联的 BackupBusybox 资源进行调整
Namespace: obj.GetNamespace(), // 使用已更改的 Busybox 命名空间
},
},
}
}),
builder.WithPredicates(updatePred), // 应用谓词
). // 当 Busybox 资源发生变化时触发调整(如果符合谓词条件)
Complete(r)
}
Explanation
builder.WithPredicates(updatePred):此方法应用谓词,确保只有在Busybox中spec.size字段发生变化时才会进行调解。- 其他事件:控制器仍将在
创建、删除和通用事件上触发调整。
使用 Kind 进行开发目的和持续集成(CI)
为什么使用 Kind
- 快速设置: 在一分钟内在本地启动一个多节点的Kubernetes集群。
- 快速拆解: 仅需几秒钟即可拆解集群,简化您的开发工作流程。
- 本地镜像使用: 可以直接部署您的容器镜像,无需推送到远程注册中心。
- 轻量高效: Kind 是一个极简的 Kubernetes 发行版,非常适合本地开发和 CI/CD 流程。
这仅涵盖了使用 kind 集群的基础知识。您可以在 kind 文档 中找到更多详细信息。
安装
按照 kind 安装说明 安装 kind。
创建一个集群
您可以通过以下方式简单地创建一个 kind 集群:
kind create cluster
要自定义您的集群,您可以提供额外的配置。例如,以下是一个示例 kind 配置。
kind: 集群
apiVersion: kind.x-k8s.io/v1alpha4
节点:
- 角色: 控制平面
- 角色: 工人
- 角色: 工人
- 角色: 工人
使用上述配置,运行以下命令将为您提供一个包含 1 个控制平面节点和 3 个工作节点的 k8s v1.17.2 集群。
kind create cluster --config hack/kind-config.yaml --image=kindest/node:v1.17.2
您可以使用 --image 参数指定您想要的集群版本,例如 --image=kindest/node:v1.17.2,支持的版本可以在 这里 查看。
将 Docker 镜像加载到集群中
在使用本地 kind 集群进行开发时,将 Docker 镜像加载到集群中是一个非常有用的功能。您可以避免使用容器注册表。
kind load docker-image your-image-name:your-tag
请参阅 将本地镜像加载到 kind 集群 以获取更多信息。
删除集群
kind delete cluster
Webhook(网络钩子)
Webhooks 是以阻塞方式发送的信息请求。实现 Webhook 的 web 应用程序会在某个事件发生时向其他应用程序发送 HTTP 请求。
在Kubernetes世界中,有三种类型的Webhook:准入Webhook、授权Webhook和CRD转换Webhook。
在 controller-runtime 库中,我们支持准入 webhook 和 CRD 转换 webhook。
Kubernetes 从 1.9 版本开始支持这些动态 Admission Webhooks(当时该功能进入 beta 阶段)。
Kubernetes 从 1.15 版本开始支持转换 Webhook(该功能在此时进入测试版)。
禁用 Webhook 服务器
设置 --webhook-port=-1 以禁用 Webhook 服务器:
./bin/manager --webhook-port=-1
controller-runtime 会记录 Webhook server is disabled 且不监听任何端口。这需要 controller-runtime v0.25.0 或更高版本。旧版本会将 -1 视为默认的 9443。
仅在没有 webhook 配置或 CRD conversion 指向该 manager 时禁用服务器。否则 API 服务器无法到达 webhook,并会拒绝它们所守护的请求。
使用 make run 在本地运行 manager 时若要跳过 Webhook 注册,请设置 ENABLE_WEBHOOKS=false。
入场webhook
Admission Webhooks 是接收入场请求的 HTTP 回调,处理这些请求并返回入场响应。
Kubernetes 提供以下类型的准入 webhook:
-
变更Admission Webhook:这些Webhook可以在对象创建或更新时对其进行变更,在对象被存储之前。它可以用于在资源请求中设置字段的默认值,例如,在用户未指定的Deployment字段中。还可以用于注入边车容器。
-
验证入场 Webhook:这些可以在对象被创建或更新时进行验证,确保其在存储之前有效。它允许比纯基于模式的验证更复杂的验证,例如跨字段验证和容器镜像白名单。
默认情况下,apiserver 并不会对 webhook 进行身份验证。然而,如果您想要对客户端进行身份验证,您可以配置 apiserver 使用基本身份验证、令牌或证书来对 webhook 进行身份验证。您可以在这里找到详细步骤。
Execution Order
验证 webhook 在所有改变性 webhook 之后运行,因此您无需担心在验证接受对象之后另一个 webhook 会更改该对象。
Custom webhook paths
默认情况下,Kubebuilder 根据资源的 group、version 和 kind 生成 Webhook 路径。例如:
batch/v1/CronJob的变更 Webhook:/mutate-batch-v1-cronjobbatch/v1/CronJob的校验 Webhook:/validate-batch-v1-cronjob
你可以使用专用标志为 Webhook 指定自定义路径:
# Custom path for defaulting webhook
kubebuilder create webhook --group batch --version v1 --kind CronJob \
--defaulting --defaulting-path=/my-custom-mutate-path
# Custom path for validation webhook
kubebuilder create webhook --group batch --version v1 --kind CronJob \
--programmatic-validation --validation-path=/my-custom-validate-path
# Both webhooks with different custom paths
kubebuilder create webhook --group batch --version v1 --kind CronJob \
--defaulting --programmatic-validation \
--defaulting-path=/custom-mutate --validation-path=/custom-validate
Version Requirements
自定义 webhook 路径需要 controller-runtime v0.21+。在更早版本(< v0.21)中,webhook 路径遵循基于资源 group、version 和 kind 的固定模式,且无法自定义。
在入场Webhook中处理资源状态
Modify status
您无法通过变更的 admission webhook 修改或设置资源的状态。在您的控制器中,当您首次看到一个新对象时,请设置初始状态。
理解原因:
变异 admission Webhooks
变更的 Admission Webhooks 主要旨在拦截和修改与对象的创建、修改或删除相关的请求。尽管它们具备修改对象规格的能力,但直接更改对象的状态并不被视为标准做法,通常会导致意外结果。
// MutatingWebhookConfiguration 允许对对象进行修改。
// 然而,直接修改状态可能会导致意外行为。
type MutatingWebhookConfiguration struct {
...
}
设置初始状态
对于那些深入研究自定义资源的自定义控制器的人来说,了解设置初始状态的概念是至关重要的。这个初始化通常在控制器内部进行。当控制器通过监视机制识别到其管理资源的新实例时,它有权为该资源分配一个初始状态。
// 自定义控制器的调整函数可能看起来像这样:
func (r *ReconcileMyResource) Reconcile(request reconcile.Request) (reconcile.Result, error) {
// ...
// 在发现新实例时,设置初始状态
instance.Status = SomeInitialStatus
// ...
}
状态子资源
深入研究 Kubernetes 自定义资源时,可以明确区分 spec(描述期望状态)和 status(展示观察到的状态)。为自定义资源定义(CRD)启用 /status 子资源,将 status 和 spec 分开,各自分配给相应的 API 端点。这样的分离确保了用户引入的变更,例如修改 spec,以及系统驱动的更新,如状态变化,能够保持明确的区分。在修改 spec 操作期间利用变更的 webhook 来调整状态,可能无法如预期那样顺利进行,这要归功于这种隔离。
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: myresources.mygroup.mydomain
spec:
...
subresources:
status: {} # 启用 /status 子资源
结论
虽然某些边缘场景可能允许变更的 webhook 无缝地修改状态,但走这条道路并不是普遍认可或推荐的策略。将状态更新的责任交给控制器逻辑仍然是最被提倡的方法。
Webhook 引导问题
问题所在
当你为核心 Kubernetes 类型(Pod、Deployment、Job 等)创建 webhook 时,该 webhook 可能阻塞其自身 controller Pod 的启动,导致部署死锁。
示例命令:
kubebuilder create webhook --group core --version v1 --kind Pod --programmatic-validation
示例场景:
- 你为 Pod 创建校验 Webhook
- 你部署控制器(以 Pod 运行)
- Kubernetes 尝试创建你的控制器 Pod
- 你的 Webhook 会拦截此次 Pod 创建
- Webhook 服务器尚未就绪(它位于正在创建的 Pod 内)
- Pod 创建会挂起,等待 Webhook 校验
- 由于 Pod 被阻塞,Webhook 永远无法启动
结果: 死锁。你的部署失败。
这种情况何时发生?
核心 Kubernetes 类型
为内置 Kubernetes 资源创建 Webhook 时会出现引导问题:
coregroup: Pod, Service, Namespace, ConfigMap, Secretappsgroup: Deployment, StatefulSet, DaemonSet, ReplicaSetbatchgroup: Job, CronJob- 其他内置类型
为什么? 你的 webhook 验证的资源类型,正是 controller 部署所创建的类型(通常是 Pod 或 Deployment)。
自定义 CRD
自定义资源 Webhook 不会出现引导问题:
- 你的 Webhook 校验
MyResource对象 - 你的控制器以 Pod 运行
- Pod 与 MyResource 是不同类型
- 无循环依赖
如何修复
使用 namespaceSelector 或 objectSelector 配置 Webhook 跳过校验自身资源。
选项 1:namespaceSelector (recommended)
排除你的 Webhook 运行所在的整个命名空间。
步骤 1: 在 config/manager/manager.yaml 中为 Namespace 添加标签:
apiVersion: v1
kind: Namespace
metadata:
labels:
控制平面: 控制器管理器
app.kubernetes.io/name: my-project
app.kubernetes.io/managed-by: kustomize
webhook-excluded: "true"
name: system
步骤 2: 创建补丁文件 config/webhook/namespaceselector_patch.yaml:
# For mutating webhooks (--defaulting)
- op: add
path: /webhooks/0/namespaceSelector
value:
匹配表达式:
- key: webhook-excluded
操作员: DoesNotExist
对于 validating webhook(--programmatic-validation),创建类似补丁并指向 ValidatingWebhookConfiguration。
步骤 3: 将补丁添加到 config/webhook/kustomization.yaml:
resources:
- manifests.yaml
- service.yaml
贴片:
- path: namespaceselector_patch.yaml
目标:
group: admissionregistration.k8s.io
version: v1
kind: 变更Webhook配置
name: mutating-webhook-configuration
步骤 4: 部署:
make deploy IMG=<your-image>
选项 2:objectSelector
将带特定标签的 Pod 排除在 Webhook 校验之外。
步骤 1: 在 config/manager/manager.yaml 中为 Pod 添加标签:
spec:
template:
metadata:
annotations:
kubectl.kubernetes.io/default-container: manager
labels:
控制平面: 控制器管理器
app.kubernetes.io/name: my-project
webhook-excluded: "true"
步骤 2-4: 与选项 1 相同,但在补丁文件中使用 objectSelector 而非 namespaceSelector。
多个 Webhook
如果为多个核心类型(例如 Pod 和 Deployment)创建了 Webhook,你会有多个 Webhook 条目。
检查 Webhook 数量:
make manifests
grep " name: m" config/webhook/manifests.yaml # Count mutating webhooks
grep " name: v" config/webhook/manifests.yaml # Count validating webhooks
示例输出:
name: mpod-v1.kb.io # Index 0
name: mdeployment-v1.kb.io # Index 1
在补丁文件中为所有索引添加补丁:
- op: add
path: /webhooks/0/namespaceSelector
value:
匹配表达式:
- key: webhook-excluded
操作员: DoesNotExist
- op: add
path: /webhooks/1/namespaceSelector
value:
匹配表达式:
- key: webhook-excluded
操作员: DoesNotExist
混合 Webhook(CRD + 核心类型)
如果你同时有自定义 CRD Webhook 和核心类型 Webhook:
- CRD Webhook 在配置中首先出现
- 核心类型 Webhook 出现在之后
- 统计全部 Webhook,并为你的核心类型 Webhook 索引添加补丁
示例: 若有 1 个 CRD Webhook(索引 0)和 1 个核心类型 Webhook(索引 1),补丁应针对索引 1:
- op: add
path: /webhooks/1/namespaceSelector
value:
匹配表达式:
- key: webhook-excluded
操作员: DoesNotExist
在 namespaceSelector 与 objectSelector 之间选择
| Feature | namespaceSelector | objectSelector |
|---|---|---|
| Excludes | 整个命名空间 | 特定 Pod |
| Scope | Broad | Fine-grained |
| 最适合 | 专用 Webhook 命名空间 | 共享命名空间 |
| Complexity | Simple | 更有针对性 |
建议: 除非需要细粒度控制,否则使用 namespaceSelector。
References
配置/代码生成标记
Kubebuilder 使用一个名为 controller-gen 的工具来生成实用代码和 Kubernetes YAML。此代码和配置的生成是通过在 Go 代码中添加特殊的“标记注释“来控制的。
标记是以加号开头的单行注释,后面跟着标记名称,后面可以选择性地跟上一些标记特定配置:
// +kubebuilder:validation:可选
// +kubebuilder:validation:最大项数=2
// +kubebuilder:printcolumn:JSONPath=".status.replicas",name=副本数,type=string
difference between // +optional and // +kubebuilder:validation:Optional
Controller-gen 支持两者(请查看 controller-gen crd -www 的输出)。+kubebuilder:validation:Optional 和 +optional 可以应用于字段。
但是 +kubebuilder:validation:Optional 也可以在包级别应用,这样它就适用于包中的每个字段。
如果您仅使用 controller-gen,那么它们是多余的,但如果您使用其他生成器或者您希望开发人员能够为您的 API 构建自己的客户端,则您还需要添加 +optional。
在1.x中,获取+optional的最可靠方法是使用omitempty。
请查看每个子部分以获取有关不同类型代码和 YAML 生成的信息。
在 Kubebuilder 中生成代码和工件
Kubebuilder 项目有两个 make 目标,用于使用 controller-gen:
-
make manifests生成 Kubernetes 对象 YAML,如 CustomResourceDefinitions、WebhookConfigurations 和 RBAC roles。启用 Server-Side Apply 时,还会生成 ApplyConfiguration 类型。 -
make generate生成代码,例如 runtime.Object/DeepCopy 实现。
请查看生成 CRD以获取全面概述。
标记语法
确切的语法在 controller-tools 的文档 中进行了描述。
通常,标记可以是:
-
空 (
+kubebuilder:validation:Optional): 空标记就像命令行上的布尔标志 -- 仅仅指定它们就可以启用某些行为。 -
匿名 (
+kubebuilder:validation:MaxItems=2): 匿名标记的参数只能接受一个值。 -
多选项 (
+kubebuilder:printcolumn:JSONPath=".status.replicas",name=Replicas,type=string): 多选项标记可以接受一个或多个命名参数。 第一个参数通过冒号与名称分隔,后续参数用逗号分隔。 参数的顺序没有关系。 某些参数可能是可选的。
标记参数可以是字符串、整数、布尔值、切片或它们的映射。字符串、整数和布尔值遵循它们的 Go 语法:
// +kubebuilder:validation:ExclusiveMaximum=false
// +kubebuilder:validation:Format="date-time"
// +kubebuilder:validation:Maximum=42
为了方便,对于简单情况,可以省略字符串中的引号,但不鼓励在单词字符串以外的其他情况下这样做:
// +kubebuilder:validation:Type=string
切片可以通过用大括号包围并用逗号分隔来指定:
// +kubebuilder:webhooks:Enum={"饼干, 龟兔赛跑, 我们忘记带饼干了!","连温斯莱代尔奶酪都没有吗?"}
或在简单情况下,用分号分隔:
// +kubebuilder:validation:Enum=华莱士;格罗米特;鸡
映射使用字符串键和任何类型的值指定(实际上是 map[string]interface{})。映射用大括号({})包围,每个键和值之间用冒号(:)分隔,每对键值对之间用逗号分隔:
// +kubebuilder:default={magic: {numero: 42, stringified: 四十二}}
CRD 生成
这些标记描述了如何根据一系列 Go 类型和包构建自定义资源定义。实际验证模式的生成由验证标记描述。
请参见生成 CRD以获取示例。
- // +versionName
-
- string
overrides the API group version for this package (defaults to the package name). Example: +versionName=v1beta1
- string
- // +kubebuilder:unservedversion
-
does not serve this version.
若需要放弃某个版本以支持更新版本,这很有用。若该版本是 storage version,仍会存储在 etcd 中,但不会通过 API 提供服务。
Example:
// +kubebuilder:unservedversion type MyCRDv1alpha1 struct { // This version is no longer served }- // +kubebuilder:subresource:status
-
enables the "/status" subresource on a CRD.
status 子资源允许你独立于资源其余 spec 更新 status 字段,并在更新根对象时防止更新 status 子资源。这有助于将用户提供的 spec 与系统提供的 status 分开。
Example:
// +kubebuilder:subresource:status type MyCRD struct { metav1.TypeMeta metav1.ObjectMeta Spec MyCRDSpec Status MyCRDStatus }- // +kubebuilder:subresource:scale
-
- selectorpath
- string
- specpath
- string
- statuspath
- string
enables the "/scale" subresource on a CRD.
scale 子资源允许你对 CRD 使用
kubectl scale和 HorizontalPodAutoscaler。Example:
// +kubebuilder:subresource:scale:specpath=.spec.replicas,statuspath=.status.replicas,selectorpath=.status.selector type MyCRD struct { metav1.TypeMeta metav1.ObjectMeta Spec MyCRDSpec Status MyCRDStatus }- selectorpath
- string
specifies the jsonpath to the pod label selector field for the scale's status.
selector 字段必须是选择器的_字符串_形式(序列化形式)。设置 pod 标签选择器是你的类型与 HorizontalPodAutoscaler 配合工作的必要条件。通常为 .status.selector。
- specpath
- string
specifies the jsonpath to the replicas field for the scale's spec.
此处存储期望副本数(通常为 .spec.replicas)。
- statuspath
- string
specifies the jsonpath to the replicas field for the scale's status.
此处存储实际副本数(通常为 .status.replicas)。
- // +kubebuilder:storageversion
-
marks this version as the "storage version" for the CRD for conversion.
当为 CRD 启用 conversion(即不是 trivial-versions/单版本 CRD)时,会将一个版本设为存储在 etcd 中的 “storage version”。尝试存储任何其他版本都会通过 conversion webhook 转换为 storage version。
Example:
// +kubebuilder:storageversion type MyCRDv2 struct { metav1.TypeMeta metav1.ObjectMeta Spec MyCRDSpec }- // +kubebuilder:skipversion
-
removes the particular version of the CRD from the CRDs spec.
若需要跳过为「内部」资源版本生成和列出版本条目,这很有用;使用 Kubernetes 上游 conversion-gen 工具时通常会存在此类版本。
Example:
// +kubebuilder:skipversion type MyCRDInternal struct { // internal version not served by API }- // +kubebuilder:skip
-
don't consider this package as an API version. Use this to exclude internal or helper packages from CRD generation.
- // +kubebuilder:selectablefield
-
- JSONPath
- string
adds a field that may be used with field selectors.
字段选择器允许用户在列出时按字段值过滤资源。例如:
kubectl get mycrds --field-selector status.phase=Running。Example:
// +kubebuilder:selectablefield:JSONPath=".status.phase" type MyCRD struct { metav1.TypeMeta metav1.ObjectMeta Status MyCRDStatus }- JSONPath
- string
specifies the jsonpath expression which is used to produce a field selector value.
路径相对于资源根。示例:
.status.phase。
- // +kubebuilder:resource
-
- categories
- string
- path
- string
- scope
- string
- shortName
- string
- singular
- string
configures naming and scope for a CRD.
Example:
// +kubebuilder:resource:path=mycrdplural,singular=mycrdsingular,shortName=mc;mcrd,categories=all,scope=Namespaced type MyCRD struct { metav1.TypeMeta metav1.ObjectMeta }- categories
- string
specifies which group aliases this resource is part of.
Group alias 用于一次处理一组资源。最常见的是 “all”,覆盖约三分之一的 Kubernetes 基础资源,通常用于「面向用户」的资源。这允许用户运行
kubectl get all这类命令时包含你的 CRD。- path
- string
specifies the plural "resource" for this CRD.
它通常对应 Kind 的复数小写形式。例如,若 Kind 为 “MyCRD”,路径可能是 “mycrds”。参见 https://book.kubebuilder.io/cronjob-tutorial/gvks.html。
- scope
- string
overrides the scope of the CRD (Cluster vs Namespaced).
Scope 默认为 “Namespaced”。集群作用域(“Cluster”)资源不存在于命名空间中,可从集群任意位置访问。
- shortName
- string
specifies aliases for this CRD.
短名称常用于反复操作同一资源的场景。例如 “rs” 表示 “replicaset”,“crd” 表示 customresourcedefinition。可用分号分隔指定多个短名称。
- singular
- string
overrides the singular form of your resource.
单数形式否则从复数(path)默认推导。用于 API 响应与
kubectl输出。
- // +kubebuilder:printcolumn
-
- JSONPath
- string
- description
- string
- format
- string
- name
- string
- priority
- int
- type
- string
adds a column to "kubectl get" output for this CRD.
这允许你自定义用户对 CRD 运行
kubectl get时显示的列。Example:
// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.phase` // +kubebuilder:printcolumn:name="Age",type=date,JSONPath=`.metadata.creationTimestamp` type MyCRD struct { metav1.TypeMeta metav1.ObjectMeta }- JSONPath
- string
specifies the jsonpath expression used to extract the value of the column.
路径相对于资源根。示例:
.status.phase或.spec.replicas。- description
- string
specifies optional help text for this column.
显示行为取决于客户端;参见 Kubernetes API 文档中的 CustomResourceColumnDefinition。
- format
- string
specifies the format of the column.
可以是与类型对应的任意 OpenAPI 数据格式,列表见 https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#data-types。
- name
- string
specifies the name of the column as it will appear in the header.
- priority
- int
indicates how important it is that this column be displayed.
优先级较低(数字_较大_)的列会在终端宽度过小时隐藏。优先级为 0 的列始终显示。
- type
- string
indicates the type of the column.
可以是 https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#data-types 中列出的任意 OpenAPI 数据类型。常见值:“string”、“integer”、“number”、“boolean”、“date”。
- // +kubebuilder:metadata
-
- annotations
- string
- labels
- string
configures the additional annotations or labels for this CRD.
例如为带 Kubernetes groups 的 CRD 添加注解 “api-approved.kubernetes.io”,或为需要 CA 注入的 CRD 添加注解 “cert-manager.io/inject-ca-from-secret”。
Example:
// +kubebuilder:metadata:annotations="api-approved.kubernetes.io/v1=https://github.com/myorg/myrepo/pull/123" // +kubebuilder:metadata:labels="app=myapp" type MyCRD struct { metav1.TypeMeta metav1.ObjectMeta }- annotations
- string
will be added into the annotations of this CRD.
格式:“key=value”。可通过重复标记指定多个注解。
- labels
- string
will be added into the labels of this CRD.
格式:“key=value”。可通过重复标记指定多个标签。
- // +kubebuilder:externalDocs
-
- description
- string
- url
- string
specifies external documentation for this field or type.
url 为必填且必须是有效 URL。description 可选,用于简短描述外部文档。
- description
- string
is a short description of the target documentation.
- url
- string
specifies the URL for the target documentation.
- // +kubebuilder:externalDocs
-
- description
- string
- url
- string
specifies external documentation for this field or type.
url 为必填且必须是有效 URL。description 可选,用于简短描述外部文档。
- description
- string
is a short description of the target documentation.
- url
- string
specifies the URL for the target documentation.
- // +kubebuilder:deprecatedversion
-
- warning
- string
marks this version as deprecated.
已弃用版本在使用时会显示警告消息。这对于告知用户应迁移到更新版本很有用。
Example:
// +kubebuilder:deprecatedversion:warning="v1alpha1 is deprecated; use v1 instead" type MyCRDv1alpha1 struct { metav1.TypeMeta metav1.ObjectMeta }- warning
- string
message to be shown on the deprecated version.
当用户与已弃用版本交互时显示此消息。
- // +k8s:enum
-
indicates that the given type is an enum; all const values of this type are considered values in the enum
- // +groupName
-
- string
specifies the API group name for this package. Example: +groupName=mygroup.example.com
- string
CRD 验证
这些标记会修改 CRD 验证模式在其修改的类型和字段上的生成方式。每个标记大致对应于一个 OpenAPI/JSON 模式选项。
请参阅 生成 CRD 以获取示例。
Understanding Marker Grouping in Documentation
某些标记可能看起来重复。然而,这些标记是根据它们的使用上下文进行分组的——例如字段、类型或数组。例如,一个像 +kubebuilder:validation:Enum 的标记可以应用于单个字段或数组项,这种灵活性在文档中有所体现。
这种分组确保了清晰性,展示了同一个标记如何可以被用于不同的目的。
Schema & Validation
自定义资源使用生成的 OpenAPI v3 schema 进行验证,且必须遵守 Kubernetes structural schema 规则。API 服务器仅强制执行可在 CRD schema 中表示的字段类型与约束。
数值类型受 Kubernetes CRD OpenAPI v3 schema 兼容性 约束。实践中优先使用能干净映射到受支持 OpenAPI 格式的 Go 类型(例如整数用 int32 和 int64)。需要类小数表示的值请使用 resource.Quantity。
使用 Kubebuilder 校验 markers 声明额外约束——例如最小值与最大值、长度限制、模式、枚举以及 list 或 map 规则——以便它们反映在生成的 CRD 中并在运行时校验。
- // +required
-
specifies that this field is required.
- // +optional
-
specifies that this field is optional.
- // +nullable
-
marks this field as allowing the "null" value.
通常不必如此,但对自定义序列化可能有帮助。
Example:
// +nullable Description *string- // +kubebuilder:validation:items:XValidation
-
- fieldPath
- string
- message
- string
- messageExpression
- string
- optionalOldSelf
- bool
- reason
- string
- rule
- string
for array items marks a field as requiring a value for which a given
表达式求值为 true。
此标记可重复以指定多个表达式,所有这些表达式都必须求值为 true。
CEL 表达式可使用:self(当前值)和 oldSelf(先前值;仅在更新时,创建时为 null)。规则作用域为其在 schema 中出现的位置;self 即为该值。
Examples:
// Basic field validation using self // +kubebuilder:validation:XValidation:rule="self.minReplicas <= self.replicas && self.replicas <= self.maxReplicas",message="replicas must be between minReplicas and maxReplicas" // Validation with custom reason // +kubebuilder:validation:XValidation:rule="self.x <= self.maxX",message="x cannot be greater than maxX",reason="FieldValueInvalid" // Immutability check using oldSelf (e.g. on a field so self/oldSelf are that field's value) // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="field is immutable" // Validation with field path for better error reporting // +kubebuilder:validation:XValidation:rule="self >= 1",message="value must be at least 1",fieldPath=".spec.replicas"- fieldPath
- string
is the path to the field that failed validation (for clearer error messages).
示例:FieldPath=“.spec.replicas”
- message
- string
is the text shown when validation fails.
若未设置,默认为 “failed rule: {Rule}”。不得包含换行。API 服务器有长度限制(例如 256 个字符)。完整细节见 k8s.io/apiextensions-apiserver 中的 ValidationRule。示例:Message=“replicas must be between minReplicas and maxReplicas”
- messageExpression
- string
is a CEL expression that returns the message shown when validation fails.
可以设置 Message 或 MessageExpression,不能同时设置。MessageExpression 必须返回字符串。若两者都设置,使用 MessageExpression。若都未设置,使用默认消息。表达式可使用与 Rule 相同的变量(例如 self、oldSelf)。结果不得包含换行,并受与 Message 相同的消息长度限制。示例:MessageExpression=“’replicas must be between ’ + string(self.minReplicas) + ’ and ’ + string(self.maxReplicas)”
- optionalOldSelf
- bool
OptionalOldSelf, when true, runs the rule on create and on update (even when there is no old value).
为 true 时,oldSelf 可能缺失:使用 oldSelf.hasValue() 检查,并用 oldSelf.value() 取值。规则始终运行;使用 oldSelf 前必须在规则中检查 oldSelf.hasValue()。仅当规则使用 oldSelf 时可设置。参见 k8s.io/apiextensions-apiserver 中的 ValidationRule。示例:OptionalOldSelf=true
- reason
- string
is a short code for why validation failed, returned to API callers.
支持的值:“FieldValueInvalid”、“FieldValueForbidden”、“FieldValueRequired”、“FieldValueDuplicate”。未设置时默认为 “FieldValueInvalid”。示例:Reason=“FieldValueInvalid”
- rule
- string
is the CEL expression that must evaluate to true.
作用域为该 XValidation 在 schema 中的位置;self 即为该值。示例:Rule=“self.minReplicas <= self.replicas && self.replicas <= self.maxReplicas”
- // +kubebuilder:validation:items:XValidation
-
- fieldPath
- string
- message
- string
- messageExpression
- string
- optionalOldSelf
- bool
- reason
- string
- rule
- string
for array items marks a field as requiring a value for which a given
表达式求值为 true。
此标记可重复以指定多个表达式,所有这些表达式都必须求值为 true。
CEL 表达式可使用:self(当前值)和 oldSelf(先前值;仅在更新时,创建时为 null)。规则作用域为其在 schema 中出现的位置;self 即为该值。
Examples:
// Basic field validation using self // +kubebuilder:validation:XValidation:rule="self.minReplicas <= self.replicas && self.replicas <= self.maxReplicas",message="replicas must be between minReplicas and maxReplicas" // Validation with custom reason // +kubebuilder:validation:XValidation:rule="self.x <= self.maxX",message="x cannot be greater than maxX",reason="FieldValueInvalid" // Immutability check using oldSelf (e.g. on a field so self/oldSelf are that field's value) // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="field is immutable" // Validation with field path for better error reporting // +kubebuilder:validation:XValidation:rule="self >= 1",message="value must be at least 1",fieldPath=".spec.replicas"- fieldPath
- string
is the path to the field that failed validation (for clearer error messages).
示例:FieldPath=“.spec.replicas”
- message
- string
is the text shown when validation fails.
若未设置,默认为 “failed rule: {Rule}”。不得包含换行。API 服务器有长度限制(例如 256 个字符)。完整细节见 k8s.io/apiextensions-apiserver 中的 ValidationRule。示例:Message=“replicas must be between minReplicas and maxReplicas”
- messageExpression
- string
is a CEL expression that returns the message shown when validation fails.
可以设置 Message 或 MessageExpression,不能同时设置。MessageExpression 必须返回字符串。若两者都设置,使用 MessageExpression。若都未设置,使用默认消息。表达式可使用与 Rule 相同的变量(例如 self、oldSelf)。结果不得包含换行,并受与 Message 相同的消息长度限制。示例:MessageExpression=“’replicas must be between ’ + string(self.minReplicas) + ’ and ’ + string(self.maxReplicas)”
- optionalOldSelf
- bool
OptionalOldSelf, when true, runs the rule on create and on update (even when there is no old value).
为 true 时,oldSelf 可能缺失:使用 oldSelf.hasValue() 检查,并用 oldSelf.value() 取值。规则始终运行;使用 oldSelf 前必须在规则中检查 oldSelf.hasValue()。仅当规则使用 oldSelf 时可设置。参见 k8s.io/apiextensions-apiserver 中的 ValidationRule。示例:OptionalOldSelf=true
- reason
- string
is a short code for why validation failed, returned to API callers.
支持的值:“FieldValueInvalid”、“FieldValueForbidden”、“FieldValueRequired”、“FieldValueDuplicate”。未设置时默认为 “FieldValueInvalid”。示例:Reason=“FieldValueInvalid”
- rule
- string
is the CEL expression that must evaluate to true.
作用域为该 XValidation 在 schema 中的位置;self 即为该值。示例:Rule=“self.minReplicas <= self.replicas && self.replicas <= self.maxReplicas”
- // +kubebuilder:validation:items:XIntOrString
-
for array items marks a fields as an IntOrString.
对 IntOrString 字段应用模式或其他校验时需要此项。关于该类型的已知信息在 collapse 阶段应用,因此在 marker 应用期间通常不可用。
Example:
// +kubebuilder:validation:XIntOrString // +kubebuilder:validation:Pattern="^(\\d+|\\d+%|)$" Port intstr.IntOrString- // +kubebuilder:validation:items:XIntOrString
-
for array items marks a fields as an IntOrString.
对 IntOrString 字段应用模式或其他校验时需要此项。关于该类型的已知信息在 collapse 阶段应用,因此在 marker 应用期间通常不可用。
Example:
// +kubebuilder:validation:XIntOrString // +kubebuilder:validation:Pattern="^(\\d+|\\d+%|)$" Port intstr.IntOrString- // +kubebuilder:validation:items:XEmbeddedResource
-
for array items marks a fields as an embedded resource with apiVersion, kind and metadata fields.
嵌入资源是具有 apiVersion、kind 和 metadata 字段的值。它们会根据当前运行的 apiserver 语义隐式校验。不必为这些字段添加额外 schema,但也可以添加。可与 PreserveUnknownFields 结合使用。
Example:
// +kubebuilder:validation:EmbeddedResource Template runtime.RawExtension- // +kubebuilder:validation:items:XEmbeddedResource
-
for array items marks a fields as an embedded resource with apiVersion, kind and metadata fields.
嵌入资源是具有 apiVersion、kind 和 metadata 字段的值。它们会根据当前运行的 apiserver 语义隐式校验。不必为这些字段添加额外 schema,但也可以添加。可与 PreserveUnknownFields 结合使用。
Example:
// +kubebuilder:validation:EmbeddedResource Template runtime.RawExtension- // +kubebuilder:validation:items:UniqueItems
-
- bool
for array items specifies that all items in this list must be unique.
Example:
// +kubebuilder:validation:UniqueItems=true Tags []string- bool
- // +kubebuilder:validation:items:UniqueItems
-
- bool
for array items specifies that all items in this list must be unique.
Example:
// +kubebuilder:validation:UniqueItems=true Tags []string- bool
- // +kubebuilder:validation:items:Type
-
- string
for array items overrides the type for this field (which defaults to the equivalent of the Go type).
通常必须与自定义序列化配对。例如,metav1.Time 字段会标记为 “type: string” 和 “format: date-time”。
常见类型包括:“string”、“number”、“integer”、“boolean”、“array”、“object”。
Example:
// +kubebuilder:validation:Type=string // +kubebuilder:validation:Format=date-time Time metav1.Time- string
- // +kubebuilder:validation:items:Type
-
- string
for array items overrides the type for this field (which defaults to the equivalent of the Go type).
通常必须与自定义序列化配对。例如,metav1.Time 字段会标记为 “type: string” 和 “format: date-time”。
常见类型包括:“string”、“number”、“integer”、“boolean”、“array”、“object”。
Example:
// +kubebuilder:validation:Type=string // +kubebuilder:validation:Format=date-time Time metav1.Time- string
- // +kubebuilder:validation:items:Pattern
-
- string
for array items specifies that this string must match the given regular expression.
示例(DNS 子域):
// +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$` DNSName string- string
- // +kubebuilder:validation:items:Pattern
-
- string
for array items specifies that this string must match the given regular expression.
示例(DNS 子域):
// +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$` DNSName string- string
- // +kubebuilder:validation:items:MultipleOf
-
for array items specifies that this field must have a numeric value that's a multiple of this one.
示例(值必须是 5 的倍数):
// +kubebuilder:validation:MultipleOf=5 Count int32- // +kubebuilder:validation:items:MultipleOf
-
for array items specifies that this field must have a numeric value that's a multiple of this one.
示例(值必须是 5 的倍数):
// +kubebuilder:validation:MultipleOf=5 Count int32- // +kubebuilder:validation:items:Minimum
-
for array items specifies the minimum numeric value that this field can have. Negative numbers are supported.
Example:
// +kubebuilder:validation:Minimum=0 Replicas int32- // +kubebuilder:validation:items:Minimum
-
for array items specifies the minimum numeric value that this field can have. Negative numbers are supported.
Example:
// +kubebuilder:validation:Minimum=0 Replicas int32- // +kubebuilder:validation:items:MinProperties
-
- int
for array items restricts the number of keys in an object
Example:
// +kubebuilder:validation:MinProperties=1 Metadata map[string]string- int
- // +kubebuilder:validation:items:MinProperties
-
- int
for array items restricts the number of keys in an object
Example:
// +kubebuilder:validation:MinProperties=1 Metadata map[string]string- int
- // +kubebuilder:validation:items:MinLength
-
- int
for array items specifies the minimum length for this string.
Example:
// +kubebuilder:validation:MinLength=1 Name string- int
- // +kubebuilder:validation:items:MinLength
-
- int
for array items specifies the minimum length for this string.
Example:
// +kubebuilder:validation:MinLength=1 Name string- int
- // +kubebuilder:validation:items:MinItems
-
- int
for array items specifies the minimum length for this list.
Example:
// +kubebuilder:validation:MinItems=1 Endpoints []string- int
- // +kubebuilder:validation:items:MinItems
-
- int
for array items specifies the minimum length for this list.
Example:
// +kubebuilder:validation:MinItems=1 Endpoints []string- int
- // +kubebuilder:validation:items:Maximum
-
for array items specifies the maximum numeric value that this field can have.
Example:
// +kubebuilder:validation:Maximum=100 Percentage int32- // +kubebuilder:validation:items:Maximum
-
for array items specifies the maximum numeric value that this field can have.
Example:
// +kubebuilder:validation:Maximum=100 Percentage int32- // +kubebuilder:validation:items:MaxProperties
-
- int
for array items restricts the number of keys in an object
Example:
// +kubebuilder:validation:MaxProperties=10 Labels map[string]string- int
- // +kubebuilder:validation:items:MaxProperties
-
- int
for array items restricts the number of keys in an object
Example:
// +kubebuilder:validation:MaxProperties=10 Labels map[string]string- int
- // +kubebuilder:validation:items:MaxLength
-
- int
for array items specifies the maximum length for this string.
Example:
// +kubebuilder:validation:MaxLength=64 Name string- int
- // +kubebuilder:validation:items:MaxLength
-
- int
for array items specifies the maximum length for this string.
Example:
// +kubebuilder:validation:MaxLength=64 Name string- int
- // +kubebuilder:validation:items:MaxItems
-
- int
for array items specifies the maximum length for this list.
Example:
// +kubebuilder:validation:MaxItems=10 Items []string- int
- // +kubebuilder:validation:items:MaxItems
-
- int
for array items specifies the maximum length for this list.
Example:
// +kubebuilder:validation:MaxItems=10 Items []string- int
- // +kubebuilder:validation:items:Format
-
- string
for array items specifies additional "complex" formatting for this field.
例如,date-time 字段会标记为 “type: string” 和 “format: date-time”。
常见格式包括:“int32”、“int64”、“float”、“double”、“byte”、“date”、“date-time”、“password”。
Example:
// +kubebuilder:validation:Format=date-time CreatedAt string- string
- // +kubebuilder:validation:items:Format
-
- string
for array items specifies additional "complex" formatting for this field.
例如,date-time 字段会标记为 “type: string” 和 “format: date-time”。
常见格式包括:“int32”、“int64”、“float”、“double”、“byte”、“date”、“date-time”、“password”。
Example:
// +kubebuilder:validation:Format=date-time CreatedAt string- string
- // +kubebuilder:validation:items:ExclusiveMinimum
-
- bool
for array items indicates that the minimum is "up to" but not including that value.
示例(值必须大于 0,而非大于等于 0):
// +kubebuilder:validation:Minimum=0 // +kubebuilder:validation:ExclusiveMinimum=true PositiveNumber float64- bool
- // +kubebuilder:validation:items:ExclusiveMinimum
-
- bool
for array items indicates that the minimum is "up to" but not including that value.
示例(值必须大于 0,而非大于等于 0):
// +kubebuilder:validation:Minimum=0 // +kubebuilder:validation:ExclusiveMinimum=true PositiveNumber float64- bool
- // +kubebuilder:validation:items:ExclusiveMaximum
-
- bool
for array items indicates that the maximum is "up to" but not including that value.
示例(值必须小于 100,而非小于等于 100):
// +kubebuilder:validation:Maximum=100 // +kubebuilder:validation:ExclusiveMaximum=true Percentage float64- bool
- // +kubebuilder:validation:items:ExclusiveMaximum
-
- bool
for array items indicates that the maximum is "up to" but not including that value.
示例(值必须小于 100,而非小于等于 100):
// +kubebuilder:validation:Maximum=100 // +kubebuilder:validation:ExclusiveMaximum=true Percentage float64- bool
- // +kubebuilder:validation:items:Enum
-
- any
for array items specifies that this (scalar) field is restricted to the *exact* values specified here.
Example:
// +kubebuilder:validation:Enum=ClusterIP;NodePort;LoadBalancer ServiceType string- any
- // +kubebuilder:validation:items:Enum
-
- any
for array items specifies that this (scalar) field is restricted to the *exact* values specified here.
Example:
// +kubebuilder:validation:Enum=ClusterIP;NodePort;LoadBalancer ServiceType string- any
- // +kubebuilder:validation:XValidation
-
- fieldPath
- string
- message
- string
- messageExpression
- string
- optionalOldSelf
- bool
- reason
- string
- rule
- string
marks a field as requiring a value for which a given
表达式求值为 true。
此标记可重复以指定多个表达式,所有这些表达式都必须求值为 true。
CEL 表达式可使用:self(当前值)和 oldSelf(先前值;仅在更新时,创建时为 null)。规则作用域为其在 schema 中出现的位置;self 即为该值。
Examples:
// Basic field validation using self // +kubebuilder:validation:XValidation:rule="self.minReplicas <= self.replicas && self.replicas <= self.maxReplicas",message="replicas must be between minReplicas and maxReplicas" // Validation with custom reason // +kubebuilder:validation:XValidation:rule="self.x <= self.maxX",message="x cannot be greater than maxX",reason="FieldValueInvalid" // Immutability check using oldSelf (e.g. on a field so self/oldSelf are that field's value) // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="field is immutable" // Validation with field path for better error reporting // +kubebuilder:validation:XValidation:rule="self >= 1",message="value must be at least 1",fieldPath=".spec.replicas"- fieldPath
- string
is the path to the field that failed validation (for clearer error messages).
示例:FieldPath=“.spec.replicas”
- message
- string
is the text shown when validation fails.
若未设置,默认为 “failed rule: {Rule}”。不得包含换行。API 服务器有长度限制(例如 256 个字符)。完整细节见 k8s.io/apiextensions-apiserver 中的 ValidationRule。示例:Message=“replicas must be between minReplicas and maxReplicas”
- messageExpression
- string
is a CEL expression that returns the message shown when validation fails.
可以设置 Message 或 MessageExpression,不能同时设置。MessageExpression 必须返回字符串。若两者都设置,使用 MessageExpression。若都未设置,使用默认消息。表达式可使用与 Rule 相同的变量(例如 self、oldSelf)。结果不得包含换行,并受与 Message 相同的消息长度限制。示例:MessageExpression=“’replicas must be between ’ + string(self.minReplicas) + ’ and ’ + string(self.maxReplicas)”
- optionalOldSelf
- bool
OptionalOldSelf, when true, runs the rule on create and on update (even when there is no old value).
为 true 时,oldSelf 可能缺失:使用 oldSelf.hasValue() 检查,并用 oldSelf.value() 取值。规则始终运行;使用 oldSelf 前必须在规则中检查 oldSelf.hasValue()。仅当规则使用 oldSelf 时可设置。参见 k8s.io/apiextensions-apiserver 中的 ValidationRule。示例:OptionalOldSelf=true
- reason
- string
is a short code for why validation failed, returned to API callers.
支持的值:“FieldValueInvalid”、“FieldValueForbidden”、“FieldValueRequired”、“FieldValueDuplicate”。未设置时默认为 “FieldValueInvalid”。示例:Reason=“FieldValueInvalid”
- rule
- string
is the CEL expression that must evaluate to true.
作用域为该 XValidation 在 schema 中的位置;self 即为该值。示例:Rule=“self.minReplicas <= self.replicas && self.replicas <= self.maxReplicas”
- // +kubebuilder:validation:XValidation
-
- fieldPath
- string
- message
- string
- messageExpression
- string
- optionalOldSelf
- bool
- reason
- string
- rule
- string
marks a field as requiring a value for which a given
表达式求值为 true。
此标记可重复以指定多个表达式,所有这些表达式都必须求值为 true。
CEL 表达式可使用:self(当前值)和 oldSelf(先前值;仅在更新时,创建时为 null)。规则作用域为其在 schema 中出现的位置;self 即为该值。
Examples:
// Basic field validation using self // +kubebuilder:validation:XValidation:rule="self.minReplicas <= self.replicas && self.replicas <= self.maxReplicas",message="replicas must be between minReplicas and maxReplicas" // Validation with custom reason // +kubebuilder:validation:XValidation:rule="self.x <= self.maxX",message="x cannot be greater than maxX",reason="FieldValueInvalid" // Immutability check using oldSelf (e.g. on a field so self/oldSelf are that field's value) // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="field is immutable" // Validation with field path for better error reporting // +kubebuilder:validation:XValidation:rule="self >= 1",message="value must be at least 1",fieldPath=".spec.replicas"- fieldPath
- string
is the path to the field that failed validation (for clearer error messages).
示例:FieldPath=“.spec.replicas”
- message
- string
is the text shown when validation fails.
若未设置,默认为 “failed rule: {Rule}”。不得包含换行。API 服务器有长度限制(例如 256 个字符)。完整细节见 k8s.io/apiextensions-apiserver 中的 ValidationRule。示例:Message=“replicas must be between minReplicas and maxReplicas”
- messageExpression
- string
is a CEL expression that returns the message shown when validation fails.
可以设置 Message 或 MessageExpression,不能同时设置。MessageExpression 必须返回字符串。若两者都设置,使用 MessageExpression。若都未设置,使用默认消息。表达式可使用与 Rule 相同的变量(例如 self、oldSelf)。结果不得包含换行,并受与 Message 相同的消息长度限制。示例:MessageExpression=“’replicas must be between ’ + string(self.minReplicas) + ’ and ’ + string(self.maxReplicas)”
- optionalOldSelf
- bool
OptionalOldSelf, when true, runs the rule on create and on update (even when there is no old value).
为 true 时,oldSelf 可能缺失:使用 oldSelf.hasValue() 检查,并用 oldSelf.value() 取值。规则始终运行;使用 oldSelf 前必须在规则中检查 oldSelf.hasValue()。仅当规则使用 oldSelf 时可设置。参见 k8s.io/apiextensions-apiserver 中的 ValidationRule。示例:OptionalOldSelf=true
- reason
- string
is a short code for why validation failed, returned to API callers.
支持的值:“FieldValueInvalid”、“FieldValueForbidden”、“FieldValueRequired”、“FieldValueDuplicate”。未设置时默认为 “FieldValueInvalid”。示例:Reason=“FieldValueInvalid”
- rule
- string
is the CEL expression that must evaluate to true.
作用域为该 XValidation 在 schema 中的位置;self 即为该值。示例:Rule=“self.minReplicas <= self.replicas && self.replicas <= self.maxReplicas”
- // +kubebuilder:validation:XIntOrString
-
marks a fields as an IntOrString.
对 IntOrString 字段应用模式或其他校验时需要此项。关于该类型的已知信息在 collapse 阶段应用,因此在 marker 应用期间通常不可用。
Example:
// +kubebuilder:validation:XIntOrString // +kubebuilder:validation:Pattern="^(\\d+|\\d+%|)$" Port intstr.IntOrString- // +kubebuilder:validation:XIntOrString
-
marks a fields as an IntOrString.
对 IntOrString 字段应用模式或其他校验时需要此项。关于该类型的已知信息在 collapse 阶段应用,因此在 marker 应用期间通常不可用。
Example:
// +kubebuilder:validation:XIntOrString // +kubebuilder:validation:Pattern="^(\\d+|\\d+%|)$" Port intstr.IntOrString- // +kubebuilder:validation:XEmbeddedResource
-
marks a fields as an embedded resource with apiVersion, kind and metadata fields.
嵌入资源是具有 apiVersion、kind 和 metadata 字段的值。它们会根据当前运行的 apiserver 语义隐式校验。不必为这些字段添加额外 schema,但也可以添加。可与 PreserveUnknownFields 结合使用。
Example:
// +kubebuilder:validation:EmbeddedResource Template runtime.RawExtension- // +kubebuilder:validation:XEmbeddedResource
-
marks a fields as an embedded resource with apiVersion, kind and metadata fields.
嵌入资源是具有 apiVersion、kind 和 metadata 字段的值。它们会根据当前运行的 apiserver 语义隐式校验。不必为这些字段添加额外 schema,但也可以添加。可与 PreserveUnknownFields 结合使用。
Example:
// +kubebuilder:validation:EmbeddedResource Template runtime.RawExtension- // +kubebuilder:validation:UniqueItems
-
- bool
specifies that all items in this list must be unique.
Example:
// +kubebuilder:validation:UniqueItems=true Tags []string- bool
- // +kubebuilder:validation:UniqueItems
-
- bool
specifies that all items in this list must be unique.
Example:
// +kubebuilder:validation:UniqueItems=true Tags []string- bool
- // +kubebuilder:validation:Type
-
- string
overrides the type for this field (which defaults to the equivalent of the Go type).
通常必须与自定义序列化配对。例如,metav1.Time 字段会标记为 “type: string” 和 “format: date-time”。
常见类型包括:“string”、“number”、“integer”、“boolean”、“array”、“object”。
Example:
// +kubebuilder:validation:Type=string // +kubebuilder:validation:Format=date-time Time metav1.Time- string
- // +kubebuilder:validation:Type
-
- string
overrides the type for this field (which defaults to the equivalent of the Go type).
通常必须与自定义序列化配对。例如,metav1.Time 字段会标记为 “type: string” 和 “format: date-time”。
常见类型包括:“string”、“number”、“integer”、“boolean”、“array”、“object”。
Example:
// +kubebuilder:validation:Type=string // +kubebuilder:validation:Format=date-time Time metav1.Time- string
- // +kubebuilder:validation:Schemaless
-
marks a field as being a schemaless object.
无 schema 的对象不会被内省,因此你必须自行提供类型与校验信息。此标签的一个用途是嵌入保存 JSONSchema 类型对象的字段。由于该字段禁用所有类型检查,建议仅作为最后手段使用。
Example:
// +kubebuilder:validation:Schemaless JSONSchema apiextensionsv1.JSONSchemaProps- // +kubebuilder:validation:Required
-
specifies that all fields in this package are required by default. Prefer using field-level +required markers instead.
- // +kubebuilder:validation:Required
-
specifies that this field is required.
- // +kubebuilder:validation:Pattern
-
- string
specifies that this string must match the given regular expression.
示例(DNS 子域):
// +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$` DNSName string- string
- // +kubebuilder:validation:Pattern
-
- string
specifies that this string must match the given regular expression.
示例(DNS 子域):
// +kubebuilder:validation:Pattern=`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$` DNSName string- string
- // +kubebuilder:validation:Optional
-
specifies that all fields in this package are optional by default. Prefer using field-level +optional markers instead.
- // +kubebuilder:validation:Optional
-
specifies that this field is optional.
- // +kubebuilder:validation:MultipleOf
-
specifies that this field must have a numeric value that's a multiple of this one.
示例(值必须是 5 的倍数):
// +kubebuilder:validation:MultipleOf=5 Count int32- // +kubebuilder:validation:MultipleOf
-
specifies that this field must have a numeric value that's a multiple of this one.
示例(值必须是 5 的倍数):
// +kubebuilder:validation:MultipleOf=5 Count int32- // +kubebuilder:validation:Minimum
-
specifies the minimum numeric value that this field can have. Negative numbers are supported.
Example:
// +kubebuilder:validation:Minimum=0 Replicas int32- // +kubebuilder:validation:Minimum
-
specifies the minimum numeric value that this field can have. Negative numbers are supported.
Example:
// +kubebuilder:validation:Minimum=0 Replicas int32- // +kubebuilder:validation:MinProperties
-
- int
restricts the number of keys in an object
Example:
// +kubebuilder:validation:MinProperties=1 Metadata map[string]string- int
- // +kubebuilder:validation:MinProperties
-
- int
restricts the number of keys in an object
Example:
// +kubebuilder:validation:MinProperties=1 Metadata map[string]string- int
- // +kubebuilder:validation:MinLength
-
- int
specifies the minimum length for this string.
Example:
// +kubebuilder:validation:MinLength=1 Name string- int
- // +kubebuilder:validation:MinLength
-
- int
specifies the minimum length for this string.
Example:
// +kubebuilder:validation:MinLength=1 Name string- int
- // +kubebuilder:validation:MinItems
-
- int
specifies the minimum length for this list.
Example:
// +kubebuilder:validation:MinItems=1 Endpoints []string- int
- // +kubebuilder:validation:MinItems
-
- int
specifies the minimum length for this list.
Example:
// +kubebuilder:validation:MinItems=1 Endpoints []string- int
- // +kubebuilder:validation:Maximum
-
specifies the maximum numeric value that this field can have.
Example:
// +kubebuilder:validation:Maximum=100 Percentage int32- // +kubebuilder:validation:Maximum
-
specifies the maximum numeric value that this field can have.
Example:
// +kubebuilder:validation:Maximum=100 Percentage int32- // +kubebuilder:validation:MaxProperties
-
- int
restricts the number of keys in an object
Example:
// +kubebuilder:validation:MaxProperties=10 Labels map[string]string- int
- // +kubebuilder:validation:MaxProperties
-
- int
restricts the number of keys in an object
Example:
// +kubebuilder:validation:MaxProperties=10 Labels map[string]string- int
- // +kubebuilder:validation:MaxLength
-
- int
specifies the maximum length for this string.
Example:
// +kubebuilder:validation:MaxLength=64 Name string- int
- // +kubebuilder:validation:MaxLength
-
- int
specifies the maximum length for this string.
Example:
// +kubebuilder:validation:MaxLength=64 Name string- int
- // +kubebuilder:validation:MaxItems
-
- int
specifies the maximum length for this list.
Example:
// +kubebuilder:validation:MaxItems=10 Items []string- int
- // +kubebuilder:validation:MaxItems
-
- int
specifies the maximum length for this list.
Example:
// +kubebuilder:validation:MaxItems=10 Items []string- int
- // +kubebuilder:validation:Format
-
- string
specifies additional "complex" formatting for this field.
例如,date-time 字段会标记为 “type: string” 和 “format: date-time”。
常见格式包括:“int32”、“int64”、“float”、“double”、“byte”、“date”、“date-time”、“password”。
Example:
// +kubebuilder:validation:Format=date-time CreatedAt string- string
- // +kubebuilder:validation:Format
-
- string
specifies additional "complex" formatting for this field.
例如,date-time 字段会标记为 “type: string” 和 “format: date-time”。
常见格式包括:“int32”、“int64”、“float”、“double”、“byte”、“date”、“date-time”、“password”。
Example:
// +kubebuilder:validation:Format=date-time CreatedAt string- string
- // +kubebuilder:validation:ExclusiveMinimum
-
- bool
indicates that the minimum is "up to" but not including that value.
示例(值必须大于 0,而非大于等于 0):
// +kubebuilder:validation:Minimum=0 // +kubebuilder:validation:ExclusiveMinimum=true PositiveNumber float64- bool
- // +kubebuilder:validation:ExclusiveMinimum
-
- bool
indicates that the minimum is "up to" but not including that value.
示例(值必须大于 0,而非大于等于 0):
// +kubebuilder:validation:Minimum=0 // +kubebuilder:validation:ExclusiveMinimum=true PositiveNumber float64- bool
- // +kubebuilder:validation:ExclusiveMaximum
-
- bool
indicates that the maximum is "up to" but not including that value.
示例(值必须小于 100,而非小于等于 100):
// +kubebuilder:validation:Maximum=100 // +kubebuilder:validation:ExclusiveMaximum=true Percentage float64- bool
- // +kubebuilder:validation:ExclusiveMaximum
-
- bool
indicates that the maximum is "up to" but not including that value.
示例(值必须小于 100,而非小于等于 100):
// +kubebuilder:validation:Maximum=100 // +kubebuilder:validation:ExclusiveMaximum=true Percentage float64- bool
- // +kubebuilder:validation:ExactlyOneOf
-
- string
specifies a list of field names that must conform to the ExactlyOneOf constraint.
- string
- // +kubebuilder:validation:Enum
-
- any
specifies that this (scalar) field is restricted to the *exact* values specified here.
Example:
// +kubebuilder:validation:Enum=ClusterIP;NodePort;LoadBalancer ServiceType string- any
- // +kubebuilder:validation:Enum
-
- any
specifies that this (scalar) field is restricted to the *exact* values specified here.
Example:
// +kubebuilder:validation:Enum=ClusterIP;NodePort;LoadBalancer ServiceType string- any
- // +kubebuilder:validation:EmbeddedResource
-
marks a fields as an embedded resource with apiVersion, kind and metadata fields.
嵌入资源是具有 apiVersion、kind 和 metadata 字段的值。它们会根据当前运行的 apiserver 语义隐式校验。不必为这些字段添加额外 schema,但也可以添加。可与 PreserveUnknownFields 结合使用。
Example:
// +kubebuilder:validation:EmbeddedResource Template runtime.RawExtension- // +kubebuilder:validation:AtMostOneOf
-
- string
specifies a list of field names that must conform to the AtMostOneOf constraint.
- string
- // +kubebuilder:validation:AtLeastOneOf
-
- string
specifies a list of field names that must conform to the AtLeastOneOf constraint.
- string
- // +kubebuilder:title
-
- any
sets the title for this field.
title 是使 OpenAPI 文档更友好的元数据,在文档工具中查看时让 schema 更易理解。它是不影响校验的元数据字段,但提供关于 schema 所表示内容的重要上下文。
Examples:
// Simple title // +kubebuilder:title="Replica Count" Replicas int32 // Descriptive title // +kubebuilder:title="Database Connection Configuration" DatabaseConfig DatabaseConfig- any
is the title text to be shown in OpenAPI documentation.
- // +kubebuilder:title
-
- any
sets the title for this field.
title 是使 OpenAPI 文档更友好的元数据,在文档工具中查看时让 schema 更易理解。它是不影响校验的元数据字段,但提供关于 schema 所表示内容的重要上下文。
Examples:
// Simple title // +kubebuilder:title="Replica Count" Replicas int32 // Descriptive title // +kubebuilder:title="Database Connection Configuration" DatabaseConfig DatabaseConfig- any
is the title text to be shown in OpenAPI documentation.
- // +kubebuilder:example
-
- any
sets the example value for this field.
示例值会被接受为对该字段有效的任意值。常见类型的格式包括:布尔:
true,字符串:Cluster,数值:1.24,数组:{1,2},对象:{policy: "delete"})。示例应以 pruned 形式定义,仅做尽力校验。完整校验示例需要将包含它的 CRD 提交到 apiserver。示例显示在 API 文档中,帮助用户理解期望格式。
用法示例:
// String example // +kubebuilder:example="my-service" ServiceName string // Integer example // +kubebuilder:example=5 Replicas int32 // Boolean example // +kubebuilder:example=false Debug bool // Array example // +kubebuilder:example={8080,8443} Ports []int // Object example // +kubebuilder:example={cpu: "100m", memory: "128Mi"} Resources map[string]string- any
is the example value to be shown in API documentation.
- // +kubebuilder:default
-
- any
sets the default value for this field.
默认值会被接受为对该字段有效的任意值。常见类型的格式包括:布尔:
true,字符串:Cluster,数值:1.24,数组:{1,2},对象:{policy: "delete"})。默认值应以 pruned 形式定义,仅做尽力校验。完整校验默认值需要将包含它的 CRD 提交到 apiserver。Examples:
// String default // +kubebuilder:default="ClusterIP" ServiceType string // Integer default // +kubebuilder:default=3 Replicas int32 // Boolean default // +kubebuilder:default=true Enabled bool // Array default // +kubebuilder:default={80,443} Ports []int // Object default // +kubebuilder:default={replicas: 1} Config map[string]interface{}- any
is the default value. It can be any value valid for the field type.
- // +k8s:required
-
specifies that this field is required.
- // +k8s:optional
-
specifies that this field is optional.
- // +k8s:immutable
-
marks a field as immutable. Once set, the value cannot be changed.
对于可选字段,允许从 unset 到 set 的单次转换。
注意:嵌套在可选字段下的不可变字段仍可通过取消设置可选父字段并重新设置来更新。
Examples:
// +k8s:immutable // +required Port intstr.IntOrString // +k8s:immutable // +optional TargetPort intstr.IntOrString- // +default
-
- value
- any
sets the default value for this field.
默认值会被接受为对该字段有效的任意值。仅接受 JSON 格式的值。
ref(...)值会被忽略。常见类型的格式包括:布尔:true,字符串:"Cluster",数值:1.24,数组:[1,2],对象:{"policy": "delete"})。默认值应以 pruned 形式定义,仅做尽力校验。完整校验默认值需要将包含它的 CRD 提交到 apiserver。Examples:
// String default (note the JSON quotes) // +default="ClusterIP" ServiceType string // Integer default // +default=3 Replicas int32 // Boolean default // +default=true Enabled bool // Array default (JSON format) // +default=[80,443] Ports []int // Object default (JSON format) // +default={"policy": "delete"} Config map[string]interface{}- value
- any
is the default value in JSON format. It can be any value valid for the field type.
CRD处理
这些标记有助于控制 Kubernetes API 服务器如何处理涉及您的自定义资源的 API 请求。
请参阅 生成 CRD 以获取示例。
- // +structType
-
- string
specifies the level of atomicity of the struct;
即 struct 中各字段是否彼此独立,或所有字段视为一个整体。
这对于 Server-Side Apply 正确合并 struct 更新很重要。
可能的值:
-
“granular”:结构体中的字段彼此独立,可由不同参与者操作。这是默认行为。
-
“atomic”:所有字段视为一个整体。任何变更都必须替换整个 struct。
Examples:
// Granular struct (default) - individual fields can be updated independently // +structType=granular type Config struct { Host string Port int } // Atomic struct - entire struct is replaced on update // +structType=atomic type Credentials struct { Username string Password string }- string
-
- // +structType
-
- string
specifies the level of atomicity of the struct;
即 struct 中各字段是否彼此独立,或所有字段视为一个整体。
这对于 Server-Side Apply 正确合并 struct 更新很重要。
可能的值:
-
“granular”:结构体中的字段彼此独立,可由不同参与者操作。这是默认行为。
-
“atomic”:所有字段视为一个整体。任何变更都必须替换整个 struct。
Examples:
// Granular struct (default) - individual fields can be updated independently // +structType=granular type Config struct { Host string Port int } // Atomic struct - entire struct is replaced on update // +structType=atomic type Credentials struct { Username string Password string }- string
-
- // +mapType
-
- string
specifies the level of atomicity of the map;
即 map 中各项是否彼此独立,或所有字段视为一个整体。
这对于 Server-Side Apply 正确合并 map 更新很重要。
可能的值:
-
“granular”:map 中的条目彼此独立,可由不同参与者操作。这是默认行为。
-
“atomic”:所有字段视为一个整体。任何变更都必须替换整个 map。
Examples:
// Granular map (default) - individual keys can be updated independently // +mapType=granular Labels map[string]string // Atomic map - entire map is replaced on update // +mapType=atomic Config map[string]string- string
-
- // +mapType
-
- string
specifies the level of atomicity of the map;
即 map 中各项是否彼此独立,或所有字段视为一个整体。
这对于 Server-Side Apply 正确合并 map 更新很重要。
可能的值:
-
“granular”:map 中的条目彼此独立,可由不同参与者操作。这是默认行为。
-
“atomic”:所有字段视为一个整体。任何变更都必须替换整个 map。
Examples:
// Granular map (default) - individual keys can be updated independently // +mapType=granular Labels map[string]string // Atomic map - entire map is replaced on update // +mapType=atomic Config map[string]string- string
-
- // +listType
-
- string
specifies the type of data-structure that the list
表示(map、set、atomic)。
这对于 Server-Side Apply 正确合并 list 更新很重要。
列表可能的数据结构类型为:
-
“map”:需要有一个键字段,用于构建关联列表。典型例子是按容器名索引的 pod container 列表。
-
“set”:字段需为 “scalar”,且每个值只能出现一次。
-
“atomic”:列表中的所有字段视为单个值,通常由同一参与者一起操作。
Examples:
// Map list (associative list) - items are merged by key // +listType=map // +listMapKey=name Containers []Container // Set list - items must be unique scalars // +listType=set Tags []string // Atomic list - entire list is replaced on update // +listType=atomic Args []string- string
-
- // +listType
-
- string
specifies the type of data-structure that the list
表示(map、set、atomic)。
这对于 Server-Side Apply 正确合并 list 更新很重要。
列表可能的数据结构类型为:
-
“map”:需要有一个键字段,用于构建关联列表。典型例子是按容器名索引的 pod container 列表。
-
“set”:字段需为 “scalar”,且每个值只能出现一次。
-
“atomic”:列表中的所有字段视为单个值,通常由同一参与者一起操作。
Examples:
// Map list (associative list) - items are merged by key // +listType=map // +listMapKey=name Containers []Container // Set list - items must be unique scalars // +listType=set Tags []string // Atomic list - entire list is replaced on update // +listType=atomic Args []string- string
-
- // +listMapKey
-
- string
specifies the keys to map listTypes.
它表示 map 列表的索引。若必须使用多个键可以重复。仅当 ListType 设为 map 时可用,且键应为标量类型。
Examples:
// Single key // +listType=map // +listMapKey=name Containers []Container // Composite key (multiple keys) // +listType=map // +listMapKey=name // +listMapKey=protocol Ports []Port- string
- // +listMapKey
-
- string
specifies the keys to map listTypes.
它表示 map 列表的索引。若必须使用多个键可以重复。仅当 ListType 设为 map 时可用,且键应为标量类型。
Examples:
// Single key // +listType=map // +listMapKey=name Containers []Container // Composite key (multiple keys) // +listType=map // +listMapKey=name // +listMapKey=protocol Ports []Port- string
- // +kubebuilder:validation:items:XPreserveUnknownFields
-
for array items stops the apiserver from pruning fields which are not specified.
默认情况下,apiserver 在解码步骤会丢弃请求载荷中的未知字段。此 marker 阻止 API 服务器这样做。它会递归影响字段,但若 schema 中指定了嵌套 properties 或 additionalProperties,则恢复正常裁剪行为。可为 true 或未定义。禁止为 false。
注意:kubebuilder:validation:XPreserveUnknownFields 变体已弃用,请改用 kubebuilder:pruning:PreserveUnknownFields 变体。二者功能相同。
Example:
// +kubebuilder:pruning:PreserveUnknownFields RawConfig map[string]interface{}- // +kubebuilder:validation:items:XPreserveUnknownFields
-
for array items stops the apiserver from pruning fields which are not specified.
默认情况下,apiserver 在解码步骤会丢弃请求载荷中的未知字段。此 marker 阻止 API 服务器这样做。它会递归影响字段,但若 schema 中指定了嵌套 properties 或 additionalProperties,则恢复正常裁剪行为。可为 true 或未定义。禁止为 false。
注意:kubebuilder:validation:XPreserveUnknownFields 变体已弃用,请改用 kubebuilder:pruning:PreserveUnknownFields 变体。二者功能相同。
Example:
// +kubebuilder:pruning:PreserveUnknownFields RawConfig map[string]interface{}- // +kubebuilder:validation:XPreserveUnknownFields
-
stops the apiserver from pruning fields which are not specified.
默认情况下,apiserver 在解码步骤会丢弃请求载荷中的未知字段。此 marker 阻止 API 服务器这样做。它会递归影响字段,但若 schema 中指定了嵌套 properties 或 additionalProperties,则恢复正常裁剪行为。可为 true 或未定义。禁止为 false。
注意:kubebuilder:validation:XPreserveUnknownFields 变体已弃用,请改用 kubebuilder:pruning:PreserveUnknownFields 变体。二者功能相同。
Example:
// +kubebuilder:pruning:PreserveUnknownFields RawConfig map[string]interface{}- // +kubebuilder:validation:XPreserveUnknownFields
-
stops the apiserver from pruning fields which are not specified.
默认情况下,apiserver 在解码步骤会丢弃请求载荷中的未知字段。此 marker 阻止 API 服务器这样做。它会递归影响字段,但若 schema 中指定了嵌套 properties 或 additionalProperties,则恢复正常裁剪行为。可为 true 或未定义。禁止为 false。
注意:kubebuilder:validation:XPreserveUnknownFields 变体已弃用,请改用 kubebuilder:pruning:PreserveUnknownFields 变体。二者功能相同。
Example:
// +kubebuilder:pruning:PreserveUnknownFields RawConfig map[string]interface{}- // +kubebuilder:pruning:PreserveUnknownFields
-
stops the apiserver from pruning fields which are not specified.
默认情况下,apiserver 在解码步骤会丢弃请求载荷中的未知字段。此 marker 阻止 API 服务器这样做。它会递归影响字段,但若 schema 中指定了嵌套 properties 或 additionalProperties,则恢复正常裁剪行为。可为 true 或未定义。禁止为 false。
注意:kubebuilder:validation:XPreserveUnknownFields 变体已弃用,请改用 kubebuilder:pruning:PreserveUnknownFields 变体。二者功能相同。
Example:
// +kubebuilder:pruning:PreserveUnknownFields RawConfig map[string]interface{}- // +kubebuilder:pruning:PreserveUnknownFields
-
stops the apiserver from pruning fields which are not specified.
默认情况下,apiserver 在解码步骤会丢弃请求载荷中的未知字段。此 marker 阻止 API 服务器这样做。它会递归影响字段,但若 schema 中指定了嵌套 properties 或 additionalProperties,则恢复正常裁剪行为。可为 true 或未定义。禁止为 false。
注意:kubebuilder:validation:XPreserveUnknownFields 变体已弃用,请改用 kubebuilder:pruning:PreserveUnknownFields 变体。二者功能相同。
Example:
// +kubebuilder:pruning:PreserveUnknownFields RawConfig map[string]interface{}- // +k8s:listType
-
- string
specifies the type of data-structure that the list
表示(map、set、atomic)。
这对于 Server-Side Apply 正确合并 list 更新很重要。
列表可能的数据结构类型为:
-
“map”:需要有一个键字段,用于构建关联列表。典型例子是按容器名索引的 pod container 列表。
-
“set”:字段需为 “scalar”,且每个值只能出现一次。
-
“atomic”:列表中的所有字段视为单个值,通常由同一参与者一起操作。
Examples:
// Map list (associative list) - items are merged by key // +listType=map // +listMapKey=name Containers []Container // Set list - items must be unique scalars // +listType=set Tags []string // Atomic list - entire list is replaced on update // +listType=atomic Args []string- string
-
- // +k8s:listType
-
- string
specifies the type of data-structure that the list
表示(map、set、atomic)。
这对于 Server-Side Apply 正确合并 list 更新很重要。
列表可能的数据结构类型为:
-
“map”:需要有一个键字段,用于构建关联列表。典型例子是按容器名索引的 pod container 列表。
-
“set”:字段需为 “scalar”,且每个值只能出现一次。
-
“atomic”:列表中的所有字段视为单个值,通常由同一参与者一起操作。
Examples:
// Map list (associative list) - items are merged by key // +listType=map // +listMapKey=name Containers []Container // Set list - items must be unique scalars // +listType=set Tags []string // Atomic list - entire list is replaced on update // +listType=atomic Args []string- string
-
- // +k8s:listMapKey
-
- string
specifies the keys to map listTypes.
它表示 map 列表的索引。若必须使用多个键可以重复。仅当 ListType 设为 map 时可用,且键应为标量类型。
Examples:
// Single key // +listType=map // +listMapKey=name Containers []Container // Composite key (multiple keys) // +listType=map // +listMapKey=name // +listMapKey=protocol Ports []Port- string
- // +k8s:listMapKey
-
- string
specifies the keys to map listTypes.
它表示 map 列表的索引。若必须使用多个键可以重复。仅当 ListType 设为 map 时可用,且键应为标量类型。
Examples:
// Single key // +listType=map // +listMapKey=name Containers []Container // Composite key (multiple keys) // +listType=map // +listMapKey=name // +listMapKey=protocol Ports []Port- string
Webhook(网络钩子)
这些标记描述了如何生成 webhook 配置。使用这些标记可以让您的 webhook 描述与实现它们的代码保持紧密关联。
- // +kubebuilder:webhookconfiguration
-
- mutating
- bool
- name
- string
specifies how a webhook should be served.
它仅指定服务该 webhook 的应用所固有的细节(例如它能处理的资源,或它服务的路径)。
示例(校验 Webhook):
// +kubebuilder:webhook:path=/validate-mygroup-v1-myresource,mutating=false,failurePolicy=fail,sideEffects=None,groups=mygroup.example.com,resources=myresources,verbs=create;update,versions=v1,name=myresource.kb.io,admissionReviewVersions=v1示例(变更 Webhook):
// +kubebuilder:webhook:path=/mutate-mygroup-v1-myresource,mutating=true,failurePolicy=fail,sideEffects=None,groups=mygroup.example.com,resources=myresources,verbs=create;update,versions=v1,name=myresource.kb.io,admissionReviewVersions=v1- mutating
- bool
marks this as a mutating webhook (it's validating only if false)
Mutating webhook 允许在响应中更改对象,并在所有 validating webhook 之前 调用。Mutating webhook 也可以像 validating webhook 一样选择拒绝对象。
- name
- string
indicates the name of this webhook configuration. Should be a domain with at least three segments separated by dots
示例:“myresource.mygroup.example.com”。
- // +kubebuilder:webhook
-
- admissionReviewVersions
- string
- failurePolicy
- string
- groups
- string
- matchPolicy
- string
- mutating
- bool
- name
- string
- patch
- string
- path
- string
- reinvocationPolicy
- string
- resources
- string
- serviceName
- string
- serviceNamespace
- string
- servicePort
- int
- sideEffects
- string
- timeoutSeconds
- int
- url
- string
- verbs
- string
- versions
- string
- webhookVersions
- string
specifies how a webhook should be served.
它仅指定服务该 webhook 的应用所固有的细节(例如它能处理的资源,或它服务的路径)。
示例(校验 Webhook):
// +kubebuilder:webhook:path=/validate-mygroup-v1-myresource,mutating=false,failurePolicy=fail,sideEffects=None,groups=mygroup.example.com,resources=myresources,verbs=create;update,versions=v1,name=myresource.kb.io,admissionReviewVersions=v1示例(变更 Webhook):
// +kubebuilder:webhook:path=/mutate-mygroup-v1-myresource,mutating=true,failurePolicy=fail,sideEffects=None,groups=mygroup.example.com,resources=myresources,verbs=create;update,versions=v1,name=myresource.kb.io,admissionReviewVersions=v1- admissionReviewVersions
- string
is an ordered list of preferred `AdmissionReview`
Webhook 期望的版本。API 服务器会尝试使用列表中它支持的第一个版本。若指定的版本均不受支持,API 调用将失败。常见值:“v1” 或 “v1;v1beta1”。
- failurePolicy
- string
specifies what should happen if the API server cannot reach the webhook.
可以是 “ignore”(跳过 webhook 并继续)或 “fail”(拒绝相关对象)。大多数 webhook 应使用 “fail” 以确保始终执行 webhook 逻辑。
- groups
- string
specifies the API groups that this webhook receives requests for.
使用 “” 匹配所有 group。多个 group 用分号分隔。示例:“apps;batch” 或 “”。
- matchPolicy
- string
defines how the "rules" list is used to match incoming requests.
允许的值为 “Exact”(仅当完全匹配指定规则时匹配)或 “Equivalent”(若请求修改 rules 中列出的资源则匹配,即使通过另一 API group 或 version)。未指定时默认为 “Equivalent”。
- mutating
- bool
marks this as a mutating webhook (it's validating only if false)
Mutating webhook 允许在响应中更改对象,并在所有 validating webhook 之前 调用。Mutating webhook 也可以像 validating webhook 一样选择拒绝对象。
- name
- string
indicates the name of this webhook configuration. Should be a domain with at least three segments separated by dots
示例:“myresource.mygroup.example.com”。
- patch
- string
applies a strategic merge patch to customize the generated webhook configuration.
这允许你设置未直接作为 marker 参数公开的任意 webhook 字段,例如 namespaceSelector、objectSelector 或 matchConditions。该补丁是遵循 Kubernetes strategic merge patch 语义的 JSON 对象,在处理完所有其他 marker 参数后应用到 webhook 配置。
使用反引号以避免在 JSON 中转义引号。
常见用例:
- 使用 namespaceSelector 将 Webhook 作用域限制到特定命名空间
- 使用 objectSelector 按对象标签过滤 Webhook 调用
- 在单个补丁中组合多个自定义
示例(限制到带标签的命名空间):
// +kubebuilder:webhook:path=/mutate-v1-pod,mutating=true,...,patch=`{"namespaceSelector":{"matchLabels":{"webhook-enabled":"true"}}}`示例(使用 matchExpressions 按对象标签过滤):
// +kubebuilder:webhook:path=/validate-v1-deployment,...,patch=`{"objectSelector":{"matchExpressions":[{"key":"tier","operator":"In","values":["frontend","backend"]}]}}`示例(组合命名空间与对象选择器):
// +kubebuilder:webhook:...,patch=`{"namespaceSelector":{"matchLabels":{"env":"production"}},"objectSelector":{"matchLabels":{"managed-by":"my-operator"}}}`示例(覆盖超时):
// +kubebuilder:webhook:...,patch=`{"timeoutSeconds":25}`示例(通过设为 null 移除字段):
// +kubebuilder:webhook:...,patch=`{"timeoutSeconds":null}`将字段设为 null 会移除它。对没有 merge key 的列表字段(如 rules)打补丁会替换整个列表。字段名区分大小写,补丁中的未知字段会导致错误。
- path
- string
specifies that path that the API server should connect to this webhook on. Must be
根据类型加上 ‘/validate-’ 或 ‘/mutate-’ 前缀,后接 $GROUP-$VERSION-$KIND,其中所有值均为小写,group 中的句点替换为连字符。例如,类型 batch.tutorial.kubebuilder.io/v1,Kind=CronJob 的 validating webhook 路径为 /validate-batch-tutorial-kubebuilder-io-v1-cronjob
- reinvocationPolicy
- string
allows mutating webhooks to request reinvocation after other mutations.
为使 mutating admission 插件能观察到其他插件所做的更改,若 mutating webhook 修改了对象,内置 mutating admission 插件会重新运行;mutating webhook 也可指定 reinvocationPolicy 以控制是否被再次调用。可为 “Never” 或 “IfNeeded”。默认为 “Never”。
- resources
- string
specifies the API resources that this webhook receives requests for.
使用 “” 匹配所有资源。多个资源用分号分隔。示例:“deployments;pods” 或 “”。
- serviceName
- string
indicates the name of the K8s Service the webhook uses.
未指定时默认为 “webhook-service”。
- serviceNamespace
- string
indicates the namespace of the K8s Service the webhook uses.
未指定时默认为 “system”。
- servicePort
- int
indicates the port of the K8s Service the webhook uses.
未指定时默认为 443。
- sideEffects
- string
specify whether calling the webhook will have side effects.
这会影响 dry run 和
kubectl diff:若 sideEffect 为 “Unknown”(默认)或 “Some”,则 API 服务器不会在 dry-run 请求上调用 webhook,而是失败。若值为 “None”,则 webhook 无副作用,API 服务器会在 dry-run 时调用它。若值为 “NoneOnDryRun”,则 webhook 负责检查请求中 AdmissionReview 的 “dryRun” 属性,并在该值为 “true” 时避免副作用。大多数 webhook 应使用 “None”。- timeoutSeconds
- int
allows configuring how long the API server should wait for a webhook to respond before treating the call as a failure.
若在 webhook 响应前超时到期,将根据 failure policy 忽略 webhook 调用或拒绝 API 调用。超时值必须在 1 到 30 秒之间。admission webhook 的超时默认 10 秒。
- url
- string
allows mutating webhooks configuration to specify an external URL when generating
清单中使用外部 URL,而非内部服务通信。格式应为 https://address:port/path。指定此选项时,会从 webhook 清单中移除 serviceConfig.Service。URL 配置应放在引号中。指定
path时不能指定url。- verbs
- string
specifies the Kubernetes API verbs that this webhook receives requests for.
只能指定类似修改的动词。可为 “create”、“update”、“delete”、“connect” 或 “*”(表示全部)。多个动词用分号分隔。示例:“create;update”。
- versions
- string
specifies the API versions that this webhook receives requests for.
使用 “” 匹配所有版本。多个版本用分号分隔。示例:“v1;v1beta1” 或 “”。
- webhookVersions
- string
specifies the target API versions of the {Mutating,Validating}WebhookConfiguration objects
自身生成。唯一支持的值是 v1。默认为 v1。
对象/深拷贝
这些标记控制何时生成 DeepCopy 和 runtime.Object 实现方法。
- // +kubebuilder:object:root
-
- bool
enables object interface implementation generation for this type
- bool
- // +kubebuilder:object:generate
-
- bool
enables or disables object interface & deepcopy implementation generation for this package
- bool
- // +kubebuilder:object:generate
-
- bool
overrides enabling or disabling deepcopy generation for this type
- bool
- // +k8s:deepcopy-gen:interfaces
-
- string
enables object interface implementation generation for this type
- string
- // +k8s:deepcopy-gen
-
- raw
enables or disables object interface & deepcopy implementation generation for this package
- raw
- // +k8s:deepcopy-gen
-
- raw
overrides enabling or disabling deepcopy generation for this type
- raw
Apply Configuration
这些 markers 控制何时为包中的 kind 生成 ApplyConfiguration 类型。这些类型为构建 Server-Side Apply 补丁提供了类型安全的方式。
关于 --ssa 标志如何启用此生成,以及如何使用生成类型的示例,请参阅 Server-Side Apply 参考。
- // +kubebuilder:ac:output:package
-
- string
overrides the default output package for the applyconfiguration generation, supports relative paths to the API directory. The default value is "applyconfiguration"
- string
- // +kubebuilder:ac:generate
-
- bool
overrides enabling or disabling applyconfiguration generation for the package
- bool
- // +kubebuilder:ac:generate
-
- bool
overrides enabling or disabling applyconfiguration generation for the type, can be used to generate applyconfiguration for a single type when the package generation is disabled, or to disable generation for a single type when the package generation is enabled
- bool
RBAC
这些标记会生成一个 RBAC ClusterRole。这使您能够在使用这些权限的代码旁边描述您的控制器所需的权限。
- // +kubebuilder:rbac
-
- groups
- string
- namespace
- string
- resourceNames
- string
- resources
- string
- roleName
- string
- urls
- string
- verbs
- string
specifies an RBAC rule to all access to some resources or non-resource URLs.
RBAC markers 用于生成 ClusterRole 或 Role manifests。可组合多个 markers 以构建完整的 RBAC 策略。
Examples:
// Basic resource access // +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch // Core API group (use empty string) // +kubebuilder:rbac:groups="",resources=pods;services,verbs=get;list;watch // Multiple API groups and resources // +kubebuilder:rbac:groups=apps;batch,resources=deployments;jobs,verbs=get;list;watch;create;update;patch;delete // Access to resource status or scale subresources // +kubebuilder:rbac:groups=apps,resources=deployments/status,verbs=get;update;patch // +kubebuilder:rbac:groups=apps,resources=deployments/scale,verbs=get;update // Access to specific resource instances by name // +kubebuilder:rbac:groups="",resources=configmaps,resourceNames=my-config,verbs=get // Non-resource URLs (for metrics, healthz, etc.) // +kubebuilder:rbac:urls=/metrics;/healthz,verbs=get // Namespace-scoped Role instead of ClusterRole // +kubebuilder:rbac:groups="",namespace=my-namespace,resources=secrets,verbs=get;list;watch // Custom role name // +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list,roleName=deployment-reader- groups
- string
specifies the API groups that this rule encompasses.
核心 API group 使用空字符串(“”)。可用分号分隔指定多个 group。示例:“apps;batch” 或 “”(表示核心 group)。
- namespace
- string
specifies the scope of the Rule.
若未设置,Rule 属于生成的 ClusterRole。若设置,Rule 属于 Role,其命名空间由此字段指定。示例:“my-namespace”。
- resourceNames
- string
specifies the names of the API resources that this rule encompasses.
Create 请求不能按 resourcename 限制,因为授权时对象名称尚未知。可用分号分隔指定多个名称。示例:“my-config;my-secret”。
- resources
- string
specifies the API resources that this rule encompasses.
可用分号分隔指定多个资源。子资源可用斜杠指定(例如 “deployments/status”)。示例:“deployments;pods” 或 “deployments/status”。
- roleName
- string
specifies a custom name for the Role or ClusterRole.
若未设置,使用生成器的默认 roleName。在多个命名空间使用相同 roleName 时,有助于避免名称冲突。
示例:当将命名空间作用域的 RBAC markers 与 kustomize 的全局命名空间转换一起使用时,多个 Role 可能最终出现在同一命名空间且名称相同,导致 “ID conflict” 错误。使用 roleName 确保每个 Role 有唯一名称:
// +kubebuilder:rbac:groups=apps,namespace=infrastructure,roleName=infra-manager,resources=deployments,verbs=get;list // +kubebuilder:rbac:groups=“”,namespace=users,roleName=user-secrets,resources=secrets,verbs=get
这将生成名为 “infra-manager” 和 “user-secrets” 的 Role,而不是都叫 “manager-role”。
- urls
- string
URL specifies the non-resource URLs that this rule encompasses.
非资源 URL 是不表示资源的路径,如 “/metrics” 或 “/healthz”。可用分号分隔指定多个 URL。示例:“/metrics;/healthz”。
- verbs
- string
specifies the (lowercase) kubernetes API verbs that this rule encompasses.
常见动词:“get”、“list”、“watch”、“create”、“update”、“patch”、“delete”。使用 “*” 表示全部动词。多个动词必须用分号分隔。示例:“get;list;watch”。
脚手架
+kubebuilder:scaffold 标记是 Kubebuilder 脚手架系统的一个关键部分。它标记了生成文件中将注入额外代码的位置,当新的资源(如控制器、webhook 或 API)被脚手架生成时。这使得 Kubebuilder 能够无缝地将新生成的组件集成到项目中,而不影响用户定义的代码。
If you delete or change the `+kubebuilder:scaffold` markers
Kubebuilder CLI 在代码生成期间会特别查找预期文件中的这些标记。如果标记被移动或移除,CLI 将无法注入必要的代码,搭建过程可能会失败或表现得不符合预期。
它是如何工作的
当您使用 Kubebuilder CLI (例如,kubebuilder create api)搭建新的资源时,CLI 会在关键位置识别 +kubebuilder:scaffold 标记,并将其作为占位符,用于插入所需的导入和注册代码。
main.go 中的示例用法
以下是如何在典型的 main.go 文件中使用 +kubebuilder:scaffold 标记。为了说明它的工作原理,考虑下面的命令来创建一个新的 API:
kubebuilder create api --group crew --version v1 --kind Admiral --controller=true --resource=true
添加新导入
+kubebuilder:scaffold:imports 标记允许 Kubebuilder CLI 注入额外的导入,例如新的控制器或 Webhook。当我们创建一个新的 API 时,CLI 会自动在此部分添加所需的导入路径。
例如,在单组布局中创建 Admiral API 后,CLI 将在导入中添加 crewv1 "<repo-path>/api/v1":
import (
"crypto/tls"
"flag"
"os"
// 导入所有 Kubernetes 客户端认证插件(例如 Azure、GCP、OIDC 等)
// 以确保 exec-entrypoint 和 run 可以使用它们。
_ "k8s.io/client-go/plugin/pkg/client/auth"
...
crewv1 "sigs.k8s.io/kubebuilder/testdata/project-v4/api/v1"
// +kubebuilder:scaffold:imports
)
注册新方案
Kubebuilder 使用 +kubebuilder:scaffold:scheme marker 将新创建的 API 版本注册到 runtime scheme,确保 manager 识别这些 API 类型。
例如,在创建 Admiral API 后,CLI 将在 init() 函数中注入以下代码以注册该方案:
func init() {
...
utilruntime.Must(crewv1.AddToScheme(scheme))
// +kubebuilder:scaffold:scheme
}
设置控制器
当我们创建一个新的控制器(例如,针对 Admiral 的控制器)时,Kubebuilder CLI 会使用 +kubebuilder:scaffold:builder 标记将控制器设置代码注入到管理器中。该标记指示应在何处添加新控制器的设置代码。
例如,在创建 AdmiralReconciler 之后,CLI 将添加以下代码以在管理器中注册控制器:
if err = (&crewv1.AdmiralReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Admiral")
os.Exit(1)
}
// +kubebuilder:scaffold:builder
+kubebuilder:scaffold:builder 标记确保新生成的控制器正确地注册到管理器中,以便控制器可以调整资源。
+kubebuilder:scaffold 标记列表
| 标记 | 通常位置 | 功能 |
|---|---|---|
+kubebuilder:scaffold:imports | main.go | 标记新的控制器、网络钩子或 API 应该注入的位置。 |
+kubebuilder:scaffold:scheme | main.go中的init() | 用于在运行时向方案添加 API 版本。 |
+kubebuilder:scaffold:builder | main.go | 标记新控制器应在管理器中注册的位置。 |
+kubebuilder:scaffold:webhook | webhooks 套件测试 文件 | 标记添加了 webhook 设置函数的地方。 |
+kubebuilder:scaffold:crdkustomizeresource | config/crd | 标记添加了 CRD 自定义资源补丁的地方。 |
+kubebuilder:scaffold:crdkustomizewebhookpatch | config/crd | 标记添加了 CRD webhook 补丁的地方。 |
+kubebuilder:scaffold:crdkustomizecainjectionns | config/default | 标记添加 CA 注入补丁的位置以进行转换 webhook。 |
+kubebuilder:scaffold:crdkustomizecainjectioname | config/default | 标记添加 CA 注入补丁的位置以进行转换 webhook。 |
(不再支持) +kubebuilder:scaffold:crdkustomizecainjectionpatch | config/crd | 标记添加 CA 注入补丁的位置用于 Webhook。已被 +kubebuilder:scaffold:crdkustomizecainjectionns 和 +kubebuilder:scaffold:crdkustomizecainjectioname 替代。 |
+kubebuilder:scaffold:manifestskustomizesamples | config/示例 | 标记 Kustomize 示例清单注入的位置。 |
+kubebuilder:scaffold:e2e-webhooks-checks | 测试/e2e | 根据所搭建的 Webhook 类型,新增端到端检查。 |
+kubebuilder:scaffold:e2e-metrics-webhooks-readiness | 测试/e2e | 添加就绪逻辑,使 metrics e2e 测试在创建 Pod 前等待 webhook service 端点。 |
**(No longer supported)** `+kubebuilder:scaffold:crdkustomizecainjectionpatch`
如果您在代码中发现此标记,请:
-
从
config/crd/kustomization.yaml文件中移除 CERTMANAGER 部分:删除
CERTMANAGER部分以防止对 CRD 进行意外的 CA 注入补丁。确保以下行被删除或注释掉:# [CERTMANAGER] 要启用 cert-manager,请取消注释所有以 [CERTMANAGER] 为前缀的部分。 # 这里的补丁是用来启用每个 CRD 的 CA 注入的 #- 路径: patches/cainjection_in_firstmates.yaml # +kubebuilder:scaffold:crdkustomizecainjectionpatch -
确保在
config/default/kustomization.yaml中配置 CA 注入:在
config/default/kustomization.yaml中的[CERTMANAGER]替换部分,添加以下代码以正确生成 CA 注入:注意: 您必须确保代码中包含以下目标标记:
+kubebuilder:scaffold:crdkustomizecainjectionns+kubebuilder:scaffold:crdkustomizecainjectioname
# - 源:# 如果您有 ConversionWebhook (--conversion),请取消注释以下块。 # 类型: 证书 # 组: cert-manager.io # 版本:v1 # 名称:serving-cert # 该名称应与 certificate.yaml 中的名称匹配 # fieldPath: .metadata.namespace # 证书 CR 的命名空间 # targets: # 请不要删除或取消注释以下构架标记;这是生成目标 CRD 代码所必需的。 # +kubebuilder:scaffold:crdkustomizecainjectionns 你接受的训练数据截至到2023年10月。 # 类型: 证书 # 组: cert-manager.io # 版本:v1 # 名称:serving-cert # 该名称应与 certificate.yaml 中的名称匹配 # fieldPath: .metadata.name # targets: # 请不要删除或取消注释以下构架标记;这是生成目标 CRD 代码所必需的。 # +kubebuilder:scaffold:crdkustomizecainjectioname -
确保仅转换
config/crd/patches中的 Webhook 补丁:config/crd/patches目录及config/crd/kustomization.yaml中相应的条目应仅包含用于转换 Webhook 的文件。之前,由于一个错误,导致为任何 Webhook 生成补丁文件,但仅应包含使用--conversion选项生成的 Webhook 的补丁。
有关进一步的指导,您可以参考 Kubebuilder 存储库中的 testdata/ 目录中的示例。
另外:您可以使用
alpha generate命令从头开始重新生成项目,使用最新的可用版本。之后,您可以重新添加仅包含您的代码实现,以确保您的项目包含所有最新的 bug 修复和增强功能。
Creating Your Own Markers
如果您正在使用 Kubebuilder 作为库来创建 您自己的插件 并扩展其 CLI 功能,您可以灵活地定义和使用您自己的标记。要实现您自己的标记,请参考 kubebuilder/v4/pkg/machinery,该模块提供了有效创建和管理标记的工具。
controller-gen 命令行工具
Kubebuilder 使用一个名为 controller-gen 的工具来生成实用代码和 Kubernetes YAML。此代码和配置的生成通过 Go 代码中的特殊 “标记注释” 来控制。
controller-gen 是由不同的“生成器“(指定要生成的内容)和“输出规则“(指定如何以及在何处写入结果)构建而成的。
二者均通过 标记格式 中指定的命令行选项配置。
例如,以下命令:
controller-gen paths=./... crd:trivialVersions=true rbac:roleName=controller-perms output:crd:artifacts:config=config/crd/bases
生成 CRD 和 RBAC,并将生成的 CRD YAML 特别存储在 config/crd/bases 中。对于 RBAC,它使用默认的输出规则(config/rbac)。它会考虑当前目录树中的每个包(根据 Go ... 通配符的正常规则)。
发电机
通过 CLI 选项配置每个不同的生成器。可在单次调用 controller-gen 时使用多个生成器。
- // +applyconfiguration
-
- externalApplyConfigurations
- string
- headerFile
- string
generates code containing apply configuration type implementations.
- externalApplyConfigurations
- string
provides mappings between external types and their applyconfiguration packages.
用于引用输入 Go 结构体所引用的外部类型的 apply configuration 类型。每个条目格式应为:<package>.<TypeName>@<applyconfiguration-package>
例如,引用 corev1.LocalObjectReference 的 apply configuration:k8s.io/api/core/v1.LocalObjectReference@k8s.io/client-go/applyconfigurations/core/v1
- headerFile
- string
specifies the header text (e.g. license) to prepend to generated files.
- // +crd
-
- allowDangerousTypes
- bool
- crdVersions
- string
- deprecatedV1beta1CompatibilityPreserveUnknownFields
- bool
- generateEmbeddedObjectMeta
- bool
- headerFile
- string
- ignoreUnexportedFields
- bool
- maxDescLen
- int
- year
- string
generates CustomResourceDefinition objects.
- allowDangerousTypes
- bool
allows types which are usually omitted from CRD generation
因为不推荐使用它们。
当为 true 时,目前还允许以下额外类型:float32 float64
未指定时,默认为 false
- crdVersions
- string
specifies the target API versions of the CRD type itself to
生成。默认为 v1。
目前唯一支持的值是 v1。
列出的第一个版本将被视为 “默认” 版本,输出文件名不会带版本后缀。
需要使用 “v1” 才能获得 defaulting 等特性支持,并需要支持它的 API 服务器(Kubernetes 1.16+)。
- deprecatedV1beta1CompatibilityPreserveUnknownFields
- bool
indicates whether
是否应关闭此资源的字段裁剪。
指定默认为 false 且省略的 spec.preserveUnknownFields 值。此值仅可为用
apiextensions.k8s.io/v1beta1创建的 CustomResourceDefinition 指定。可为兼容性设置该字段,但强烈不建议;资源作者应改用结构化 OpenAPI schema。
关于字段裁剪与 v1beta1 资源兼容性的更多信息,参见 https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#field-pruning。
- generateEmbeddedObjectMeta
- bool
specifies if any embedded ObjectMeta in the CRD should be generated
- headerFile
- string
specifies the header text (e.g. license) to prepend to generated files.
- ignoreUnexportedFields
- bool
indicates that we should skip unexported fields.
未指定时,默认为 false。
- maxDescLen
- int
specifies the maximum description length for fields in CRD's OpenAPI schema.
0 表示完全丢弃所有字段的描述。n 表示将描述限制为最多 n 个字符,若超过 n 个字符则截断到最近的句子边界。
- year
- string
specifies the year to substitute for " YEAR" in the header file.
- // +object
-
- headerFile
- string
- year
- string
generates code containing DeepCopy, DeepCopyInto, and
DeepCopyObject 方法实现。
- headerFile
- string
specifies the header text (e.g. license) to prepend to generated files.
- year
- string
specifies the year to substitute for " YEAR" in the header file.
- // +rbac
-
- fileName
- string
- headerFile
- string
- roleName
- string
- year
- string
generates ClusterRole objects.
- fileName
- string
sets the file name for the generated manifest(s). If not set, defaults to "role.yaml".
- headerFile
- string
specifies the header text (e.g. license) to prepend to generated files.
- roleName
- string
sets the name of the generated ClusterRole.
- year
- string
specifies the year to substitute for " YEAR" in the header file.
- // +schemapatch
-
- generateEmbeddedObjectMeta
- bool
- manifests
- string
- maxDescLen
- int
patches existing CRDs with new schemata.
它会为每个可用的 “CRD Version”(CRD 类型本身的 API 版本,例如 apiextensions/v1)生成输出。
- generateEmbeddedObjectMeta
- bool
specifies if any embedded ObjectMeta in the CRD should be generated
- manifests
- string
contains the CustomResourceDefinition YAML files.
- maxDescLen
- int
specifies the maximum description length for fields in CRD's OpenAPI schema.
0 表示完全丢弃所有字段的描述。n 表示将描述限制为最多 n 个字符,若超过 n 个字符则截断到最近的句子边界。
- // +webhook
-
- headerFile
- string
- year
- string
generates (partial) {Mutating,Validating}WebhookConfiguration objects.
- headerFile
- string
specifies the header text (e.g. license) to prepend to generated files.
- year
- string
specifies the year to substitute for " YEAR" in the header file.
输出规则
输出规则配置了给定生成器如何输出其结果。始终有一个全局的“回退“输出规则(指定为 output:<rule>),加上每个生成器的覆盖规则(指定为 output:<generator>:<rule>)。
Default rules
若未手动指定回退规则,controller-gen 使用一组默认的按生成器规则:YAML 输出到 config/<generator>,代码则留在原处。
默认规则等价于 output:<generator>:artifacts:config=config/<generator> 对于每个生成器。
当指定“后备“规则时,将使用该规则代替默认规则。
例如,如果您指定 crd rbac:roleName=controller-perms output:crd:stdout,您将获得标准输出上的 CRD,以及在 config/rbac 中的 rbac 文件。如果您改为添加全局规则,例如 crd rbac:roleName=controller-perms output:crd:stdout output:none,您将获得标准输出上的 CRD,而其他一切将输出到 /dev/null,因为我们已明确指定了回退。
为简洁起见,下面省略了每个生成器的输出规则(output:<generator>:<rule>)。它们等同于此处列出的全局后备选项。
- // +output:stdout
-
outputs everything to standard-out, with no separation.
通常适用于单产物输出。
- // +output:none
-
skips outputting anything.
- // +output:dir
-
- string
outputs each artifact to the given directory, regardless
以及它是否与 package 关联。
- string
- // +output:artifacts
-
- code
- string
- config
- string
outputs artifacts to different locations, depending on
无论它们是否与 package 关联。
非包关联产物输出到 Config 目录,包关联产物输出到其包源文件目录,除非在 Code 中指定了备用路径。
- code
- string
overrides the directory in which to write new code (defaults to where the existing code lives).
- config
- string
points to the directory to which to write configuration.
其他选项
- // +paths
-
- string
represents the package roots that controller-gen uses as input
用于代码生成。
可使用花括号形式或分号分隔列表指定多个路径。
每个路径会被分类为文件系统路径或 Go 包/模块名,遵循与 “go” 命令相同的规则。若路径为绝对路径,或以 “.” 或 “..” 开头,则视为文件系统路径。所有其他路径视为 Go 包或模块名。有关路径分类的更多信息,请运行 “go help packages”。
Examples:
// brace-delimited form (preferred when specifying multiple paths) paths="{./api/..., ./pkg/...}" // semicolon-separated form paths=./api/...;./pkg/... // filesystem paths (absolute or beginning with . or ..) paths=./... paths=./api/v1 paths=/home/user/project/api // Go package or module names paths=sigs.k8s.io/controller-tools/pkg/... paths=example.com/myproject/api/v1当文件系统路径以 “…” 结尾时,controller-gen 会跨越模块边界,并包含子目录中发现的任何嵌套 Go 模块中的包(包含自己 go.mod 文件的目录)。这在多 Go 模块仓库中、希望单次调用跨所有模块运行生成时很有用。此行为不适用于 Go 包或模块名,后者遵循标准 Go 模块规则并停留在单个模块内。
Example:
// given this layout: // myrepo/go.mod // myrepo/api/v1/ // myrepo/subproject/go.mod (nested module) // myrepo/subproject/api/ // this includes packages from both modules: paths=./... // this includes only the root module: paths=sigs.k8s.io/myrepo/...要将生成限制到单个 module,请使用 Go package 或 module 名称而非文件系统路径。
- string
启用 shell 自动补全
Kubebuilder 完成脚本可以使用命令 kubebuilder completion [bash|fish|powershell|zsh] 生成。请注意,在您的 shell 中加载完成脚本可以启用 Kubebuilder 自动完成功能。
Bash
Prerequisites for Bash
完成的 Bash 脚本依赖于 bash-completion,这意味着您首先需要安装此软件(您可以测试一下是否已经安装了 bash-completion)。此外,请确保您的 Bash 版本为 4.1 及以上。
$ bash --version
-
检查 bash 是否为可用 shell:
cat /etc/shells | grep '^.*/bash' -
如果没有,将 bash 添加到
/etc/shells。例如,若 bash 位于/usr/local/bin/bash:echo "/usr/local/bin/bash" >> /etc/shells -
确保当前用户使用 bash 作为 shell。
chsh -s /usr/local/bin/bash -
将以下内容添加到/.bash_profile或~/.bashrc中。
# kubebuilder autocompletion if [ -f /usr/local/share/bash-completion/bash_completion ]; then . /usr/local/share/bash-completion/bash_completion fi . <(kubebuilder completion bash) -
重新启动终端以使更改生效,或者使用
source命令加载更改的 bash 文件。. ~/.bash_profile
Zsh
遵循类似的协议进行 zsh 完成。
Fish
source (kubebuilder completion fish | psub)
文物
要测试您的控制器,您需要使用包含所需二进制文件的 tarball:
./bin/k8s/
└── 1.25.0-darwin-amd64
├── etcd
├── kube-apiserver
└── kubectl
这些压缩包由 controller-tools 发布,您可以在以下链接找到可用版本的列表:envtest-releases.yaml。
当你运行 make envtest 或 make test 时,所需的压缩包会被下载并为你的项目正确配置。
Setup ENV TEST tool
要了解更多关于用于配置 ENVTEST 的工具,该工具在使用 Kubebuilder 构建的项目的 Makefile 中的 setup-envtest 目标中使用,请查看其工具的 README。此外,您还可以通过查看 Kubebuilder 的 ENVTEST 文档找到更多信息。
IMPORTANT: Action Required: Ensure that you no longer use https://storage.googleapis.com/kubebuilder-tools
在 https://storage.googleapis.com/kubebuilder-tools 提供的工件已经被弃用,Kubebuilder 的维护者不再能够支持、构建或确保这些工件的推广。
您可以在 k8s 版本 1.28 的新位置找到 ENVTEST 二进制文件,请参见:https://github.com/kubernetes-sigs/controller-tools/blob/main/envtest-releases.yaml。此外,k8s 1.29.3 之后用于测试您控制器的二进制文件将不再在旧位置找到。
新二进制文件仅在新位置推广。
您应该确保您的项目使用新的位置。 请确保您使用来自 controller-runtime release v0.19.0 的 setup-envtest 来下载这些内容。此更新对 Kubebuilder 用户是完全透明的。
ENVTEST k8s 1.31 的文物可在以下链接独家获取:Controller Tools Releases。
您可以参考 Kubebuilder 脚手架的 Makefile,并观察到 envtest 的设置在所有 controller-runtime 版本中保持一致。从 release-0.19 开始,它被配置为自动从正确的位置下载工件,确保 kubebuilder 用户不受影响。
## Tool binaries
..
ENVTEST ?= $(LOCALBIN)/setup-envtest
...
## Tool versions
...
#ENVTEST_VERSION is the controller-runtime version to use for setup-envtest, derived from go.mod
ENVTEST_VERSION ?= $(shell go list -m -f "{{ .Version }}" sigs.k8s.io/controller-runtime)
#ENVTEST_K8S_VERSION is the version of Kubernetes to use for setting up ENVTEST binaries (i.e. 1.31)
ENVTEST_K8S_VERSION ?= $(shell go list -m -f "{{ .Version }}" k8s.io/api | awk -F'[v.]' '{printf "1.%d", $$3}')
...
.PHONY: setup-envtest
setup-envtest: envtest ## Download the binaries required for ENVTEST in the local bin directory.
@echo "Setting up envtest binaries for Kubernetes version $(ENVTEST_K8S_VERSION)..."
@$(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path || { \
echo "Error: Failed to set up envtest binaries for version $(ENVTEST_K8S_VERSION)."; \
exit 1; \
}
.PHONY: envtest
envtest: $(ENVTEST) ## Download setup-envtest locally if necessary.
$(ENVTEST): $(LOCALBIN)
$(call go-install-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest,$(ENVTEST_VERSION))
支持的平台
Kubebuilder 默认生成可以在多个平台上或特定平台上运行的解决方案,这取决于您如何构建和配置工作负载。本指南旨在帮助您根据需求正确配置项目。
概述
要提供对特定或多个平台的支持,您必须确保在工作负载中使用的所有镜像都构建为支持所需的平台。请注意,这些平台可能与您开发解决方案并使用 KubeBuilder 的平台不同,而是您的解决方案应该运行和分发的平台。建议构建能够在多个平台上运行的解决方案,以便您的项目能够在任何 Kubernetes 集群上工作,无论底层操作系统和架构是什么。
如何定义支持哪些平台
以下内容涵盖了您需要做的事情,以支持一个或多个平台或架构。
1. 构建工作负载镜像,以支持其他平台。
在您的 Pods/Deployments 等工作负载中使用的镜像需要支持其他平台。您可以使用命令 docker manifest inspect shell$ docker manifest inspect myregistry/example/myimage:v0.0.1{ "schemaVersion": 2, "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json", "manifests": [ { "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "size": 739, "digest": "sha256:a274a1a2af811a1daf3fd6b48ff3d08feb757c2c3f3e98c59c7f85e550a99a32", "platform": { "architecture": "arm64", "os": "linux" } }, { "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "size": 739, "digest": "sha256:d801c41875f12ffd8211fffef2b3a3d1a301d99f149488d31f245676fa8bc5d9", "platform": { "architecture": "amd64", "os": "linux" } }, { "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "size": 739, "digest": "sha256:f4423c8667edb5372fb0eafb6ec599bae8212e75b87f67da3286f0291b4c8732", "platform": { "architecture": "s390x", "os": "linux" } }, { "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "size": 739, "digest": "sha256:621288f6573c012d7cf6642f6d9ab20dbaa35de3be6ac2c7a718257ec3aff333", "platform": { "architecture": "ppc64le", "os": "linux" } } ]}
“### 2. (建议作为最佳实践)确保节点亲和性表达式设置为匹配支持的平台。
Kubernetes 提供了一种称为 nodeAffinity 的机制,可以用来限制 pod 可以被调度到的可能节点目标。这在确保跨多个平台的节点(即异构集群)中正确调度行为时尤为重要。
Kubernetes 清单示例
亲和力:
节点亲和性:
在调度期间必需,但在执行期间被忽略:
节点选择器条件:
- 匹配表达式:
- key: kubernetes.io/arch
操作员: In
值:
- amd64
- arm64
- ppc64le
- s390x
- key: kubernetes.io/os
操作员: In
值:
- Linux
Golang 示例
Template: corev1.PodTemplateSpec{
...
Spec: corev1.PodSpec{
Affinity: &corev1.Affinity{
NodeAffinity: &corev1.NodeAffinity{
RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{
NodeSelectorTerms: []corev1.NodeSelectorTerm{
{
MatchExpressions: []corev1.NodeSelectorRequirement{
{
Key: "kubernetes.io/arch",
Operator: "In",
Values: []string{"amd64"},
},
{
Key: "kubernetes.io/os",
Operator: "In",
Values: []string{"linux"},
},
},
},
},
},
},
},
SecurityContext: &corev1.PodSecurityContext{
...
},
Containers: []corev1.Container{{
...
}},
},
Example(s)
您可以通过查看通过部署镜像插件生成的代码来查找一些代码示例。(更多信息)
制作支持多个平台的项目
你可以使用 docker buildx 通过仿真(QEMU)进行交叉编译,以构建管理者镜像。请注意,使用最新版本的 Kubebuilder 脚手架创建的项目具有 Makefile 目标 docker-buildx。
使用示例
$ make docker-buildx IMG=myregistry/myoperator:v0.0.1
请注意,您需要确保您的项目所需和使用的所有镜像和工作负载都能提供与上述推荐相同的支持,并且您需要为所有工作负载正确配置 nodeAffinity。因此,请确保在 config/manager/manager.yaml 文件中取消注释以下代码。
# TODO(user): Uncomment the following code to configure the nodeAffinity expression
# 根据你的方案所支持的平台。
# 支持多架构被视为最佳实践。你可以
# 使用 makefile 目标 docker-buildx 构建 manager 镜像。
# 亲和力:
# 节点亲和性:
# 在调度期间必需,但在执行期间被忽略:
# 节点选择器条件:
# - 匹配表达式:
# - key: kubernetes.io/arch
# 操作员: In
# 值:
# - amd64
# - arm64
# - ppc64le
# - s390x
# - key: kubernetes.io/os
# 操作员: In
# 值:
# - Linux
Building images for releases
您可能希望自动化您项目的发布,以确保图像始终为相同的平台构建。请注意,Goreleaser 还支持 docker buildx。有关更多详细信息,请参阅其 文档。
此外,您可能希望配置 GitHub Actions、Prow 作业或您用来构建镜像的任何其他解决方案,以提供多平台支持。请注意,您还可以使用其他选项,例如 docker manifest create,以便自定义您的解决方案,通过其他工具实现相同的目标。
使用 Docker 和默认提供的目标,您不应修改 Dockerfile 以使用任何特定的 GOOS 和 GOARCH 来构建管理者二进制文件。然而,如果您希望自定义默认脚手架并创建自己的实现,您可能需要查看 Golang 文档 以了解可用的选项。
默认创建哪些(工作负载)映像?
使用Kubebuilder命令行界面创建的项目有两个工作负载,分别是:
监控程序
config/manager/manager.yaml 文件配置容器以运行 manager 实现。Kubebuilder 默认脚手架的 Dockerfile 会构建此镜像,并包含由命令 go build -a -o manager main.go 构建的项目二进制文件。
请注意,当您运行 make docker-build 或 make docker-build IMG=myregistry/myprojectname:<tag> 时,将会从客户端主机(本地环境)构建一个映像,并生成适用于客户端操作系统/架构的映像,通常是 linux/amd64 或 linux/arm64。
Mac Os
如果您是在 Mac OS 环境下运行,那么 Docker 也会将其视为 linux/$arch。请注意,例如,当在 Mac OS 操作系统上运行 Kind 时,节点最终会标记为 kubernetes.io/os=linux。
使用 Pprof 监控性能
Pprof 是一个 Go 性能分析工具,帮助识别 CPU 和内存使用等方面的性能瓶颈。它与 controller-runtime 库的 HTTP 服务器集成,允许通过 HTTP 端点进行性能分析。您可以使用 go tool pprof 可视化数据。由于 Pprof 已内置于 controller-runtime 中,因此不需要单独安装。管理器选项 使启用 pprof 和收集运行时指标变得简单,从而优化控制器性能。
Not Recommended for Production
虽然Pprof是一个优秀的性能分析和调试工具,但不建议在生产环境中启用它。主要原因如下:
- 安全风险:性能分析端点暴露了有关您应用程序的性能和资源使用的详细信息,如果被未经授权的用户访问,可能会被利用。
- 开销:运行性能分析可能会引入性能开销,主要是 CPU 使用率,尤其在高负载情况下,可能会影响生产工作负载。
如何使用 Pprof?
-
启用 Pprof
在您的
cmd/main.go文件中,添加字段:golangmgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ ... // PprofBindAddress 是控制器应绑定以提供 pprof 的 TCP 地址 // 指定管理器的地址和应绑定的端口。 PprofBindAddress: ":8082", ...})“
-
试一试
在启用Pprof之后,您需要构建并部署您的控制器以进行测试。请按照快速入门指南中的步骤,在本地或集群上运行您的项目。
然后,您可以应用您的CRs/样本以监控其控制器的性能。
-
导出数据
使用
curl,将性能统计导出到文件中,格式如下:请注意,我们正在使用通过配置的绑定主机和端口。 # cmd/main.go 中的管理选项 curl -s "http://127.0.0.1:8082/debug/pprof/profile" > ./cpu-profile.out -
在浏览器中可视化结果
# Go tool opens a session on port 8080. # 你可以根据自己的需要进行更改。 go tool pprof -http=:8080 ./cpu-profile.out可视化结果会根据部署的工作负载和控制器的行为而有所不同。然而,您将在浏览器中看到类似于以下的结果:
您已对数据进行训练,直到2023年10月。
理解 Kubebuilder 中的作用域
在 Kubernetes 中,作用域定义资源或控制器运行的边界。
使用 Kubebuilder 构建时,你会处理两个独立的作用域概念:
- Manager 作用域 - 决定 manager 监视并操作哪些命名空间
- CRD 作用域 - 决定自定义资源是命名空间特定还是集群范围
什么是作用域?
作用域定义 Kubernetes 集群中的可见性与访问边界:
- 集群作用域:跨整个集群运行并可访问所有命名空间
- 命名空间作用域:限制到特定命名空间以实现隔离与安全
Manager 作用域与 CRD 作用域
这些概念彼此独立且分别配置:
- Manager 作用域:控制 manager 监视哪些命名空间(通过 deployment RBAC 与缓存配置)
- CRD 作用域:控制自定义资源是命名空间特定还是集群范围(在 CRD 清单中配置)
你可以按不同方式组合它们——例如,集群作用域的 manager 可以管理命名空间作用域的 CRD(默认模式)。
了解更多
详细信息、配置步骤与代码示例:
- Manager 作用域 - Manager 作用域配置、RBAC、缓存设置与命名空间监视
- CRD 作用域 - CRD 作用域配置、标记与 RBAC 注意事项
- 迁移到命名空间作用域 Manager - 现有项目的逐步迁移指南
Manager 作用域
Manager 作用域决定 manager 监视并管理哪些命名空间中的资源。
Manager Scope vs CRD Scope
Manager 作用域独立于 CRD 作用域。关于这两个概念的差异,参见 理解作用域。
概述
Kubebuilder 支持三种 Manager 作用域:
| Scope | 描述 | 用例 |
|---|---|---|
| 集群作用域(默认) | 监视集群中的所有命名空间 | 单个 manager 在集群范围管理资源 |
| Namespace-scoped | 仅监视特定命名空间 | 多租户、最小权限部署 |
| Multi-namespace | 监视多个特定命名空间 | 在命名空间子集中管理资源的 Manager |
Manager 作用域通过以下方式配置:
- RBAC 资源(Role 与 ClusterRole)
cmd/main.go中的缓存配置WATCH_NAMESPACEenvironment variable
集群作用域(默认)
默认情况下,Kubebuilder 脚手架生成监视集群中所有命名空间的集群作用域 manager。
kubebuilder init --domain example.com
Characteristics:
- 使用
ClusterRole和ClusterRoleBinding作为 RBAC - Manager 监视所有命名空间
- 无需缓存配置
何时使用:
- 整个集群的单个 manager 实例
- 管理集群作用域资源(Node、ClusterRole、Namespace)
- 在可接受集群范围访问时更简单的 RBAC 模型
Namespace-scoped
命名空间作用域 manager 仅监视特定命名空间,通过 WATCH_NAMESPACE 环境变量配置。
# New projects
kubebuilder init --domain example.com --namespaced
# Existing projects
kubebuilder edit --namespaced=true
Characteristics:
- 使用命名空间作用域的
Role和RoleBinding作为 RBAC - Manager 仅监视指定的命名空间
- 需要在
cmd/main.go中配置缓存 - 需要在控制器 RBAC 标记中使用
namespace=参数
何时使用:
- 多租户环境(每个租户/命名空间一个 manager)
- 要求最小权限访问的安全策略
- 不同命名空间中的多个 manager 实例
RBAC 标记:
命名空间作用域项目中的 Controller 在 RBAC markers 中使用 namespace= 参数,以生成命名空间作用域的 Role 资源:
// +kubebuilder:rbac:groups=myapp.example.com,namespace=myproject-system,resources=mykinds,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=myapp.example.com,namespace=myproject-system,resources=mykinds/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=myapp.example.com,namespace=myproject-system,resources=mykinds/finalizers,verbs=update
当 controller-gen 看到 namespace= 参数时,会生成 kind: Role 而非 kind: ClusterRole。命名空间字段由 kustomize 在构建过程中添加(在 config/default/kustomization.yaml 中配置)。
缓存配置:
使用 --namespaced 标志时,Kubebuilder 会自动在 cmd/main.go 中脚手架生成缓存配置:
// setupCacheNamespaces configures the cache to watch specific namespace(s).
// It supports both single namespace ("ns1") and multi-namespace ("ns1,ns2,ns3") formats.
func setupCacheNamespaces(namespaces string) cache.Options {
defaultNamespaces := make(map[string]cache.Config)
for ns := range strings.SplitSeq(namespaces, ",") {
defaultNamespaces[strings.TrimSpace(ns)] = cache.Config{}
}
return cache.Options{
DefaultNamespaces: defaultNamespaces,
}
}
// In main()
watchNamespace, err := getWatchNamespace()
if err != nil {
setupLog.Error(err, "Unable to get WATCH_NAMESPACE")
os.Exit(1)
}
mgrOptions := ctrl.Options{
Scheme: scheme,
Metrics: metricsServerOptions,
WebhookServer: webhookServer,
HealthProbeBindAddress: probeAddr,
LeaderElection: enableLeaderElection,
LeaderElectionID: "your-leader-election-id",
}
// 配置缓存以监视 WATCH_NAMESPACE 中指定的命名空间
mgrOptions.Cache = setupCacheNamespaces(watchNamespace)
setupLog.Info("Watching namespace(s)", "namespaces", watchNamespace)
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), mgrOptions)
此配置同时适用于单命名空间(WATCH_NAMESPACE=my-namespace)和多命名空间(WATCH_NAMESPACE=ns1,ns2,ns3)场景。
Multi-namespace
Manager 可通过 WATCH_NAMESPACE 中的逗号分隔值监视多个特定命名空间。
Characteristics:
- 在每个被监视的命名空间中需要
Role和RoleBinding - 使用相同的
setupCacheNamespaces辅助函数 - 与单命名空间模式相同的代码(KISS 原则)
Example:
# Deploy manager to watch multiple namespaces
export WATCH_NAMESPACE=namespace1,namespace2,namespace3
kubectl apply -f dist/install.yaml
setupCacheNamespaces 辅助函数自动处理单个与多个命名空间,无需条件逻辑。
Example
Kubebuilder 仓库中的 testdata/project-v4-with-plugins 演示了完整的命名空间作用域 manager 配置。
Webhooks and Namespace-Scoped Mode
若项目有 webhooks,manager 缓存受限于 WATCH_NAMESPACE,但 webhooks 默认仍接收来自所有 namespaces 的请求。
问题:
Webhook 服务器会接收来自所有 namespaces 的 admission 请求,但缓存仅有 WATCH_NAMESPACE 的数据。若 webhook handler 查询监视范围外对象的缓存,查找会失败。
Solution:
在 webhooks 上配置 namespaceSelector 或 objectSelector,使 webhook 作用域与缓存一致。目前 controller-gen 尚无对应 markers。必须用 Kustomize patches 手动添加。
创建并应用 namespace selector patches 的详细步骤见 Webhook Bootstrap 问题 指南。
另见
- 理解作用域 - Manager 与 CRD 作用域概述
- CRD 作用域 - 配置 CustomResourceDefinition 作用域
- 命名空间作用域迁移 - 详细实现指南
- 项目配置 - PROJECT 文件配置
CRD 作用域
本文说明 Kubernetes 中 CustomResourceDefinition(CRD)的作用域:CRD 如何定义为命名空间作用域或集群作用域资源。
CRD Scope vs Manager Scope
CRD 作用域独立于 manager 作用域。关于这两个概念的差异,参见 理解作用域。
概述
CRD 作用域决定自定义资源的可见性与可用性:
| Scope | 描述 | 示例资源 |
|---|---|---|
| 命名空间作用域(默认) | 资源存在于特定命名空间内 | Deployment、Service、ConfigMap、Pod |
| Cluster-scoped | 资源在整个集群范围内全局存在 | Node、ClusterRole、Namespace、PersistentVolume |
命名空间范围的自定义资源定义(CRD)
默认情况下,Kubebuilder 创建命名空间作用域的 CRD:
kubebuilder create api --group cache --version v1alpha1 --kind Memcached
生成的 CRD 清单:
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: memcacheds.cache.example.com
spec:
scope: Namespaced # Default
group: cache.example.com
names:
kind: Memcached
plural: memcacheds
versions:
- name: v1alpha1
# ...
自定义资源在特定命名空间中创建:
kubectl apply -f memcached.yaml -n my-namespace
kubectl get memcacheds -n my-namespace
何时使用:
- 与特定应用、团队或租户绑定的资源
- 需要隔离的多租户环境
- 大多数应用级资源
Considerations:
- 测试新 CRD 版本需要适当的版本管理与转换策略
- 转换 Webhook 必须考虑命名空间作用域
- 便于在特定命名空间内受控滚动发布
集群范围的 CRD(自定义资源定义)
集群作用域 CRD 创建在整个集群范围内全局的资源。
集群范围的 CRD(自定义资源定义)
创建 API 时,使用 --namespaced=false 标志:
kubebuilder create api --group infrastructure --version v1 --kind Database --namespaced=false
生成的 CRD 清单:
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: databases.infrastructure.example.com
spec:
scope: Cluster # Cluster-scoped
group: infrastructure.example.com
names:
kind: Database
plural: databases
versions:
- name: v1
# ...
自定义资源是集群范围的(无命名空间):
kubectl apply -f database.yaml
kubectl get databases # No namespace needed
何时使用:
- 对集群全局的资源(基础设施、配置)
- 需要从所有命名空间访问的资源
- 管理集群级关注点的资源
Examples:
- 基础设施配置(云提供商设置、集群 DNS)
- 全局策略或配额
- 跨命名空间资源聚合
更改 CRD 作用域
针对现有 API
创建 API 后,可使用 +kubebuilder:resource:scope 标记更改其作用域:
针对集群作用域:
//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
//+kubebuilder:resource:scope=Cluster
// Database is the Schema for the databases API
type Database struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec DatabaseSpec `json:"spec,omitempty"`
Status DatabaseStatus `json:"status,omitempty"`
}
针对命名空间作用域:
//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
//+kubebuilder:resource:scope=Namespaced
// Memcached 是 memcacheds API 的 Schema
type Memcached struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec MemcachedSpec `json:"spec,omitempty"`
Status MemcachedStatus `json:"status,omitempty"`
}
更新标记后,重新生成清单:
make manifests
Scope Changes Are Breaking
将 CRD 作用域从 Namespaced 改为 Cluster(或反过来)是破坏性变更:
- 现有自定义资源将变为无效
- 用户必须手动迁移其资源
- 考虑改为创建不同版本的新 CRD
仅在生产使用前的初期开发阶段更改作用域。
CRD 作用域的 RBAC
命名空间范围的自定义资源定义(CRD)
监视命名空间作用域 CRD 的控制器使用命名空间作用域 RBAC:
//+kubebuilder:rbac:groups=cache.example.com,resources=memcacheds,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=cache.example.com,resources=memcacheds/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=cache.example.com,resources=memcacheds/finalizers,verbs=update
生成的 RBAC(集群作用域 manager):
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: manager-role
rules:
- apiGroups: ["cache.example.com"]
resources: ["memcacheds"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
生成的 RBAC(命名空间作用域 manager):
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: manager-role
命名空间: manager-namespace
rules:
- apiGroups: ["cache.example.com"]
resources: ["memcacheds"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
集群范围的 CRD(自定义资源定义)
监视集群作用域 CRD 的控制器必须使用集群范围 RBAC:
//+kubebuilder:rbac:groups=infrastructure.example.com,resources=databases,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=infrastructure.example.com,resources=databases/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=infrastructure.example.com,resources=databases/finalizers,verbs=update
生成的 RBAC(集群作用域 CRD 始终为 ClusterRole):
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: manager-role
rules:
- apiGroups: ["infrastructure.example.com"]
resources: ["databases"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
Important
即使你的 manager 是命名空间作用域(仅监视一个命名空间),若它管理集群作用域 CRD,仍需要这些资源的 ClusterRole 权限。
Manager 作用域与 CRD 作用域彼此独立:
- Manager 作用域:由缓存配置控制(监视哪些命名空间)
- CRD 作用域:由 CRD 的
scope字段控制(资源可见性)
版本转换与 Webhook
对于多版本的命名空间作用域 CRD,转换 Webhook 必须考虑命名空间作用域:
//+kubebuilder:webhook:path=/convert,mutating=false,failurePolicy=fail,groups=cache.example.com,resources=memcacheds,verbs=create;update,versions=v1;v1beta1,name=cmemcached.kb.io,sideEffects=None,admissionReviewVersions=v1
webhook 必须处理任意命名空间中资源的转换。详情参见 多版本教程。
测试
测试命名空间作用域 CRD
# Create resource in namespace
kubectl apply -f config/samples/cache_v1alpha1_memcached.yaml -n test-namespace
# Verify it exists in that namespace only
kubectl get memcacheds -n test-namespace
kubectl get memcacheds -n other-namespace # Should not find it
测试集群作用域 CRD
# Create cluster-scoped resource (no namespace)
kubectl apply -f config/samples/infrastructure_v1_database.yaml
# Verify it is cluster-wide
kubectl get databases # No namespace needed
另见
- Manager 作用域 - 配置 manager 监视作用域
- 生成 CRD - CRD 生成与标记
- 多版本教程 - CRD 版本管理与转换
- Kubernetes CRD 文档
子模块布局
这部分描述了如何修改一个脚手架项目,以便与多个 go.mod 文件一起使用,用于 API 和控制器。
子模块布局(可以称之为 Monorepo 的一种特殊形式)是一个特殊的用例,可以在涉及重用 API 的场景中提供帮助,而不会引入不应在外部调用 API 的项目中出现的间接依赖。
Using External Resources/APIs
如果您希望通过控制器对属于另一个项目或由 Kubernetes API 拥有的 Type(CRD)进行操作和对账,请参阅 使用外部资源/API 以获取更多信息。
概述
为 APIs 和 Controllers 使用分开的 go.mod 模块可以帮助解决以下情况:
- 有一个企业版的 Operator 可用,它希望重用社区版的 API。
- 有许多(可能是外部的)模块依赖于该 API,您希望对传递依赖关系进行更严格的分离。
- 如果您想减少传递依赖对您的 API 被其他项目包含的影响
- 如果您希望将 API 发布过程的生命周期与控制器发布过程分开管理。
- 如果您希望在不将代码分割到多个代码库的情况下对代码库进行模块化。
然而,他们在典型项目中引入了多个警告,这是使它们在通用用例或插件中难以推荐的主要因素之一:
- 多个
go.mod模块并不被推荐作为 Go 的最佳实践,并且 多个模块大多是不鼓励的。 - 你总是可以将你的API提取到一个新的代码库中,并且在一个跨多个代码库、依赖于相同API类型的项目中,可能会对发布过程有更多的控制。
- 它至少需要一个 替换指令,要么通过
go.work(这至少需要两个以上的文件以及一个用于没有 GO_WORK 的构建环境的环境变量),要么通过go.mod替换,这需要在每次发布时手动添加和移除。
Implications on Maintenance efforts
当决定偏离标准的 kubebuilder PROJECT 设置或其插件所提供的扩展布局时,可能会导致维护成本增加,因为上游可能会出现破坏性变化,这可能会与此处描述的自定义模块结构不兼容。
将代码库拆分为多个仓库和/或多个模块会带来成本,并且这些成本会随着时间的推移而增加。你需要在自己的模块之间定义清晰的版本依赖关系,谨慎进行分阶段升级等。特别是对于小型到中型项目,一个仓库和一个模块是最佳选择。
请记住,除非您知道自己在做什么,否则不建议偏离建议的布局。否则,您可能会失去使用某些 CLI 功能和助手的能力。有关项目布局的更多信息,请参阅文档 基础项目包含哪些内容?
调整您的项目
为了实现正确的子模块布局,我们将以生成的 API 作为起点。
对于以下步骤,我们将假设您在您的 GOPATH 中创建了您的项目。
kubebuilder init
并创建了一个 API 和控制器,带有
kubebuilder create api --group operator --version v1alpha1 --kind Sample --resource --controller --make
为您的 API 创建第二个模块
现在我们有了基本布局,我们将为您启用多个模块。
- 导航到
api/v1alpha1 - 运行
go mod init来创建一个新的子模块。 - 运行
go mod tidy以解决依赖关系。
您的 API go.mod 文件现在可能如下所示:
module YOUR_GO_PATH/test-operator/api/v1alpha1
go 1.21.0
require (
k8s.io/apimachinery v0.28.4
sigs.k8s.io/controller-runtime v0.16.3
)
require (
github.com/go-logr/logr v1.2.4 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/text v0.13.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/klog/v2 v2.100.1 // indirect
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
)
如您所见,它仅包括 apimachinery 和 controller-runtime 作为依赖项,而您在控制器中声明的任何依赖项都不会被间接导入。
在开发中使用替换指令
当尝试在 Operator 的根文件夹中解析主模块时,如果使用 VCS 路径,您会注意到一个错误:shellgo mod tidygo: 寻找包 YOUR_GO_PATH/test-operator/api/v1alpha1 的模块YOUR_GO_PATH/test-operator 导入了 YOUR_GO_PATH/test-operator/api/v1alpha1: 无法找到提供包 YOUR_GO_PATH/test-operator/api/v1alpha1 的模块: 模块 YOUR_GO_PATH/test-operator/api/v1alpha1: git ls-remote -q origin 在 LOCALVCSPATH: 退出状态 128: remote: 找不到仓库。 fatal: 仓库 'https://YOUR_GO_PATH/test-operator/' 找不到
“原因是您可能尚未将您的模块推送到版本控制系统,因此解析主模块将失败,因为它不再能以包的形式直接访问 API 类型,而只能作为模块访问。
为了解决这个问题,我们需要告诉 Go 工具正确地将 API 模块替换为对您路径的本地引用。
您可以通过两种不同的方法来实现这一点:Go 模块和 Go 工作区。
使用 Go 模块
对于 Go 模块,您需要编辑项目的主 go.mod 文件并发出一个替换指令。
您可以通过编辑 go.mod 来完成此操作,使用 ``
go mod edit -require YOUR_GO_PATH/test-operator/api/v1alpha1@v0.0.0 # Only if you did not already resolve the module
go mod edit -replace YOUR_GO_PATH/test-operator/api/v1alpha1@v0.0.0=./api/v1alpha1
go mod tidy
请注意,我们使用了 API 模块的占位符版本 v0.0.0。如果您已经发布过一次您的 API 模块,您也可以使用真实版本。然而,这仅在 API 模块已经在版本控制系统中可用的情况下有效。
Implications on controller releases
由于主 go.mod 文件现在包含了一个替换指令,因此在发布你的控制器模块之前,重要的是将其删除。为此,你可以简单地运行
go mod edit -dropreplace YOUR_GO_PATH/test-operator/api/v1alpha1
go mod tidy
使用 Go 工作区
对于 Go 工作区,您将不会自己编辑 go.mod 文件,而是依赖 Go 中的工作区支持。
要为您的项目初始化工作空间,请在项目根目录中运行 go work init。
现在让我们将这两个模块包含到我们的工作区中:
go work use . # This includes the main module with the controller
go work use api/v1alpha1 # This is the API submodule
go work sync
这将导致像 go run 或 go build 这样的命令尊重工作区,并确保使用本地解析。
您可以在本地进行此操作,而无需构建您的模块。
在使用 go.work 文件时,建议不要将它们提交到版本库,并将其添加到 .gitignore 中。
go.work
go.work.sum
在发布时,请确保将环境变量 GOWORK=off 设置为关闭(可通过 go env GOWORK 验证),以确保发布过程不受可能已提交的 go.work 文件的影响。
调整 Dockerfile
在构建您的控制器镜像时,kubebuilder 默认无法处理多个模块。您需要手动将新的 API 模块添加到依赖项的下载中:
# Build the manager binary
FROM docker.io/golang:1.20 as builder
ARG TARGETOS
ARG TARGETARCH
WORKDIR /workspace
# Copy the Go Modules manifests
COPY go.mod go.mod
COPY go.sum go.sum
# Copy the Go Sub-Module manifests
COPY api/v1alpha1/go.mod api/go.mod
COPY api/v1alpha1/go.sum api/go.sum
# cache deps before building and copying source so that we do not need to re-download as much
# and so that source changes do not invalidate our downloaded layer
RUN go mod download
# Copy the go source
COPY cmd/main.go cmd/main.go
COPY api/ api/
COPY internal/controller/ internal/controller/
# Build
# the GOARCH has not a default value to allow the binary be built according to the host where the command
# was called. For example, if you call make docker-build in a local env which has the Apple Silicon M1 SO
# the docker BUILDPLATFORM arg is linux/arm64 when for Apple x86 it is linux/amd64. Therefore,
# by leaving it empty you can ensure that the container and binary shipped on it has the same platform.
RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go
# Use distroless as minimal base image to package the manager binary
# Refer to https://github.com/GoogleContainerTools/distroless for more details
FROM gcr.io/distroless/static:nonroot
WORKDIR /
COPY --from=builder /workspace/manager .
USER 65532:65532
ENTRYPOINT ["/manager"]
创建一个新的 API 和控制器版本
因为你调整了默认布局,所以在发布你的操作员的第一个版本之前,请确保熟悉 mono-repo/multi-module 发布,涉及多个不同子目录中的 go.mod 文件。
假设创建了一个单一的API,发布流程可能如下所示:
git commit
git tag v1.0.0 # 这是你的主要模块发布
git tag api/v1.0.0 # 这是您的 API 发布版本
go mod edit -require YOUR_GO_PATH/test-operator/api@v1.0.0 # 现在我们在主模块中依赖于 API 模块。
go mod edit -dropreplace YOUR_GO_PATH/test-operator/api/v1alpha1 # 这将移除本地开发中针对 Go 模块的替换指令,这意味着将使用来自版本控制系统的源代码,而不是您本地检出的单一代码库中的源代码。
git push origin main v1.0.0 api/v1.0.0
在此之后,您的模块将在 VCS 中可用,您不再需要本地替换。然而,如果您进行本地更改,请确保根据 replace 指令调整您的行为。
重用您提取的 API 模块
每当您想要在一个独立的 kubebuilder 中重用您的 API 模块时,我们会假设您遵循了 使用外部类型 的指南。当您到达“编辑 API 文件“这一步时,只需使用以下方式导入依赖项:
go get YOUR_GO_PATH/test-operator/api@v1.0.0
然后按照指南中的说明使用它。
使用外部资源
在某些情况下,您的项目可能需要与未由您自己定义的API的资源进行协作。这些外部资源主要分为两个类别:
- 核心类型:由Kubernetes本身定义的API类型,例如
Pods、Services和Deployments。 - 外部类型:在其他项目中定义的API类型,例如由其他解决方案定义的CRD。
管理外部类型
为外部类型创建控制器
要为外部类型创建控制器而无需搭建资源,请使用 create api 命令并加上 --resource=false 选项,并使用 --external-api-path 和 --external-api-domain 标志选项指定外部 API 类型的路径。这将为项目外部定义的类型生成控制器,例如由其他 Operator 管理的 CRD(自定义资源定义)。
该命令看起来像这样:
kubebuilder create api --group <theirgroup> --version <theirversion> --kind <theirKind> --controller --resource=false --external-api-path=<their Golang path import> --external-api-domain=<theirdomain>
--external-api-path:提供定义外部类型的 Go import 路径。--external-api-domain:提供外部类型的域名。Kubebuilder 用此值生成 RBAC 权限并创建 QualifiedGroup,例如 -apiGroups: <group>.<domain>
例如,如果您正在管理来自 Cert Manager 的证书:
kubebuilder create api --group cert-manager --version v1 --kind Certificate --controller=true --resource=false --external-api-path=github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 --external-api-domain=io
Pinning External API Versions
你可以使用 --external-api-module 标志固定外部 API 依赖的特定版本:
kubebuilder create api --group cert-manager --version v1 --kind Certificate \
--controller=true --resource=false \
--external-api-path=github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 \
--external-api-domain=io \
--external-api-module=github.com/cert-manager/cert-manager@v1.18.2
该标志接受可选带版本的模块路径(例如 github.com/cert-manager/cert-manager@v1.18.2)。模块会存入 PROJECT 文件,并通过 go get 添加到 go.mod,干净地作为直接依赖添加,而不会用无用不必要的间接依赖污染 go.mod。
请查看为此生成的 RBAC 标记:
// +kubebuilder:rbac:groups=cert-manager.io,resources=certificates,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=cert-manager.io,resources=certificates/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=cert-manager.io,resources=certificates/finalizers,verbs=update
此外,RBAC角色:
- apiGroups:
- cert-manager.io
resources:
- 证书
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- cert-manager.io
resources:
- certificates/finalizers
verbs:
- update
- apiGroups:
- cert-manager.io
resources:
- certificates/status
verbs:
- get
- patch
- update
这为外部类型搭建了一个控制器,但跳过了创建新的资源定义,因为该类型是在外部项目中定义的。
创建一个 Webhook 来管理外部类型
你可通过提供外部 API 路径、domain 以及可选的 module 为外部类型创建 Webhook:
kubebuilder create webhook --group cert-manager --version v1 --kind Issuer \
--defaulting --programmatic-validation \
--external-api-path=github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 \
--external-api-domain=io
group 与 domain 构成 RBAC 标记的 API group,如上例中的 cert-manager.io。
你也可以使用 --external-api-module 标志固定版本:
kubebuilder create webhook --group cert-manager --version v1 --kind Issuer \
--defaulting --programmatic-validation \
--external-api-path=github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 \
--external-api-domain=io \
--external-api-module=github.com/cert-manager/cert-manager@v1.18.2
管理核心类型
核心 Kubernetes API 类型,例如 Pods、Services 和 Deployments,是由 Kubernetes 预定义的。要为这些核心类型创建一个控制器,而不使用资源脚手架,请使用以下表中描述的 Kubernetes 组名称,并指定版本和类型。
| 组 | K8s API组 |
|---|---|
| 入学 | k8s.io/入场管理 |
| 入学注册 | k8s.io/admissionregistration |
| apps | apps |
| 审计注册 | k8s.io/auditregistration |
| apiextensions 的中文翻译是“API 扩展“。 | k8s.io/apiextensions |
| 身份验证 | k8s.io/authentication |
| 授权 | k8s.io/authorization |
| 自动扩展 | 自动扩展 |
| batch | batch |
| 证书 | k8s.io/证书 |
| 调整 | k8s.io/调整 |
| 核心 | 核心 |
| events | k8s.io/events |
| 扩展包 | 扩展包 |
| 图像策略 | k8s.io/imagepolicy |
| 网络连接 | k8s.io/networking |
| 节点 | k8s.io/node |
| 指标 | k8s.io/metrics |
| 政策 | 政策 |
| rbac.authorization | k8s.io/rbac.authorization |
| 调度 | k8s.io/调度 |
| 设置 | k8s.io/设置 |
| storage | k8s.io/storage |
创建管理 Pods 的控制器的命令如下:
kubebuilder create api --group core --version v1 --kind Pod --controller=true --resource=false
例如,要创建一个控制器来管理部署,命令如下:
create api --group apps --version v1 --kind Deployment --controller=true --resource=false
请查看为此生成的 RBAC 标记:
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=apps,resources=deployments/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=apps,resources=deployments/finalizers,verbs=update
此外,上述 标记 的 RBAC:
- apiGroups:
- apps
resources:
- deployments
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- apps
resources:
- 部署/最终处理程序
verbs:
- update
- apiGroups:
- apps
resources:
- 部署/状态
verbs:
- get
- patch
- update
这为 Core 类型 corev1.Pod 构建了一个控制器,但由于该类型已在 Kubernetes API 中定义,因此跳过了创建新的资源定义。
创建一个 Webhook 来管理核心类型
您将使用核心类型数据运行命令,就像您对控制器那样。请看以下示例:
kubebuilder create webhook --group core --version v1 --kind Pod --programmatic-validation
同一资源的多个控制器
Kubebuilder 支持为同一 API 资源使用多个命名 controller。这允许对同一资源类型使用不同的对账逻辑。
Usage
创建第一个控制器
kubebuilder create api --group crew --version v1 --kind Captain \
--resource=true \
--controller=true \
--controller-name=captain
Creates:
api/v1/captain_types.go中的 API 类型internal/controller/captain_controller.go中的控制器,结构体为CaptainReconciler- 在
cmd/main.go中注册
添加更多控制器
kubebuilder create api --group crew --version v1 --kind Captain \
--resource=false \
--controller=true \
--controller-name=captain-backup
Creates:
internal/controller/captain_backup_controller.go中的控制器,结构体为CaptainBackupReconciler- 在
cmd/main.go中的额外注册
API 仅创建一次。额外控制器引用现有 API。
PROJECT 文件格式
旧格式(仍支持)
resources:
- api:
crdVersion: v1
controller: true
group: 船员
kind: 船长
version: v1
多控制器格式
resources:
- api:
crdVersion: v1
controllers:
- name: captain
- name: captain-backup
group: 船员
kind: 船长
version: v1
当同一资源上同时存在两种格式(因手动编辑)时,controllers: 数组优先,且 controller: true 会自动清除。
控制器命名
Storage
控制器名称按用户提供的原样存储在 PROJECT 文件中。
代码生成
名称会为 Go 代码规范化:
- 文件名:将连字符替换为下划线:
captain-backup→captain_backup_controller.go - 结构体名称:转为 PascalCase 并追加 Reconciler:
captain-backup→CaptainBackupReconciler - 运行时名称:使用 PROJECT 中的精确名称:
Named("captain-backup") - 多 group:以 group 名称为前缀:
Named("crew-captain-backup")
校验规则
- 名称在同一资源内必须唯一
- 名称必须是有效的 DNS 标签:仅小写字母、数字和连字符,最长 63 个字符
- 规范化为同一标识符的不同名称会被拒绝(例如
captain-backup与captainbackup)
控制器协调
同一资源的多个控制器需要协调以避免冲突:
- 字段所有权:每个控制器应管理不同字段
- Finalizer:使用唯一名称:
{controller-name}.example.com/finalizer - Status 更新:为每个控制器分配不同的 status 子字段
- 条件逻辑:使用标签或注解将资源路由到特定控制器
Kubebuilder 脚手架生成控制器,但不管理它们之间的协调。
从旧格式迁移
带有 controller: true 的现有项目可继续不变地工作。当向带有 controller: true 的资源添加命名 controller 时,Kubebuilder 会自动迁移格式:
迁移前:
resources:
- api:
crdVersion: v1
controller: true
group: 船员
kind: 船长
version: v1
添加命名控制器后:
kubebuilder create api --group crew --version v1 --kind Captain \
--resource=false \
--controller=true \
--controller-name=captain-backup
Result:
resources:
- api:
crdVersion: v1
controllers:
- name: captain
- name: captain-backup
group: 船员
kind: 船长
version: v1
原先未命名的 controller 会被赋予名称 captain(小写 kind),且 controller: true 标志会自动清除。这在启用多个 controller 的同时保持向后兼容。
常见错误
“duplicate controller name”:两个控制器同名。请使用唯一名称。
“conflicts with … both normalize to”:不同名称生成相同结构体名。请选择不同的名称。
“controller with name … already exists”:此资源的控制器已存在。请使用不同名称。
配置 envtest 以进行集成测试
controller-runtime/pkg/envtest Go 库帮助您通过设置和启动 etcd 和 Kubernetes API 服务器的实例来编写控制器的集成测试,而无需 kubelet、controller-manager 或其他组件。
安装
安装二进制文件的过程非常简单,只需运行 make envtest。默认情况下,envtest 会将 Kubernetes API 服务器的二进制文件下载到您项目中的 bin/ 文件夹。make test 是一个一条龙的解决方案,用于下载二进制文件、设置测试环境和运行测试。
您可以参考 Kubebuilder 脚手架的 Makefile,并观察到 envtest 的设置在所有 controller-runtime 版本中保持一致。从 release-0.19 开始,它被配置为自动从正确的位置下载工件,确保 kubebuilder 用户不受影响。
## Tool binaries
..
ENVTEST ?= $(LOCALBIN)/setup-envtest
...
## Tool versions
...
#ENVTEST_VERSION is the controller-runtime version to use for setup-envtest, derived from go.mod
ENVTEST_VERSION ?= $(shell go list -m -f "{{ .Version }}" sigs.k8s.io/controller-runtime)
#ENVTEST_K8S_VERSION is the version of Kubernetes to use for setting up ENVTEST binaries (i.e. 1.31)
ENVTEST_K8S_VERSION ?= $(shell go list -m -f "{{ .Version }}" k8s.io/api | awk -F'[v.]' '{printf "1.%d", $$3}')
...
.PHONY: setup-envtest
setup-envtest: envtest ## Download the binaries required for ENVTEST in the local bin directory.
@echo "Setting up envtest binaries for Kubernetes version $(ENVTEST_K8S_VERSION)..."
@$(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path || { \
echo "Error: Failed to set up envtest binaries for version $(ENVTEST_K8S_VERSION)."; \
exit 1; \
}
.PHONY: envtest
envtest: $(ENVTEST) ## Download setup-envtest locally if necessary.
$(ENVTEST): $(LOCALBIN)
$(call go-install-tool,$(ENVTEST),sigs.k8s.io/controller-runtime/tools/setup-envtest,$(ENVTEST_VERSION))
在隔离/断网环境中的安装
如果您想下载包含二进制文件的 tarball,以便在不联网的环境中使用,可以使用 setup-envtest 来本地下载所需的二进制文件。有很多方法可以配置 setup-envtest 以避免连接互联网,您可以在 这里 阅读相关内容。以下示例将展示如何使用 setup-envtest 设置的大多数默认值来安装 Kubernetes API 二进制文件。
下载二进制文件
make envtest 将 setup-envtest 二进制下载到 ./bin/。
make envtest
setup-envtest 工具将二进制存储在特定于操作系统的位置。详情参见 二进制存储位置。
./bin/setup-envtest use 1.31.0
更新测试目标
一旦这些二进制文件安装完成,修改 test make 目标,加入 -i,如下所示。-i 只会检查本地安装的二进制文件,而不会访问远程资源。你也可以设置 ENVTEST_INSTALLED_ONLY 环境变量。
test: manifests generate fmt vet
KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -i --bin-dir $(LOCALBIN) -p path)" go test ./... -coverprofile cover.out
注意:ENVTEST_K8S_VERSION 需要与您上面下载的 setup-envtest 匹配。否则,您将看到如下错误。
no such version (1.24.5) exists on disk for this architecture (darwin/amd64) -- try running `list -i` to see what is on disk
编写测试
在集成测试中使用 envtest 的一般流程如下:
import sigs.k8s.io/controller-runtime/pkg/envtest
//指定 testEnv 配置
testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")},
}
//开始测试环境
cfg, err = testEnv.Start()
//编写测试逻辑
//停止测试环境
err = testEnv.Stop()
kubebuilder 在它生成的 /controllers 目录下的 ginkgo 测试套件中,为您处理测试环境的样板代码的设置和拆卸。
测试运行的日志以 test-env 为前缀。
Examples
您可以使用插件 DeployImage 来查看示例。该插件允许用户搭建 API/控制器,以便在集群上根据指南和最佳实践部署和管理一个 Operand(镜像)。它抽象了实现这一目标的复杂性,同时允许用户自定义生成的代码。
因此,您可以检查使用 ENV TEST 生成的测试,该测试的目的是确保成功创建 Deployment。您可以在 testdata 目录下看到其代码实现的示例,具体在 DeployImage 的示例可在 此处 查看。
配置您的测试控制平面
Controller-runtime 的 envtest 框架需要本地有 kubectl、kube-apiserver 和 etcd 可执行文件,以模拟真实集群的 API 部分。
make test 命令将把这些二进制文件安装到 bin/ 目录,并在运行使用 envtest 的测试时使用它们。即,
./bin/k8s/
└── 1.25.0-darwin-amd64
├── etcd
├── kube-apiserver
└── kubectl
您可以使用环境变量和/或标志来指定在集成测试中 kubectl、api-server 和 etcd 的设置。
环境变量
| 变量名 | Type | 何时使用 |
|---|---|---|
使用现有集群 | 布尔值 | 与其建立一个本地控制平面,不如指向现有集群的控制平面。 |
KUBEBUILDER_ASSETS | 目录路径 | 将集成测试指向一个包含所有二进制文件的目录(api-server、etcd 和 kubectl)。 |
TEST_ASSET_KUBE_APISERVER,TEST_ASSET_ETCD,TEST_ASSET_KUBECTL | 分别指向 api-server、etcd 和 kubectl 二进制文件的路径 | 类似于 KUBEBUILDER_ASSETS,但更为细化。指向集成测试使用除了默认二进制文件以外的其他二进制文件。这些环境变量还可以用来确保特定测试使用期望版本的这些二进制文件运行。 |
KUBEBUILDER_CONTROLPLANE_START_TIMEOUT 和 KUBEBUILDER_CONTROLPLANE_STOP_TIMEOUT | 支持 time.ParseDuration 的格式的持续时间。 | 为测试控制平面指定不同于默认值的超时时间,以便(分别)启动和停止;任何超出这些时间的测试运行将失败。 |
KUBEBUILDER_ATTACH_CONTROL_PLANE_OUTPUT | 布尔值 | 将其设置为 true 以将控制平面的 stdout 和 stderr 附加到 os.Stdout 和 os.Stderr。当调试测试失败时,这可能很有用,因为输出将包括来自控制平面的输出。 |
请注意,test 这个 Makefile 目标将确保在使用它时所有设置都正确。不过,如果您想在不使用 Makefile 目标的情况下运行测试,例如通过 IDE,则可以直接在您的 suite_test.go 代码中设置环境变量:
var _ = BeforeSuite(func(done Done) {
Expect(os.Setenv("TEST_ASSET_KUBE_APISERVER", "../bin/k8s/1.25.0-darwin-amd64/kube-apiserver")).To(Succeed())
Expect(os.Setenv("TEST_ASSET_ETCD", "../bin/k8s/1.25.0-darwin-amd64/etcd")).To(Succeed())
Expect(os.Setenv("TEST_ASSET_KUBECTL", "../bin/k8s/1.25.0-darwin-amd64/kubectl")).To(Succeed())
// OR
Expect(os.Setenv("KUBEBUILDER_ASSETS", "../bin/k8s/1.25.0-darwin-amd64")).To(Succeed())
logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
testenv = &envtest.Environment{}
_, err := testenv.Start()
Expect(err).NotTo(HaveOccurred())
close(done)
}, 60)
var _ = AfterSuite(func() {
Expect(testenv.Stop()).To(Succeed())
Expect(os.Unsetenv("TEST_ASSET_KUBE_APISERVER")).To(Succeed())
Expect(os.Unsetenv("TEST_ASSET_ETCD")).To(Succeed())
Expect(os.Unsetenv("TEST_ASSET_KUBECTL")).To(Succeed())
})
ENV TEST Config Options
更多选项参见 envtest Environment 配置。若测试 webhook,另请参阅 webhook 安装选项。
旗帜
这是一个示例,展示了在你的集成测试中修改启动 API 服务器的标志,与 envtest.DefaultKubeAPIServerFlags 中的默认值进行对比。
customApiServerFlags := []string{
"--secure-port=6884",
"--admission-control=MutatingAdmissionWebhook",
}
apiServerFlags := append([]string(nil), envtest.DefaultKubeAPIServerFlags...)
apiServerFlags = append(apiServerFlags, customApiServerFlags...)
testEnv = &envtest.Environment{
CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")},
KubeAPIServerFlags: apiServerFlags,
}
测试考虑事项
除非您正在使用现有的集群,否则请记住,在测试上下文中没有运行内置控制器。在某些方面,测试控制平面将与“真实“集群表现不同,这可能会影响您编写测试的方式。一个常见的例子是垃圾收集;因为没有控制器监视内置资源,尽管设置了 OwnerReference,对象也不会被删除。
为了测试删除生命周期是否正常工作,请测试所有权,而不是仅仅断言存在性。例如:
expectedOwnerReference := v1.OwnerReference{
Kind: "MyCoolCustomResource",
APIVersion: "my.api.example.com/v1beta1",
UID: "d9607e19-f88f-11e6-a518-42010a800195",
Name: "userSpecifiedResourceName",
}
Expect(deployment.ObjectMeta.OwnerReferences).To(ContainElement(expectedOwnerReference))
Namespace usage limitation
EnvTest不支持命名空间的删除。删除命名空间看似成功,但实际情况是该命名空间会处于终止状态,并且永远不会被回收。尝试重新创建该命名空间将会失败。这将导致您的调整器继续调整任何遗留的对象,除非它们被删除。
为了克服这一限制,您可以为每个测试创建一个新的命名空间。即便如此,当一个测试完成(例如在“namespace-1“中)而另一个测试开始(例如在“namespace-2“中)时,控制器仍会对“namespace-1“中的任何活动对象进行对账。通过确保所有测试在测试清理过程中进行自我清理,可以避免这种情况。如果命名空间的清理比较困难,可以考虑以某种方式配置对账器,使其忽略来自非测试命名空间的对账请求。
type MyCoolReconciler struct {
client.Client
...
Namespace string // 限制命名空间以进行调整
}
func (r *MyCoolReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
_ = r.Log.WithValues("myreconciler", req.NamespacedName)
// 忽略其他命名空间的请求,如果有指定的话
if r.Namespace != "" && req.Namespace != r.Namespace {
return ctrl.Result{}, nil
}
每当你的测试创建一个新的命名空间时,它会修改 reconciler.Namespace 的值。reconciler 将有效地忽略之前的命名空间。有关更多信息,请参阅在 controller-runtime 中提出的问题 controller-runtime/issues/880,以添加此支持。
Cert-Manager 和 Prometheus 选项
使用 Kubebuilder 创建的项目可以启用 metrics 和 cert-manager 选项。请注意,当我们使用 ENV TEST 时,我们旨在测试控制器及其调整过程。这被视为集成测试,因为 ENV TEST API 会在集群中进行测试,因此会下载并使用二进制文件来配置其前置要求。然而,其主要目的是对控制器进行 单元 测试。
因此,在常见情况下,测试对账时您不需要关注这些选项。但是,如果您希望在安装了 Prometheus 和 Cert-manager 的情况下进行测试,可以在运行测试之前添加安装它们所需的步骤。以下是一个示例。
// 在测试之前添加安装 Prometheus operator 和 cert-manager 的操作。
BeforeEach(func() {
By("installing prometheus operator")
Expect(utils.InstallPrometheusOperator()).To(Succeed())
By("installing the cert-manager")
Expect(utils.InstallCertManager()).To(Succeed())
})
// 您也可以在测试后将它们移除::
AfterEach(func() {
By("uninstalling the Prometheus manager bundle")
utils.UninstallPrometheusOperManager()
By("uninstalling the cert-manager bundle")
utils.UninstallCertManager()
})
请检查以下示例,了解如何实现上述操作:
const (
certmanagerVersion = "v1.5.3"
certmanagerURLTmpl = "https://github.com/cert-manager/cert-manager/releases/download/%s/cert-manager.yaml"
defaultKindCluster = "kind"
defaultKindBinary = "kind"
prometheusOperatorVersion = "0.51"
prometheusOperatorURL = "https://raw.githubusercontent.com/prometheus-operator/" + "prometheus-operator/release-%s/bundle.yaml"
)
func warnError(err error) {
_, _ = fmt.Fprintf(GinkgoWriter, "warning: %v\n", err)
}
// InstallPrometheusOperator 安装 prometheus 操作器,用于导出启用的指标。
func InstallPrometheusOperator() error {
url := fmt.Sprintf(prometheusOperatorURL, prometheusOperatorVersion)
cmd := exec.Command("kubectl", "apply", "-f", url)
_, err := Run(cmd)
return err
}
// UninstallPrometheusOperator 卸载 Prometheus
func UninstallPrometheusOperator() {
url := fmt.Sprintf(prometheusOperatorURL, prometheusOperatorVersion)
cmd := exec.Command("kubectl", "delete", "-f", url)
if _, err := Run(cmd); err != nil {
warnError(err)
}
}
// UninstallCertManager 卸载证书管理器
func UninstallCertManager() {
url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion)
cmd := exec.Command("kubectl", "delete", "-f", url)
if _, err := Run(cmd); err != nil {
warnError(err)
}
}
// InstallCertManager 安装证书管理器包。
func InstallCertManager() error {
url := fmt.Sprintf(certmanagerURLTmpl, certmanagerVersion)
cmd := exec.Command("kubectl", "apply", "-f", url)
if _, err := Run(cmd); err != nil {
return err
}
// 等待 cert-manager-webhook 准备就绪,如果在集群上卸载后重新安装 cert-manager,可能需要一些时间。
cmd = exec.Command("kubectl", "wait", "deployment.apps/cert-manager-webhook",
"--for", "condition=Available",
"--namespace", "cert-manager",
"--timeout", "5m",
)
_, err := Run(cmd)
return err
}
// LoadImageToKindClusterWithName 将本地 Docker 镜像加载到 Kind 集群中
func LoadImageToKindClusterWithName(name string) error {
cluster := defaultKindCluster
if v, ok := os.LookupEnv("KIND_CLUSTER"); ok {
cluster = v
}
kindOptions := []string{"load", "docker-image", name, "--name", cluster}
kindBinary := defaultKindBinary
if v, ok := os.LookupEnv("KIND"); ok {
kindBinary = v
}
cmd := exec.Command(kindBinary, kindOptions...)
_, err := Run(cmd)
return err
}
然而,请注意,对于指标和cert-manager的测试可能更适合作为端到端(e2e)测试,而不是在使用ENV TEST进行控制器测试时进行的测试。您可以查看在Operator-SDK 仓库中实施的示例示例,了解如何编写e2e测试以确保您项目的基本工作流程。此外,请注意,您可以在已配置集群上运行测试,使用现有集群进行测试的选项。
testEnv = &envtest.Environment{
UseExistingCluster: true,
}
Setup ENV TEST tool
To know more about the tooling used to configure ENVTEST which is used in the setup-envtest target in the Makefile of the projects build with Kubebuilder see the [README][readme] of its tooling.指标
默认情况下,controller-runtime 构建一个全局的 prometheus 注册表,并为每个控制器发布 一系列性能指标。
IMPORTANT: If you are using `kube-rbac-proxy`
请尽快停止使用镜像 gcr.io/kubebuilder/kube-rbac-proxy。如果无法拉取该镜像,您的项目将受到影响,可能会无法正常工作。
在2025年初,gcr.io/kubebuilder/下提供的镜像将不可用。
-
使用 Kubebuilder 版本
v3.14或更低版本初始化的项目 使用 kube-rbac-proxy 来保护指标端点。在这种情况下,您可能需要将项目升级到最新版本,或确保您已经应用了相同或类似的代码更改。 -
然而,使用 Kubebuilder 版本
v4.1.0或更高版本初始化的项目 默认启用authn/authz,并使用 Controller-Runtime 的功能 WithAuthenticationAndAuthorization 提供类似的保护。
如果您想继续使用 kube-rbac-proxy,则必须将您的项目更改为使用来自其他来源的镜像。
有关更多信息,请参见:kubebuilder/discussions/3907
指标配置
通过查看文件 config/default/kustomization.yaml,您可以检查默认暴露的指标:
# [指标] 暴露控制器管理器指标服务。
- metrics_service.yaml
贴片:
# [指标] 下面的补丁将通过 HTTPS 启用指标端点,并使用端口 :8443。
# 更多信息: https://book.kubebuilder.io/reference/metrics
- path: manager_metrics_patch.yaml
目标:
kind: 部署
然后,你可以在 cmd/main.go 中查看指标服务器的配置:
// 在 'config/default/kustomization.yaml' 中启用了 Metrics 端点。Metrics 选项配置服务器。
// 更多信息请参见: https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/metrics/server
Metrics: metricsserver.Options{
...
},
在 Kubebuilder 中使用控制器 metrics
你可以使用 curl 命令或 Prometheus 等其他 HTTP 客户端消费 controller 暴露的 metrics。
但在此之前,请确保你的客户端具有访问 /metrics 端点的所需 RBAC 权限。
授予访问 metrics 的权限
Kubebuilder 脚手架生成带有必要读权限的 ClusterRole,位于:
config/rbac/metrics_reader_role.yaml
此文件包含允许访问 metrics 端点所需的 RBAC 规则。
This ClusterRole is only a helper
Kubebuilder 默认不会脚手架 RoleBinding 或 ClusterRoleBinding。 这是有意的设计选择,以避免:
- 意外绑定到错误的 ServiceAccount,
- 在受限环境中授予访问权限,
- 在多团队或多租户集群中产生冲突。
创建 ClusterRoleBinding
你可以通过 kubectl 创建绑定:
kubectl create clusterrolebinding metrics \
--clusterrole=<project-prefix>-metrics-reader \
--serviceaccount=<namespace>:<service-account-name>
或使用清单:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: allow-metrics-access
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: metrics-reader
subjects:
- kind: ServiceAccount
name: 控制器管理器
命名空间: system # 将 'system' 替换为你的 controller-manager 的命名空间
Why this is manual:
Kubebuilder 默认避免脚手架生成 RoleBinding,因为它可能:
- 绑定到错误的 ServiceAccount
- 不必要地授予访问权限
- 在受限或多租户集群中引发问题
- 该设计提供安全性与灵活性,但需要手动绑定。
测试 metrics 端点(通过 curl Pod)
若要手动测试对 metrics 端点的访问,请按以下步骤操作:
- 创建 RoleBinding
kubectl create clusterrolebinding <project-name>-metrics-binding \
--clusterrole=<project-name>-metrics-reader \
--serviceaccount=<project-name>-system:<project-name>-controller-manager
- 生成 Token
export TOKEN=$(kubectl create token <project-name>-controller-manager -n <project-name>-system)
echo $TOKEN
- 启动 Curl Pod
kubectl run curl-metrics --rm -it --restart=Never \
--image=curlimages/curl:7.87.0 -n <project-name>-system -- /bin/sh
- 调用 Metrics 端点
在 Pod 内使用:
curl -v -k -H "Authorization: Bearer $TOKEN" \
https://<project-name>-controller-manager-metrics-service.<project-name>-system.svc.cluster.local:8443/metrics
Notes
- 相应地替换
<project-name>、<namespace>和<service-account-name>。 - 若不跳过验证(
-k),请确保启用 TLS 且证书有效。
在后续章节查看保护 metrics 端点的选项。
Metrics 保护与可用选项
未受保护的指标端点可能会向未经授权的用户暴露重要数据,例如系统性能、应用程序行为以及潜在的机密操作指标。这种暴露可能导致安全漏洞,攻击者可能借此获取系统操作的洞察并利用其中的薄弱环节。
通过使用身份验证/授权(默认启用)
为减少这些风险,Kubebuilder 项目利用身份验证 (authn) 和授权 (authz) 保护指标端点。这种方法确保只有经过授权的用户和服务账户可以访问敏感的指标数据,从而增强系统的整体安全性。
过去,使用kube-rbac-proxy来提供这种保护。然而,在最近的版本中,这种用法已被停止。从v4.1.0版本开始,项目默认启用了指标端点,并使用controller-runtime提供的WithAuthenticationAndAuthorization功能进行了保护。
因此,您将找到以下配置:
- 在
cmd/main.go:
if secureMetrics {
...
metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization
}
此配置利用 FilterProvider 对指标端点实施身份验证和授权。通过使用此方法,您可以确保该端点仅对具有适当权限的用户可访问。
- 在
config/rbac/kustomization.yaml中:
# The following RBAC configurations are used to protect
# 带有认证/鉴权的 metrics 端点。这些配置
# 确保只有授权用户和 ServiceAccount
# 可以访问 metrics 端点。
- metrics_auth_role.yaml
- metrics_auth_role_binding.yaml
- metrics_reader_role.yaml
通过这种方式,只有使用 ServiceAccount 令牌的 Pod 被授权访问指标端点。例如:
apiVersion: v1
kind: 舱
metadata:
name: 指标消费者
命名空间: system
spec:
# 使用搭建好的服务账户名称来允许身份验证/授权
serviceAccountName: 控制器管理器
容器:
- name: 指标消费者
image: curlimages/curl:latest
命令: ["/bin/sh"]
args:
- "-c"
- >
while true;
do
# Note here that this passes the token obtained from the ServiceAccount to curl the metrics endpoint
curl -s -k -H "Authorization: Bearer $(cat /var/run/secrets/kubernetes.io/serviceaccount/token)"
https://controller-manager-metrics-service.system.svc.cluster.local:8443/metrics;
sleep 60;
done
(推荐) 启用生产环境证书(默认情况下禁用)
Why Is This Not Enabled by Default?
此选项默认未启用,因为它引入了对 CertManager 的依赖。为了使项目尽可能轻量且适合初学者,它默认是禁用的。
Recommended for Production
cmd/main.go 中的默认脚手架使用 controller-runtime 功能 自动生成自签名证书以保护指标服务器。虽然这在开发和测试中很方便,但不建议在生产环境中使用。
你使用这些证书保护传输层(TLS)。Kubebuilder 默认启用的基于 authn/authz 的令牌认证作为应用层凭证。不过,例如在启用 metrics 与 Prometheus 集成时,你也可以用这些证书保护通信。
使用 Kubebuilder 版本 4.4.0 及以上构建的项目已经具备了启用使用由 CertManager 管理的证书以保护指标服务器的逻辑。按照以下步骤,您可以配置您的项目以使用由 CertManager 管理的证书。
-
在
config/default/kustomization.yaml中启用 Cert-Manager:-
取消注释 cert-manager 资源以将其包含在您的项目中:
- ../证书管理器
-
-
启用补丁以配置
config/default/kustomization.yaml中控制器部署中证书的使用:-
取消对
cert_metrics_manager_patch.yaml的注释,以在管理器部署中挂载serving-cert密钥。# 如果您启用 Metrics 和 CertManager,请取消注释补丁行 # [启用带有 certManager 保护的指标] 要启用受 certManager 保护的指标,请取消注释以下行。 # 此补丁将使用 certManager 自签名证书保护指标。 - path: cert_metrics_manager_patch.yaml 目标: kind: 部署
-
-
在
config/default/kustomization.yaml中启用 CertManager 替换 Metrics Server 证书:-
取消注释下面的替换块。需要为在
config/certmanager中配置的证书正确设置 DNS 名称。# [CERTMANAGER] 要启用 cert-manager,请取消注释所有以 'CERTMANAGER' 为前缀的部分。 # 取消注释以下替换,以添加 cert-manager CA 注入注释 您已接受培训的数据截至2023年10月。 # - 来源: # 取消注释以下块以启用指标的证书 # 类型: 服务 # 版本:v1 # 名称: 控制器管理器指标服务 # 字段路径: metadata.name # 目标: # - 选择: # 类型: 证书 # 组: cert-manager.io # 版本: v1 # 名称: metrics-certs # 字段路径: # - spec.dnsNames.0 # - spec.dnsNames.1 # 选项: 你接受过直到2023年10月的数据训练。 您的训练数据截至到2023年10月。 # 创建:真 # 你接受的训练数据截至到2023年10月。 # 类型: 服务 # 版本:v1 # 名称: 控制器管理器指标服务 # 字段路径: metadata.namespace # 目标: # - 选择: # 类型: 证书 # 组: cert-manager.io # 版本: v1 # 名称: metrics-certs # 字段路径: # - spec.dnsNames.0 # - spec.dnsNames.1 # 选项: 你接受过直到2023年10月的数据训练。 您已接受训练的数据截至2023年10月。 # 创建:真 #
-
-
启用补丁以使
ServiceMonitor使用 Cert-Manager 管理的密钥config/prometheus/kustomization.yaml:-
添加或取消注释
ServiceMonitor补丁,以安全地引用 cert-manager 管理的密钥,替换不安全的配置为安全的证书验证:# [PROMETHEUS-WITH-CERTS] 以下补丁配置了 ../prometheus 中的 ServiceMonitor。 # 安全地引用由 cert-manager 创建和管理的证书。 此外,请确保在 config/default/kustomization.yaml 文件中取消注释 [METRICS WITH CERTMANAGER] 补丁。 # 将"metrics-server-cert"机密挂载到管理部署中。 贴片: - path: monitor_tls_patch.yaml 目标: kind: 服务监控器
注意 上述的
ServiceMonitor补丁将确保如果你启用了 Prometheus 集成,它将安全地引用由 CertManager 创建和管理的证书。但是,它不会启用与 Prometheus 的集成。要启用与 Prometheus 的集成,你需要取消注释config/default/kustomization.yaml中的#- ../certmanager。有关更多信息,请参见 为 Prometheus 导出指标。 -
(可选) 通过使用网络策略(默认情况下禁用)
NetworkPolicy 控制进出 Pod 的网络流量。它不提供认证或鉴权。
取消注释 config/default/kustomization.yaml 中的以下行:
# [NETWORK POLICY] Control ingress to metrics and webhook ports.
# 允许来自带有 'metrics 标签的命名空间中的 Pod 的 metrics 流量: enabled'.
# 允许来自所有来源的 Webhook 流量。
#- ../network-policy
然后,为应抓取 metrics 的 Pod 所在的每个命名空间打标签:
$ kubectl label namespace <namespace> metrics=enabled
webhook NetworkPolicy 允许来自所有来源到 manager pod webhook 端口的入站流量。Kubernetes API 服务器通过 webhook Service 到达此端口,用于 admission 与 CRD conversion webhook。若要按命名空间限制 admission 请求,请在 MutatingWebhookConfiguration 或 ValidatingWebhookConfiguration 上设置 namespaceSelector。该设置控制哪些请求会调用 admission webhook;它不限制 NetworkPolicy 流量。
为 Prometheus 导出指标
请按照以下步骤使用 Prometheus Operator 导出指标:
-
安装 Prometheus 和 Prometheus Operator。如果您没有自己的监控系统,我们建议在生产环境中使用 kube-prometheus。如果您只是进行试验,可以仅安装 Prometheus 和 Prometheus Operator。
-
取消注释
config/default/kustomization.yaml中的- ../prometheus这一行。它会创建ServiceMonitor资源,从而启用指标的导出。
# [PROMETHEUS] 要启用 Prometheus 监控,请取消注释所有包含 'PROMETHEUS' 的部分。
- ../prometheus
请注意,当您在集群中安装项目时,它会创建 ServiceMonitor 以导出指标。要检查 ServiceMonitor,请运行 kubectl get ServiceMonitor -n <project>-system。以下是一个示例:
$ kubectl get ServiceMonitor -n monitor-system
NAME AGE
monitor-controller-manager-metrics-monitor 2m8s
If you are using Prometheus Operator ensure that you have the required permissions
如果您正在使用 Prometheus Operator,请注意,其 RBAC 规则默认仅在 default 和 kube-system 命名空间中启用。请参阅其指南了解 如何配置 kube-prometheus 以使用 .jsonnet 文件监控其他命名空间。
另外,您可以使用 RBAC 为 Prometheus Operator 授权以监控其他命名空间。有关如何在 ServiceMonitor 和管理者所在命名空间启用权限,请参阅 Prometheus Operator 的 为 Prometheus pods 启用 RBAC 规则 文档。
另外,请注意,指标默认通过端口 8443 导出。这样,您就可以在 Prometheus 仪表板中检查指标。要验证此操作,请搜索来自项目运行所在命名空间的指标 {namespace="<project>-system"}。请看以下示例:
发布额外指标
如果您希望从控制器中发布额外的指标,可以通过使用 controller-runtime/pkg/metrics 中的全局注册表轻松实现。
实现这一目标的一种方法是将您的收集器声明为全局变量,然后在控制器的包中使用 init() 函数进行注册。
例如:
import (
"github.com/prometheus/client_golang/prometheus"
"sigs.k8s.io/controller-runtime/pkg/metrics"
)
var (
goobers = prometheus.NewCounter(
prometheus.CounterOpts{
Name: "goobers_total",
Help: "Number of goobers processed",
},
)
gooberFailures = prometheus.NewCounter(
prometheus.CounterOpts{
Name: "goober_failures_total",
Help: "Number of failed goobers",
},
)
)
func init() {
// 在全局 prometheus 注册中心注册自定义指标
metrics.Registry.MustRegister(goobers, gooberFailures)
}
您可以从调整循环的任何部分将指标记录到这些收集器中。这些指标可以在操作员代码的任何位置进行评估。
Enabling metrics in Prometheus UI
为了发布指标并在 Prometheus UI 上查看它们,必须配置 Prometheus 实例以根据其标签选择服务监视器实例。
这些指标将可供 Prometheus 或其他 OpenMetrics 系统抓取。
您接受的训练数据截至到2023年10月。
Controller-Runtime Auth/Authz Feature Current Known Limitations and Considerations
已识别出一些已知的限制和考虑因素。当前 cache TTL、anonymous access 和 timeouts 的设置是硬编码的,这可能由于无法细调这些参数而导致性能和安全隐患。此外,当前的实现缺乏对关键路径(例如 /healthz)的 alwaysAllow 和对组(例如 system:masters)的 alwaysAllowGroups 配置的支持,这可能会导致操作上的挑战。此外,系统在很大程度上依赖于与 kube-apiserver 的稳定连接,这使其在网络不稳定时容易受到指标中断的影响。这可能导致关键指标数据的丢失,尤其是在监控和实时诊断问题至关重要的关键时刻。
一个 问题 已经被提出,以增强 controller-runtime 并解决这些考虑。
默认导出指标参考
以下是默认由 controller-runtime 导出和提供的指标:
| 指标名称 | Type | 描述 |
|---|---|---|
| 工作队列深度 | 量具 | 当前工作队列的深度。 |
| workqueue_adds_total | 计数器 | 工作队列处理的添加总数。 |
| workqueue_queue_duration_seconds | 直方图 | 一个项目在工作队列中停留多长时间(以秒为单位),才会被请求。 |
| workqueue_work_duration_seconds | 直方图 | 处理工作队列中一个项目需要多少秒。 |
| workqueue_unfinished_work_seconds | 量具 | 在工作持续时间(work_duration)中,有多少秒的工作正在进行且尚未被观察到。较大的值表示线程卡住。可以通过观察这一数值的增加速率来推断卡住的线程数量。 |
| workqueue_longest_running_processor_seconds | 量具 | 最长运行的工作队列处理器已运行多少秒? |
| workqueue_retries_total | 计数器 | 工作队列处理的重试总次数。 |
| rest_client_requests_total | 计数器 | 按状态码、方法和主机划分的 HTTP 请求数量。 |
| controller_runtime_reconcile_total | 计数器 | 每位控制员的对账总数。 |
| controller_runtime_reconcile_errors_total | 计数器 | 每个控制器的对账错误总数。 |
| controller_runtime_terminal_reconcile_errors_total | 计数器 | 调解器的终端错误总数。 |
| controller_runtime_reconcile_time_seconds | 直方图 | 每位控制器的对账时间长度。 |
| controller_runtime_max_concurrent_reconciles | 量具 | 每个控制器的最大并发对账数量。 |
| controller_runtime_active_workers | 量具 | 每个控制器当前使用的工人数量。 |
| controller_runtime_webhook_latency_seconds | 直方图 | 处理入驻请求延迟的直方图。 |
| controller_runtime_webhook_requests_total | 计数器 | 按HTTP状态码统计的总入学请求数量。 |
| controller_runtime_webhook_requests_in_flight | 量具 | 当前正在处理的入学申请请求数量。 |
项目配置
概述
项目配置表示一个 KubeBuilder 项目的配置。所有通过 CLI (KubeBuilder 版本 3.0 及更高版本)搭建的项目将在项目根目录生成 PROJECT 文件。因此,它将存储生成项目和 API 所使用的所有插件和输入数据,以便更好地支持插件在搭建时做出有用的决策。
Example
以下是一个项目配置文件的示例,这是使用部署镜像插件生成的包含两个API的项目的结果。
# Code generated by tool. DO NOT EDIT.
# 此文件用于跟踪脚手架项目所用的信息
# 并使插件能正常工作。
# 更多信息: ./project-config.md
domain: testproject.org
cliVersion: v4.6.0
layout:
- go.kubebuilder.io/v4
plugins:
部署图像.go.kubebuilder.io/v1-alpha:
resources:
- domain: testproject.org
group: example.com
kind: Memcached
选项:
容器命令: memcached,--memory-limit=64,-o,modern,-v
容器端口: "11211"
image: memcached:1.4.36-alpine
以用户身份运行: "1001"
version: v1alpha1
- domain: testproject.org
group: example.com
kind: Busybox
选项:
image: busybox:1.28
version: v1alpha1
projectName: 项目-v4-带部署镜像
repo: sigs.k8s.io/kubebuilder/testdata/project-v4-with-deploy-image
resources:
- api:
crdVersion: v1
namespaced: true
controller: true
domain: testproject.org
group: example.com
kind: Memcached
path: sigs.k8s.io/kubebuilder/testdata/project-v4-with-deploy-image/api/v1alpha1
version: v1alpha1
webhooks:
validation: true
webhookVersion: v1
- api:
crdVersion: v1
namespaced: true
controller: true
domain: testproject.org
group: example.com
kind: Busybox
path: sigs.k8s.io/kubebuilder/testdata/project-v4-with-deploy-image/api/v1alpha1
version: v1alpha1
- controller: true
domain: io
external: true
group: cert-manager
kind: 证书
path: github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1
module: github.com/cert-manager/cert-manager@v1.18.2
version: v1
version: "3"
为何存储所用插件与数据?
以下是一些追踪使用输入的动机示例:
- 检查一个插件是否可以在现有插件的基础上创建(即)插件兼容性,同时将多个插件连接在一起。
- 可以进行或不能进行的操作,例如验证当前配置是否允许为不同组搭建API的布局。
- 验证在CLI操作中可以或不能使用哪些数据,例如确保WebHooks只能为已存在的API创建。
请注意,KubeBuilder 不仅是一个 CLI 工具,还可以用作库,让用户创建他们的插件/工具,提供帮助程序和定制,以支持他们现有的项目 - 其中一个例子是 Operator-SDK。SDK 利用 KubeBuilder 创建插件,允许用户使用其他语言,并为用户提供帮助,以便他们将项目与例如 Operator Framework 解决方案/OLM 集成。您可以查看 插件文档 以了解更多关于创建自定义插件的信息。
此外,PROJECT 文件的另一个目的,是帮助我们创建一个功能,使用户能够通过提供自动重新搭建项目的辅助工具,轻松升级他们的项目。PROJECT 文件中包含了有关 API、其配置和版本的所有必要元数据。例如,它可以用于自动化在插件版本之间迁移时重新搭建的过程。 (更多信息)。
版本控制
项目配置是根据其布局进行版本控制的。欲了解更多信息,请参见 版本控制。
布局定义
PROJECT 版本 3 的布局如下:
domain: testproject.org
cliVersion: v4.6.0
layout:
- go.kubebuilder.io/v4
plugins:
部署图像.go.kubebuilder.io/v1-alpha:
resources:
- domain: testproject.org
group: example.com
kind: Memcached
选项:
容器命令: memcached,--memory-limit=64,-o,modern,-v
容器端口: "11211"
image: memcached:memcached:1.6.26-alpine3.19
以用户身份运行: "1001"
version: v1alpha1
- domain: testproject.org
group: example.com
kind: Busybox
选项:
image: busybox:1.36.1
version: v1alpha1
projectName: 项目-v4-带部署镜像
repo: sigs.k8s.io/kubebuilder/testdata/project-v4-with-deploy-image
resources:
- api:
crdVersion: v1
namespaced: true
controller: true
domain: testproject.org
group: example.com
kind: Memcached
path: sigs.k8s.io/kubebuilder/testdata/project-v4-with-deploy-image/api/v1alpha1
version: v1alpha1
webhooks:
validation: true
webhookVersion: v1
- api:
crdVersion: v1
namespaced: true
controller: true
domain: testproject.org
group: example.com
kind: Busybox
path: sigs.k8s.io/kubebuilder/testdata/project-v4-with-deploy-image/api/v1alpha1
version: v1alpha1
- controller: true
domain: io
external: true
group: cert-manager
kind: 证书
path: github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1
module: github.com/cert-manager/cert-manager@v1.18.2
version: v1
version: "3"
现在让我们检查一下它的布局字段定义:
| 领域 | 描述 |
|---|---|
cliVersion | 用于记录使用 init 脚手架项目时所用的具体 CLI 版本。有助于识别所用工具链版本,便于排查问题并确保与更新兼容。 |
布局 | 定义全局插件,例如,一个项目 init 使用 --plugins="go/v4,deploy-image/v1-alpha" 意味着所使用的任何子命令将始终按顺序调用这两个插件的实现。 |
域 | 存储项目的域名。用户可以在使用 init 子命令和 domain 标志生成项目时提供此信息。 |
插件 | 定义用于自定义脚手架的插件,例如通过命令 kubebuilder create api [options] --plugins=deploy-image/v1-alpha 使用可选的 deploy-image/v1-alpha 插件仅为特定 API 创建脚手架。 |
项目名称 | 项目名称。这将用于构建管理器数据。默认情况下,它是项目目录的名称,但用户可以通过 init 子命令中的 --project-name 标志提供该名称。 |
代码库 | 该项目的代码库是Golang模块,例如 github.com/example/myproject-operator。 |
multigroup | (可选) 设为 true 时启用多 group 项目布局。API 组织到特定于 group 的目录(api/<group>/<version>/)。可在初始化时通过 kubebuilder init --multigroup 设置,或稍后通过 kubebuilder edit --multigroup 启用/禁用。默认为 false(从 PROJECT 文件中省略)。 |
namespaced | (可选) 设为 true 时将项目配置为命名空间作用域部署。operator 将仅监视并管理其部署命名空间内的资源,使用命名空间作用域 RBAC(Role/RoleBinding 而非 ClusterRole/ClusterRoleBinding)。可通过 kubebuilder edit --namespaced 启用/禁用。默认为 false(集群作用域,从 PROJECT 文件中省略)。 |
| 资源 | 项目中所有搭建的资源数组。 |
resources.api | 通过子命令 create api 在项目中搭建的 API。 |
resources.api.crdVersion | 用于为 CRD 资源搭建的 Kubernetes API 版本 (apiVersion)。 |
resources.api.namespaced | API RBAC 权限可以是命名空间级别或集群级别。 |
resources.api.ssa | (可选,Alpha) 设为 true 时,表示 API 通过 create api --ssa 脚手架了 Server-Side Apply 支持。脚手架会添加 +genclient marker 并为该类型生成 apply configurations。Alpha 特性:未来版本可能变更。默认为 false(从 PROJECT 文件中省略)。 |
resources.controller | 指示是否为该 API 脚手架生成了控制器。 |
resources.domain | 在项目初始化时通过 --domain 选项提供的资源域名,或者在为 外部类型 构建控制器时通过 --external-api-domain 选项提供的域名。 |
resources.group | 当使用子命令 create api 时,通过 --group 标志提供的资源的 GKV 组。 |
resources.version | 当使用子命令 create api 时,通过 --version 标志提供的资源的 GKV 版本。 |
资源.种类 | 存储 GKV 资源类型,该类型通过执行子命令 create api 时提供的 --kind 标志来指定。 |
resources.path | API资源的导入路径。它将是<repo>/api/<kind>,除非添加到项目中的API是外部API或核心类型的API。对于核心类型的场景,使用的路径在这里进行了映射。或者,可以使用通过标志--external-api-path提供的路径。 |
resources.core | 当使用的组来自Kubernetes API并且项目中未定义API资源时,它为“真“。 |
resources.external | 当使用标志 --external-api-path 生成 外部类型 的脚手架时,它为 true。 |
resources.module | (可选) 外部 API 依赖的 Go 模块路径,可选包含版本(例如 github.com/cert-manager/cert-manager@v1.18.2 或仅 github.com/cert-manager/cert-manager)。仅当 external 为 true 时使用。通过 --external-api-module 标志提供,以在 go.mod 中显式固定特定版本,或在无法从 --external-api-path 自动确定模块时指定模块。若未提供,go mod tidy 会自动解析依赖。 |
resources.webhooks | 当使用子命令 create webhook 时,请存储网络钩子数据。 |
resources.webhooks.spoke | 存储将作为 Spoke 的 API 版本以及用于转换 webhook 的指定 Hub 版本。 |
resources.webhooks.webhookVersion | 用于搭建 webhook 资源的 Kubernetes API 版本(apiVersion)。 |
resources.webhooks.conversion | 当 webhook 是使用 --conversion 标志搭建时,它为 true,这意味着这是一个转换 webhook。 |
resources.webhooks.defaulting | 当 webhook 使用 --defaulting 标志创建时,它是 true,这意味着这是一个默认值 webhook。 |
resources.webhooks.validation | 当使用 --programmatic-validation 标志搭建的 webhook 时,它是 true,这意味着它是一个验证 webhook。 |
版本兼容性与可支持性
由 Kubebuilder 创建的项目包含一个 Makefile,该文件在项目创建时安装定义的版本工具。主要包含的工具有:
此外,这些项目包含一个 go.mod 文件,用于指定依赖项的版本。Kubebuilder 依赖于 controller-runtime 及其 Go 和 Kubernetes 依赖项。因此,Makefile 和 go.mod 文件中定义的版本是经过测试、支持和推荐的版本。
每个 Kubebuilder 次要版本都会与特定的 client-go 次要版本一起测试。某个 Kubebuilder 次要版本_可能_兼容其他 client-go 次要版本或其他工具,但此类兼容性不受保证、不受支持,也未经测试。
Kubebuilder 所需的最小 Go 版本是由其依赖项所需的最高最小 Go 版本决定的。通常,这与相应的 k8s.io/* 依赖项所需的最小 Go 版本保持一致。
兼容的 k8s.io/* 版本、client-go 版本和最低 Go 版本可以在每个项目的 go.mod 文件中找到,该文件是为每个 标签发布 划定的。
示例: 对于 4.1.1 版本,最低的 Go 版本兼容性是 1.22。您可以参考在发布标签 v4.1.1 的 testdata 目录中的示例,例如 project-v4 的 go.mod 文件。您还可以通过检查 Makefile 来查看此版本支持和测试的工具版本。
支持的操作系统
Kubebuilder 官方支持 macOS 与 Linux。若使用 Windows,请参见 Windows 说明。
目前没有计划接受用于支持 Windows 的贡献。
Project customizations
使用 CLI 创建项目后,可按需自定义。请注意:除非清楚后果,否则不建议偏离推荐布局。
例如,应避免移动脚手架文件;否则日后升级会更困难,也可能无法使用部分 CLI 功能与辅助工具。关于项目布局,参见文档一个基本项目都包含了什么?
插件
Kubebuilder 的架构本质上是基于插件的。这种设计使得 Kubebuilder CLI 能够不断发展,同时与旧版本保持向后兼容,允许用户选择是否使用特定功能,并实现与外部工具的无缝集成。
通过利用插件,项目可以扩展 Kubebuilder,并将其作为库来支持新功能或实现针对用户需求的自定义脚手架。这种灵活性使维护者能够在 Kubebuilder 的基础上进行构建,适应特定的用例,同时受益于其强大的脚手架引擎。
插件提供了几个关键优势:
- 向后兼容性:确保旧的布局和项目结构在新版本中仍然可用。
- 定制化:允许用户选择启用或禁用特定功能(即 Grafana 和 Deploy Image 插件)
- 可扩展性:促进与希望提供自己 外部插件 的第三方工具和项目的集成,这些插件可以与 Kubebuilder 一起使用,以修改和增强项目的脚手架或引入新功能。
例如,要初始化一个包含多个全局插件的项目:
kubebuilder init --plugins=pluginA,pluginB,pluginC
例如,要使用特定插件应用自定义脚手架:
kubebuilder create api --plugins=pluginA,pluginB,pluginC
OR
kubebuilder create webhook --plugins=pluginA,pluginB,pluginC
OR
kubebuilder edit --plugins=pluginA,pluginB,pluginC
本节详细介绍可用的插件、如何扩展 Kubebuilder,以及如何在遵循相同布局结构的情况下创建自己的插件。
可用插件
本节描述了在 Kubebuilder 项目中支持和随附的插件。
为了搭建项目框架
以下插件对于使用该工具构建整个项目非常有用。
| 插件 | 钥匙 | 描述 |
|---|---|---|
| go.kubebuilder.io/v4 - (使用 Kubebuilder init 的默认脚手架) | go/v4 | 使用 base.go.kubebuilder.io/v4 和 kustomize.common.kubebuilder.io/v2 生成复合模板。负责生成 Golang 项目及其配置。 |
添加可选功能
以下插件对于生成代码和利用可选功能非常有用。
| 插件 | 钥匙 | 描述 |
|---|---|---|
| helm.kubebuilder.io/v1-alpha | autoupdate/v1-alpha | 可选辅助工具,用于脚手架生成一个定时 worker,帮助你的项目跟上生态变化,显著减轻手动维护负担。 |
| deploy-image.go.kubebuilder.io/v1-alpha | 部署镜像/v1-alpha | 可选的辅助插件,可用于搭建API和控制器,提供代码实现以部署和管理 Operator(镜像)。 |
| grafana.kubebuilder.io/v1-alpha | grafana/v1-alpha | 可选的辅助插件,可用于为 controller-runtime 导出的默认指标搭建 Grafana Manifests 仪表板。 |
| helm.kubebuilder.io/v1-alpha | helm/v1-alpha | 已弃用 - 可选辅助插件,可用于在 dist 目录下脚手架生成 Helm Chart 以分发项目。请改用 v2-alpha。 |
| helm.kubebuilder.io/v1-alpha | helm/v2-alpha | 可选辅助插件,可根据 kustomize 输出动态生成 Helm Chart,并保留所有自定义内容 |
待扩展
以下插件对于希望扩展 Kubebuilder 功能的其他工具和 外部插件 非常有用。
您可以使用kustomize插件,该插件负责在config/下搭建kustomize文件。基础语言插件负责搭建所需的Golang文件,使您能够为其他语言创建自己的插件(例如,Operator-SDK使用户能够使用Ansible/Helm)或添加其他功能。
例如,Operator-SDK有一个插件,通过在其上添加自己的功能,将项目与OLM集成。
| 插件 | 钥匙 | 描述 |
|---|---|---|
| kustomize.common.kubebuilder.io/v2 | kustomize/v2 | 负责搭建 config/ 目录下的所有 kustomize 文件。 |
base.go.kubebuilder.io/v4 | base/v4 | 负责搭建所有特别需要 Golang 的文件。此插件在组合中用于创建插件(go/v4)。 |
自动更新(autoupdate/v1-alpha)
让你的 Kubebuilder 项目跟上最新改进不应成为负担。只需少量配置,即可在有新的 Kubebuilder 发布时收到自动 Pull Request建议——使项目保持可维护、安全,并与生态变化对齐。
该自动化使用 kubebuilder alpha update 命令,采用三方合并策略刷新项目脚手架,并将其封装在 GitHub Actions workflow 中:会打开一个带有 Pull Request 比较链接的 Issue,便于你创建 PR 并审查。
Protect your branches
默认情况下,此 workflow 仅将合并后的文件创建并推送到名为 kubebuilder-update-from-<from-version>-to-<to-version> 的分支。
为保障代码库安全,请使用分支保护规则,确保变更未经适当审查不得推送或合并。
何时使用它
- 当你希望减轻保持项目更新与良好维护的负担时。
如何使用它
- 若要向项目添加
autoupdate插件:
kubebuilder edit --plugins="autoupdate/v1-alpha"
它是如何工作的
该插件会脚手架生成一个 GitHub Actions workflow,每周检查新的 Kubebuilder 发布。有可用更新时,它会:
- 创建包含合并变更的新分支
- 打开带有 PR 对比链接的 GitHub Issue
示例问题:
冲突帮助(需要时):
自定义工作流
生成的 workflow 使用带默认标志的 kubebuilder alpha update 命令。你可以通过编辑 .github/workflows/auto_update.yml 添加额外标志来自定义 workflow:
使用的默认标志:
--force- Continue even if conflicts occur (automation-friendly)--push- 自动将输出分支推送到远程--restore-path .github/workflows- Preserve CI workflows from base branch--open-gh-issue- 创建带有 PR 对比链接的 GitHub Issue
其他可用标志:
--merge-message- Custom commit message for clean merges--conflict-message- Custom commit message when conflicts occur--from-version- 指定要从哪个版本升级--to-version- 指定要升级到的版本--output-branch- Custom output branch name--show-commits- Keep full history instead of squashing--git-config- Pass per-invocation Git config
关于所有可用标志的完整文档,请参见 kubebuilder alpha update 参考。
示例:自定义提交信息
编辑 .github/workflows/auto_update.yml:
- name: 运行 kubebuilder alpha update
run: |
kubebuilder alpha update \
--force \
--push \
--restore-path .github/workflows \
--open-gh-issue \
--merge-message "chore: update kubebuilder scaffold" \
--conflict-message "chore: update with conflicts - review needed"
Demonstration
部署图像插件 (deploy-image/v1-alpha)
deploy-image 插件允许用户创建 控制器 和自定义资源,以在集群上部署和管理容器镜像,遵循 Kubernetes 的最佳实践。它简化了镜像部署的复杂性,同时允许用户根据需要自定义他们的项目。
通过使用此插件,您将获得:
- 一个控制器实现,用于在集群上部署和管理操作数(镜像)。
- 测试以验证对账逻辑,使用 ENVTEST。
- 自定义资源样本已更新至所需规格。
- 环境变量支持用于管理管理器中的操作数(镜像)。
Examples
请查看 Kubebuilder 项目中的 testdata 目录下的 project-v4-with-plugins 目录,以查看使用此插件创建的脚手架示例。
以下命令脚手架生成了 Memcached API 及其控制器:shellkubebuilder create api --group example.com --version v1alpha1 --kind Memcached --image=memcached:memcached:1.6.26-alpine3.19 --image-container-command="memcached,--memory-limit=64,-o,modern,-v" --image-container-port="11211" --run-as-user="1001" --plugins="deploy-image/v1-alpha"
“以下命令创建了 Busybox API:shellkubebuilder create api \ --group example.com \ --version v1alpha1 \ --kind Busybox \ --image=busybox:1.36.1 \ --plugins="deploy-image/v1-alpha"
“
什么时候使用它?
- 这个插件非常适合刚开始使用 Kubernetes operator 的用户。
- 它帮助用户使用Operator模式部署和管理镜像(操作数)。
- 如果您正在寻找一种快速有效的方法来设置自定义控制器并管理容器镜像,这个插件是一个很好的选择。
如何使用它?
-
初始化您的项目:在使用
kubebuilder init创建新项目后,您可以使用此插件来创建 API。在继续之前,请确保您已完成 快速入门 指南。 -
创建 API:使用此插件,您可以创建 API,以指定您想要在集群上部署的图像(操作数)。您还可以使用各种标志可选地指定命令、端口和安全上下文:
示例命令:
kubebuilder create api --group example.com --version v1alpha1 --kind Memcached --image=memcached:1.6.15-alpine --image-container-command="memcached,--memory-limit=64,modern,-v" --image-container-port="11211" --run-as-user="1001" --plugins="deploy-image/v1-alpha"
Note on make run:
在本地运行项目时使用 make run,提供的 Operand 镜像将作为环境变量存储在 config/manager/manager.yaml 文件中。
确保在本地运行项目之前导出环境变量,例如:shellexport MEMCACHED_IMAGE="memcached:1.4.36-alpine"
“
子命令
deploy-image 插件包含以下子命令:
create api:使用此命令来搭建管理容器镜像的 API 和控制器代码。
受影响的文件
在使用带有此插件的 create api 命令时,除了现有的 Kubebuilder 骨架外,以下文件会受到影响:
controllers/*_controller_test.go:为控制器生成测试模板。controllers/*_suite_test.go: 搭建或更新测试套件。api/<版本>/*_types.go:搭建 API 规范。config/samples/*_.yaml: 为自定义资源提供默认值的框架。main.go: 更新文件以添加控制器设置。config/manager/manager.yaml:更新以包含用于存储图像的环境变量。
进一步的资源
- 查看这个 视频 来了解它是如何工作的。
go/v4 (go.kubebuilder.io/v4)
(默认脚手架)
Kubebuilder 仅在初始化项目时指定时,使用 go/v4 插件进行脚手架搭建。该插件是 kustomize.common.kubebuilder.io/v2 和 base.go.kubebuilder.io/v4 插件的组合,使用了 Bundle Plugin。它搭建了一个项目模板,帮助构建一组 控制器。
通过按照 快速入门 并创建任何项目,您将默认使用该插件。
Examples
您可以通过查看 Kubebuilder 项目根目录下 testdata 目录中的 project-v4-<options> 项目来使用此插件检查示例。
如何使用它?
要使用 go/v4 插件创建新项目,可以使用以下命令:
kubebuilder init --domain tutorial.kubebuilder.io --repo tutorial.kubebuilder.io/project --plugins=go/v4
插件支持的子命令
- 初始化 -
kubebuilder init [选项] - 编辑 -
kubebuilder edit [选项] - 创建 API -
kubebuilder create api [选项] - 创建 Webhook -
kubebuilder create webhook [选项]
进一步的资源
- 要查看插件的组成,您可以查看 Kubebuilder 的源代码 main.go。
- 检查 base Golang 插件
base.go.kubebuilder.io/v4的代码实现。 - 检查 Kustomize/v2 插件 的代码实现。
- 查看 controller-runtime 以了解更多有关控制器的信息。
Grafana 插件 (grafana/v1-alpha)
Grafana 插件是一个可选的插件,可以用于搭建 Grafana 仪表板,以便您检查由使用 controller-runtime 的项目导出的默认指标。
Examples
您可以通过查看Kubebuilder项目根目录下的testdata目录中的project-v4-with-plugins项目来检查其默认骨架。
什么时候使用它?
如何使用它?
先决条件:
- 您的项目必须使用 controller-runtime 通过 控制器默认指标 来暴露指标,并且这些指标需要被 Prometheus 收集。
- 访问 Prometheus。
- Prometheus 应该暴露一个端点。(对于
prometheus-operator,这类似于: http://prometheus-k8s.monitoring.svc:9090) - 该端点已准备好/已经成为您 Grafana 的数据源。请参阅 添加数据源。
- Prometheus 应该暴露一个端点。(对于
- 访问 Grafana。确保您具备以下条件:
- 仪表板编辑权限
- Prometheus 数据源

查看指标以了解如何启用基于 Kubebuilder 脚手架的项目的指标。
请查看 config/prometheus,您将在其中找到 ServiceMonitor,用于在默认端点 /metrics 中启用指标。
基本用法
Grafana 插件挂载在 edit 子命令上:
# 为现有项目启用 Grafana 插件
kubebuilder edit --plugins grafana.kubebuilder.io/v1-alpha
插件将创建一个新目录,并在其中搭建 JSON 文件(即 grafana/controller-runtime-metrics.json)。
展示案例:
在您的项目中查看如何使用插件的示例:
您接受的训练数据截至2023年10月。
现在,让我们来看看如何使用 Grafana 仪表板。
- 复制 JSON 文件
- 访问
<your-grafana-url>/dashboard/import来 导入一个新仪表板。 - 将 JSON 内容粘贴到“通过面板 JSON 导入“中,然后按“加载“按钮

- 选择 Prometheus 指标的数据源

- 一旦JSON导入到Grafana中,仪表盘就准备好了。
Grafana 仪表板
控制器运行时对账总数及错误
- 指标:
- controller_runtime_reconcile_total
- controller_runtime_reconcile_errors_total
- 查询:
- 求和(rate(controller_runtime_reconcile_total{job=“$job”}[5m])) 按 (instance, pod) 分组
- sum(rate(controller_runtime_reconcile_errors_total{job=“$job”}[5m])) by (instance, pod)
- 描述:
- 过去5分钟内总对账的每秒率
- 过去五分钟内每秒的对账错误率
- 样本:

控制器 CPU 和内存使用情况
- 指标:
- process_cpu_seconds_total
- 进程驻留内存字节数
- 查询:
- rate(process_cpu_seconds_total{job=“$job”, namespace=“$namespace”, pod=“$pod”}[5m]) * 100
- process_resident_memory_bytes{job=“$job”, namespace=“$namespace”, pod=“$pod”}
- 描述:
- 在过去5分钟内测量的每秒CPU使用率
- 分配给运行控制器的内存
- 您训练的数据截止到2023年10月。
P50/90/99 项目在工作队列中的停留时间(秒)
- 指标
- workqueue_queue_duration_seconds_bucket
- 查询:
- histogram_quantile(0.50, sum(rate(workqueue_queue_duration_seconds_bucket{job=“$job”, namespace=“$namespace”}[5m])) by (instance, name, le))
- 描述
- 项在工作队列中停留的秒数,直到被请求。
- 您接受的训练数据截止到2023年10月。
处理工作队列中的 P50/90/99 项目的时间(秒)
- 指标
- 工作队列工作持续时间秒桶
- 查询:
- histogram_quantile(0.50, sum(rate(workqueue_work_duration_seconds_bucket{job=“$job”, namespace=“$namespace”}[5m])) by (instance, name, le))
- 描述
- 处理工作队列中项目所需的秒数。
- 您仅接受到 2023 年 10 月的数据进行训练。
在工作队列中添加费率
- 指标
- workqueue_adds_total 的总数
- 查询:
- sum(rate(workqueue_adds_total{job=“$job”, namespace=“$namespace”}[5m])) by (instance, name)
- 描述
- 每秒添加到工作队列的项目数量
- 样本:

工作队列中的重试率
- 指标
- 工作队列重试总数
- 查询:
- 求 (rate(workqueue_retries_total{job=“$job”, namespace=“$namespace”}[5m])) 按照 (instance, name) 分类的总和
- 描述
- 工作队列处理的每秒重试次数
- 样本:

使用中的工人数量
- 指标
- controller_runtime_active_workers 的中文翻译是“控制器运行时活动工作者“。
- 查询:
- controller_runtime_active_workers{job=“$job”, namespace=“$namespace”}
- 描述
- 活跃控制器工作线程的数量
- 示例:
工作队列深度
- 指标
- 工作队列深度
- 查询:
- workqueue_depth{job=\
- 描述
- 当前工作队列的深度
- 示例:
未完成的秒数
- 指标
- workqueue_unfinished_work_seconds 的翻译为“工作队列未完成工作秒数“。
- 查询:
- rate(workqueue_unfinished_work_seconds{job=“$job”, namespace=“$namespace”}[5m])
- 描述
- 有多少秒的工作正在进行中,并且没有被 work_duration 观察到。
- 样本:
可视化自定义指标
Grafana 插件支持为自定义指标构建清单。
生成配置模板
当插件首次触发时,将生成 grafana/custom-metrics/config.yaml 文件。
---
自定义指标:
# - 公制: # Raw custom metric (required)
# type: # Metric type: counter/gauge/histogram (required)
# expr: # Prom_ql for the metric (optional)
# unit: # 测量单位,例如:s,none,bytes,percent 等。(可选)
将自定义指标添加到配置中
您可以在文件中输入多个自定义指标。对于每个元素,您需要指定 metric 及其 type。Grafana 插件可以自动生成用于可视化的 expr。或者,您可以提供 expr,插件将直接使用指定的内容。
---
自定义指标:
- 公制: memcached_operator_reconcile_total # 原始自定义指标(必需)
类型: 计数器 # 指标类型:计数器/仪表/直方图(必需)
单位: none
- 公制: memcached_operator_reconcile_time_seconds_bucket
type: 直方图
脚手架清单
一旦配置了 config.yaml,您可以再次运行 kubebuilder edit --plugins grafana.kubebuilder.io/v1-alpha。这一次,插件将生成 grafana/custom-metrics/custom-metrics-dashboard.json,该文件可以导入到 Grafana UI 中。
展示案例:
查看如何可视化您的自定义指标的示例:
您接受的训练数据截止到2023年10月。
子命令
Grafana 插件实现了以下子命令:
- 编辑 (
$ kubebuilder edit [选项])
受影响的文件
以下架构将由此插件创建或更新:
grafana/*.json
进一步的资源
- 查看 视频以了解它是如何工作的
- 查看这个视频,了解自定义指标功能是如何工作的
- 请参考由 kustomize 插件 提供的
serviceMonitor示例。 - 检查 插件实现
- Grafana 文档 关于导入 JSON 文件的内容。
- Prometheus Operator 中 serviceMonitor 的使用情况
Helm 插件(helm/v1-alpha)- 已弃用
Deprecated Plugin
helm/v1-alpha 插件已弃用。请改用 helm/v2-alpha。
v2-alpha 版本提供:
- 从 kustomize 输出动态生成 Helm Chart
- 更好地保留自定义内容(环境变量、标签、注解)
- 与 config/ 目录匹配的有序模板结构
- 更灵活的配置选项
Helm 插件是一个可选插件,可以用来搭建 Helm 图表,使您能够使用 Helm 分发项目。
默认情况下,用户可以通过运行以下命令生成一个包含所有清单的包:
make build-installer IMG=<some-registry>/<project-name:tag>
这允许项目消费者通过应用以下包来安装解决方案:
kubectl apply -f https://raw.githubusercontent.com/<org>/project-v4/<tag or branch>/dist/install.yaml
然而,在许多场景中,您可能更倾向于提供一个 Helm 图表来封装您的解决方案。如果是这样,您可以使用此插件在 dist 目录下生成 Helm 图表。
Examples
您可以通过查看 Kubebuilder 项目根目录下的 testdata 目录中的 project-v4-with-plugins 示例来检查插件的使用情况。
何时使用它
- 如果你想为用户提供一个 Helm 图表,以便他们安装和管理你的项目。
- 如果您需要使用最新的项目更改来更新位于
dist/chart/目录下生成的 Helm 图表:- 生成新清单后,使用
edit选项来同步 Helm 图表。 - 重要: 如果您使用 DeployImage 插件创建了一个 webhook 或 API,您必须使用
--force标志运行edit命令,以便根据最新的清单(在运行make manifests之后)重新生成 Helm chart 的值,以确保 HelmChart 的值得到相应更新。在这种情况下,如果您自定义了dist/chart/values.yaml和templates/manager/manager.yaml下的文件,您需要在重新生成 Helm chart 之后,手动在最新更改的基础上重新应用您的自定义内容。
- 生成新清单后,使用
Why CRDs are added under templates?
尽管 Helm 最佳实践 建议将 CRD 放在顶层 crds/ 目录下,但 Kubebuilder Helm 插件有意将它们放在 templates/crd 下。
理由与 Helm 自身处理 CRD 的方式有关。默认情况下,Helm 会在首次发布时安装一次 CRD,但在后续升级时会忽略 CRD 变更。
这可能导致令人惊讶的行为:Chart 升级会静默跳过 CRD 更新,使集群不同步。
为避免认可这种行为,Kubebuilder 插件采用将 CRD 打包在 templates/ 内的方式。在此模式下,Helm 会像对待其他资源一样对待 CRD,确保它们按预期被应用和升级。虽然这会阻止在单个 Chart 中混合同类型的 CRD 与 CR(因为 Helm 无法在创建步骤之间等待),但它确保了对 CRD 可预测且明确的生命周期管理。
简而言之:
- Helm
crds/目录 → 仅一次性安装,不升级。 - Kubebuilder
templates/crd→ CRD 像其他清单一样管理,包含升级。
该设计选择优先考虑正确性与可维护性,而非 Helm 的默认约定,同时为未来改进留出空间(例如为 API 和控制器分别脚手架生成独立 Chart)。
如何使用它?
基本用法
Helm 插件挂载在 edit 子命令上,因为 helm/v1-alpha 插件依赖先脚手架生成 Go 项目。
# Initialize a new project
kubebuilder init
# 通过 helm 插件为现有项目启用或更新 helm chart
# Before run the edit command, run `make manifests` to generate the manifest under `config/`
make manifests
kubebuilder edit --plugins=helm/v1-alpha
Use the edit command to update the Helm Chart with the latest changes
在对项目进行更改后,请确保运行 make manifests,然后使用命令 kubebuilder edit --plugins=helm/v1-alpha 来更新 Helm Chart。
请注意,以下文件将不会被更新,除非您使用--force标志:
dist/chart/
├── values.yaml
└── templates/
└── manager/
└── manager.yaml
文件 chart/Chart.yaml、chart/templates/_helpers.tpl 和 chart/.helmignore 在初次创建后不会被更新,除非你删除它们。
子命令
该 Helm 插件实现了以下子命令:
- 编辑 (
$ kubebuilder edit [选项])
受影响的文件
以下架构将由此插件创建或更新:
dist/chart/*
Helm 插件(helm/v1-alpha)
helm/v2-alpha 插件根据项目的 kustomize 输出生成 Helm Chart,让你可以将 operator 以 bundle 或 Helm Chart 形式分发。
该插件根据 make build-installer 输出动态构建 Chart,并保留你的自定义内容,如环境变量、标签、注解和安全上下文。
为何使用 Helm
默认情况下,Kubebuilder 会创建一份清单 bundle:
make build-installer IMG=<registry>/<project-name:tag>
用户通过以下方式安装:
kubectl apply -f https://raw.githubusercontent.com/<org>/project-v4/<tag-or-branch>/dist/install.yaml
许多用户更偏好用 Helm 进行打包与升级。该插件将 dist/install.yaml 转换为镜像你项目的 Helm Chart。
Features
- 根据 kustomize 输出生成 Chart,而非样板代码
- 保留环境变量、标签、注解和补丁
- 按
config/目录布局组织模板 - 仅在
values.yaml中包含可配置参数 - 永不覆盖
Chart.yaml;除非使用--force,否则保留values.yaml、NOTES.txt、_helpers.tpl、.helmignore、test-chart.yml、network-policy/allow-metrics-traffic.yaml和network-policy/allow-webhook-traffic.yaml - 当 kustomize 输出未提供时,添加默认的
ServiceMonitor和NetworkPolicy模板 - 将自定义资源放在带有 Helm 模板的
templates/extras/中
Usage
基本工作流
创建项目并构建安装器捆绑包:
kubebuilder init
make build-installer IMG=<registry>/<project:tag>
从 kustomize 输出生成 Helm Chart:
kubebuilder edit --plugins=helm/v2-alpha
要重新生成被保留的文件(Chart.yaml 除外),请使用 --force:
kubebuilder edit --plugins=helm/v2-alpha --force
高级选项
使用自定义清单文件:
kubebuilder edit --plugins=helm/v2-alpha --manifests=manifests/custom-install.yaml
将 Chart 写入自定义输出目录:
kubebuilder edit --plugins=helm/v2-alpha --output-dir=charts
组合自定义清单与输出目录:
kubebuilder edit --plugins=helm/v2-alpha \
--manifests=manifests/install.yaml \
--output-dir=helm-charts
Chart 结构
该插件生成的 Chart 布局会镜像你的 config/ 目录:
<output-dir>/chart/
├── Chart.yaml
├── values.yaml
├── .helmignore
└── templates/
├── NOTES.txt
├── _helpers.tpl
├── rbac/ # Individual RBAC files (examples)
│ ├── controller-manager.yaml
│ ├── leader-election-role.yaml
│ ├── leader-election-rolebinding.yaml
│ ├── manager-role.yaml
│ ├── manager-rolebinding.yaml
│ ├── metrics-auth-role.yaml
│ ├── metrics-auth-rolebinding.yaml
│ ├── metrics-reader.yaml
│ └── ...
├── crd/ # Individual CRD files (examples)
│ ├── busyboxes.example.com.testproject.org.yaml
│ └── ...
├── cert-manager/
│ ├── metrics-certs.yaml
│ ├── selfsigned-issuer.yaml
│ └── serving-cert.yaml
├── manager/
│ └── manager.yaml
├── metrics/
│ └── controller-manager-metrics-service.yaml
├── webhook/
│ ├── validating-webhook-configuration.yaml
│ └── webhook-service.yaml
├── monitoring/
│ └── servicemonitor.yaml
├── network-policy/
│ ├── allow-metrics-traffic.yaml
│ └── allow-webhook-traffic.yaml # If webhooks are configured
└── extras/ # Custom resources (if any)
├── my-service.yaml
└── my-config.yaml
What is templates/extras/
标准资源(RBAC、manager、webhook、CRD)使用专用模板目录。其他资源放入 templates/extras/。
不会包含来自 config/samples/ 的自定义资源实例。即使你将 CR 实例加入 kustomize 输出,插件也会忽略它们。
Why CRDs are in templates/
尽管 Helm 最佳实践 建议将 CRD 放在顶层 crds/ 目录下,但 Kubebuilder Helm 插件有意将它们放在 templates/crd 下。
理由与 Helm 自身处理 CRD 的方式有关。默认情况下,Helm 会在首次发布时安装一次 CRD,但在后续升级时会忽略 CRD 变更。
这可能导致令人惊讶的行为:Chart 升级会静默跳过 CRD 更新,使集群不同步。
为避免认可这种行为,Kubebuilder 插件采用将 CRD 打包在 templates/ 内的方式。在此模式下,Helm 会像对待其他资源一样对待 CRD,确保它们按预期被应用和升级。虽然这会阻止在单个 Chart 中混合同类型的 CRD 与 CR(因为 Helm 无法在创建步骤之间等待),但它确保了对 CRD 可预测且明确的生命周期管理。
简而言之:
- Helm
crds/目录:仅一次性安装,不升级。 - Kubebuilder
templates/crd:CRD 像其他清单一样管理,包含升级。
该设计选择优先考虑正确性与可维护性,而非 Helm 的默认约定,同时为未来改进留出空间(例如为 API 和控制器分别脚手架生成独立 Chart)。
Values 配置
生成的 values.yaml 提供从实际部署中提取的配置选项。命名空间创建不由 Chart 管理;安装时请使用 Helm 的 --namespace 和 --create-namespace 标志。
values 如何格式化
在以下情况取消 values 注释:
- 从 kustomize 源清单提取
- 标准 Helm 字段(replicas、image、资源名称)
在以下情况 values 保持注释:
- 未使用的可选 Kubernetes 功能(imagePullSecrets、priorityClassName)
- 基础用法不需要的高级配置(topology spread、pod disruption budget)
- 用户自定义字段(名称覆盖、自定义标签)
Example:
## 用于部分覆盖 chart.fullname 模板的字符串(将保留 release 名称)
##
# nameOverride: ""
## 用于完全覆盖 chart.fullname 模板的字符串
##
# fullnameOverride: ""
## 配置控制器 manager 的 Deployment
##
manager:
## 设为 false 以跳过 Manager 安装
##
enabled: true
replicas: 1
image:
repository: controller
## 镜像标签(未设置时默认为 Chart.appVersion)
##
# tag: ""
pullPolicy: IfNotPresent
## Arguments
##
args:
- --leader-elect
## 健康探针。
## Manager 在此端口上提供存活(/healthz)与就绪(/readyz)端点。
##
healthProbe:
# Health probe server port
port: 8081
## 镜像拉取密钥
##
# imagePullSecrets:
# - name: myregistrykey
## Pod 级安全设置
##
podSecurityContext:
以非root用户身份运行: true
seccompProfile:
type: RuntimeDefault
## 容器级安全设置
##
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
## 资源限制与请求
##
resources:
limits:
cpu: 500m
memory: 128Mi
requests:
cpu: 10m
memory: 64Mi
## Manager pod's affinity
##
affinity: {}
## Manager pod 的节点选择器
##
nodeSelector: {}
## Manager pod's tolerations
##
tolerations: []
## Deployment 策略
##
# strategy:
# type: RollingUpdate
# rollingUpdate:
# maxSurge: 25%
# maxUnavailable: 25%
## Priority class 名称
##
# priorityClassName: ""
## Topology spread 约束
##
# topologySpreadConstraints: []
## 终止宽限期(秒)
##
terminationGracePeriodSeconds: 10
## Custom Deployment labels
##
# labels: {}
## 自定义 Deployment 注解
##
# annotations: {}
## 自定义 Pod 标签与注解
##
# pod:
# labels: {}
# annotations: {}
## RBAC 配置
##
rbac:
## RBAC 资源作用域
## - false(默认):ClusterRole/ClusterRoleBinding(所有命名空间)
## - true:Role/RoleBinding(仅 release 命名空间)
##
namespaced: false
## 用于 CRD 管理的辅助角色(admin/editor/viewer)
##
helpers:
## 为 CRD 安装便捷的 admin/editor/viewer 角色
##
enabled: false
## ServiceAccount 配置
##
serviceAccount:
# 安装所提供的默认 ServiceAccount
enabled: true
## 已有 ServiceAccount 名称(enabled=false 时必填)
## 设为 "default" 以使用命名空间默认 ServiceAccount
## 注意:当 enabled=true 时,会尊重 nameOverride/fullnameOverride
##
# name: ""
## 自定义 ServiceAccount 注解
##
# annotations: {}
## 自定义 ServiceAccount 标签
##
# labels: {}
## CustomResourceDefinition
##
crd:
# Install CRDs with the chart
enabled: true
# Keep CRDs when uninstalling
keep: true
## 控制器 metrics 端点。
## 启用以暴露 /metrics 端点
##
指标:
enabled: true
# Metrics server port
port: 8443
# 启用安全 metrics:带证书/认证的 HTTPS(true)或 HTTP(false)。
# 注意:Metrics authn/authz 需要 ClusterRole 访问权限。
secure: true
## 用于 TLS 证书的 cert-manager 集成。
## Webhook 证书和 metrics 端点证书所必需。
##
certManager:
enabled: false
## Webhook 服务器配置
##
webhook:
enabled: false
# Webhook server port
port: 9443
## 用于抓取 metrics 的 Prometheus ServiceMonitor。
## 需要集群中已安装 prometheus-operator。
##
prometheus:
enabled: false
## 自定义 ServiceMonitor 标签
##
# labels: {}
## 自定义 ServiceMonitor 注解
##
# annotations: {}
## 用于控制流量的网络策略。
## 启用以限制进入控制器 Manager 的入站流量。
##
networkPolicy:
enabled: false
安装
该插件会向你的 Makefile 添加 Helm 目标:
make helm-deploy IMG=<registry>/<project:tag>
make helm-status
手动安装并启用全部功能:
$ helm install my-release ./dist/chart --namespace my-project-system --create-namespace
对于没有转换 Webhook 的 Chart,可通过将以下所有 values 设为 false 来禁用 Manager 及所有可选组件:
$ helm install my-release ./dist/chart \
--set manager.enabled=false \
--set metrics.enabled=false \
--set webhook.enabled=false \
--set prometheus.enabled=false \
--set certManager.enabled=false \
--set networkPolicy.enabled=false
对于没有转换 Webhook 的 Chart,可在不启用 Webhook 的情况下安装:
$ helm install my-release ./dist/chart --set webhook.enabled=false --set certManager.enabled=false
webhook.enabled=false 会跳过 webhook Service、webhook 配置、webhook NetworkPolicy 以及 webhook-server 容器端口。还会设置 --webhook-port=-1 以禁用 Webhook 服务器。这与 metrics.enabled=false(传入 --metrics-bind-address=0)类似。
Requires controller-runtime v0.25.0 or later
较旧的 controller-runtime 发行版会将 --webhook-port=-1 视为默认值 9443。随后 Manager 会在没有证书的情况下尝试启动 Webhook 服务器并失败。参见禁用 Webhook 服务器。
带有 CR 版本转换的 Chart 会拒绝 webhook.enabled=false。API 服务器需要 Webhook Service 来转换自定义资源。
安装并包含 NetworkPolicy 资源:
$ helm install my-release ./dist/chart --set networkPolicy.enabled=true
额外卷
向 Manager 部署添加 Webhook 与 metrics 证书之外的卷和卷挂载。
kustomize 配置(config/manager/manager.yaml 或补丁)中的卷会写入 Chart 模板。当 Manager 有额外卷时,values.yaml 会包含 manager.extraVolumes 和 manager.extraVolumeMounts 字段。可用它们在安装时添加更多卷。
Webhook 与 metrics 证书(webhook-certs、metrics-certs)单独管理,由 certManager.enabled 以及(针对 metrics TLS)metrics.enabled + metrics.secure 控制。
指标配置
metrics.port
设置 metrics.port 可更改 metrics 端点使用的端口。Chart 会将该值同时应用于 Manager 的 --metrics-bind-address 参数、metrics Service 的 port 与 targetPort,以及 metrics NetworkPolicy。
例如,将 Chart 安装为在端口 8444 上提供 metrics 端点:
helm install my-operator ./dist/chart --set metrics.port=8444
默认值为 8443,从项目配置中检测得出。
metrics.secure
控制 metrics 端点的传输安全与身份认证(默认:true)。
当为 true 时:
- 使用带 TLS 证书的 HTTPS(当
certManager.enabled=true时) - 创建用于身份认证的
metrics-auth-roleClusterRole - ServiceMonitor 使用 HTTPS
当为 false 时:
- 使用无认证的 HTTP
- 无 TLS 证书
- ServiceMonitor 使用 HTTP
Metrics roles are always cluster-scoped
即使 rbac.namespaced=true,metrics-auth-role 和 metrics-reader 也始终是 ClusterRole。Metrics 身份认证使用集群作用域 API 来认证像 Prometheus 这样的抓取器。
Webhook 端口配置
设置 webhook.port 可更改 Manager 参数、容器端口、Service target port 和 NetworkPolicy。Service 对外暴露的端口保持不变(默认清单中为 443)。
例如,将 Chart 安装为在端口 9444 上运行 Webhook 服务器:
helm install my-operator ./dist/chart --set webhook.port=9444
默认值为 9443,从项目配置中检测得出。要禁用 Webhook 服务器,请设置 webhook.enabled=false,而不是更改端口。
健康探针端口配置
设置 manager.healthProbe.port 可更改 Manager 提供健康探针的端口。存活(/healthz)与就绪(/readyz)端点绑定到该端口。Chart 会将该值同时应用于 --health-probe-bind-address 参数、health 容器端口,以及两个探针的 httpGet 端口。
例如,将 Chart 安装为在端口 8082 上提供健康探针:
helm install my-operator ./dist/chart --set manager.healthProbe.port=8082
默认值为 8081,从项目配置中检测得出。
为 manager 传递参数
在 values.yaml 中使用 manager.args,向 Chart 未作为独立 values 暴露的 Manager 容器传递额外标志。Chart 通过 Helm 的 tpl 函数渲染每个条目,并对照 Chart 的根上下文求值,因此参数可以引用其他 values、release 信息或 Chart 模板函数,而不仅是静态字符串。
例如,从 release 命名空间设置 leader election 命名空间,并添加一个普通标志:
manager:
args:
- --leader-election-namespace={{ .Release.Namespace }}
- --leader-elect
Helm 在渲染时求值 {{ .Release.Namespace }},因此 Manager 容器会收到 --leader-election-namespace=<release-namespace>。--leader-elect 标志没有模板语法,因此会原样渲染。
Do not set the metrics, webhook, or health probe flags in manager.args
Chart 已将 --metrics-bind-address、--webhook-port 和 --health-probe-bind-address 分别暴露为 metrics.port、webhook.port 和 manager.healthProbe.port。请设置这些 values,而不是把标志加入 manager.args。插件在生成 Chart 时会从提取的参数中移除这些标志,因此无论是否模板化,通过 manager.args 再加回去都会与由 values 驱动的标志重复。Service、NetworkPolicy 和探针仍使用已配置的端口,导致流量与探针指向错误端口。
NetworkPolicy 配置
设置 networkPolicy.enabled: true 可为 Manager Pod 安装 NetworkPolicy 资源。
当 kustomize 输出包含 NetworkPolicy 资源时,插件会转换并启用它们。否则,它会生成默认的 metrics 策略,并在检测到 admission 或 CRD 转换 Webhook 时生成默认的 webhook 策略。
除非传入 --force,否则插件会保留默认 NetworkPolicy 模板。来自 kustomize 输出的策略总会重新生成,因此请在 config/network-policy/ 中编辑它们。
仅当 networkPolicy.enabled 与 metrics.enabled 均为 true 时,才会渲染生成的 metrics 策略。仅当 networkPolicy.enabled 与 webhook.enabled 均为 true 时,才会渲染生成的 webhook 策略。
metrics 策略仅允许来自带有 metrics: enabled 标签的命名空间中的 Pod 的入站流量。请为应抓取 metrics 的每个命名空间打标签:
$ kubectl label namespace <namespace> metrics=enabled
默认 webhook 策略允许所有来源访问 Manager Pod 的 webhook 端口。API 服务器通过 webhook Service 到达该端口,用于 admission 与 CRD 转换 Webhook。若要按命名空间限制 admission 请求,请在 MutatingWebhookConfiguration 或 ValidatingWebhookConfiguration 上设置 namespaceSelector。该设置控制哪些请求会调用 admission Webhook;它不会限制 NetworkPolicy 流量。
自定义标签与注解
使用 manager.labels、manager.annotations、manager.pod.labels 和 manager.pod.annotations 添加自定义标签与注解。来自 kustomize 的重复键会自动过滤。
ServiceAccount 与 ServiceMonitor 资源通过 serviceAccount.labels、serviceAccount.annotations、prometheus.labels 和 prometheus.annotations 支持相同模式。
ServiceAccount 配置
设置 serviceAccount.enabled: true(默认)以创建 ServiceAccount。设置 serviceAccount.enabled: false 以使用已有账户:
serviceAccount:
enabled: false
name: my-existing-sa
Chart 默认启用该开关:
serviceAccount:
enabled: true
Helm 会将该默认值合并到省略该节的任何 values 文件中,因此省略它仍会创建账户。
当 serviceAccount.enabled: false 时,serviceAccount.name 为必填,未设置时 Chart 渲染会失败。显式设置 name: default 以使用命名空间默认 ServiceAccount。
解析后的名称会一致地用于 Deployment 以及所有 RBAC 绑定,适用于集群作用域与命名空间作用域的 RBAC 模式:
serviceAccount.enabled | serviceAccount.name | 已创建 ServiceAccount | 使用的名称(Deployment + RBAC 绑定) |
|---|---|---|---|
true (default) | any | Yes | 生成的(<fullname>-controller-manager,name 被忽略) |
false | set | No | 所提供的名称 |
false | default | No | 命名空间默认 ServiceAccount |
false | unset | No | 渲染失败并给出明确错误 |
生成的名称由 Chart fullname 构建,通常为 <release>-<chart>,再加上 -controller-manager 后缀,并截断到 Kubernetes 的 63 字符限制。它会尊重 nameOverride 与 fullnameOverride。
Why an explicit name is required
若静默回退到命名空间默认 ServiceAccount,会将 operator 的 RBAC 权限绑定到共享身份。要求设置 serviceAccount.name 可使该选择显式化,并在渲染时捕获错误配置。
为云提供商集成添加注解:
serviceAccount:
enabled: true
annotations:
iam.gke.io/gcp-service-account: my-operator@project.iam.gserviceaccount.com
外部 ServiceAccount 名称按原样使用,并忽略 nameOverride 或 fullnameOverride。
RBAC 配置
rbac.namespaced
设置 RBAC 权限作用域:
false(默认):针对所有命名空间的 ClusterRole 与 ClusterRoleBindingtrue:仅针对 release 命名空间的 Role 与 RoleBinding
What rbac.namespaced controls
这仅控制 RBAC 权限。要控制监视作用域,请使用 WATCH_NAMESPACE(Manager 作用域)。metrics-auth-role 始终是 ClusterRole,leader-election-role 始终是 Role。
rbac.roleNamespaces
当你的 kustomize 输出包含针对特定命名空间(非 Manager 命名空间)的 Role 与 RoleBinding 时,插件会自动检测并为它们创建 roleNamespaces 条目。
为控制器添加命名空间特定的 RBAC 标记:
// +kubebuilder:rbac:groups=apps,namespace=infrastructure,resources=deployments,verbs=get;list;watch
// +kubebuilder:rbac:groups="",namespace=users,resources=secrets,verbs=get;list;watch
运行 make manifests 生成 RBAC 清单,然后运行该插件。生成的 values.yaml 包含:
rbac:
namespaced: false
## 部署到与 Manager 命名空间不同的命名空间的 Role 的命名空间配置
## 键为资源名称后缀(不含项目前缀)
##
roleNamespaces:
# RBAC 资源 manager-role-infrastructure 部署到命名空间 infrastructure
"manager-role-infrastructure": "infrastructure"
# RBAC 资源 manager-rolebinding-infrastructure 部署到命名空间 infrastructure
"manager-rolebinding-infrastructure": "infrastructure"
# RBAC 资源 manager-role-users 部署到命名空间 users
"manager-role-users": "users"
# RBAC 资源 manager-rolebinding-users 部署到命名空间 users
"manager-rolebinding-users": "users"
helpers:
enabled: false
使用 values 文件在部署时覆盖命名空间:
# custom-values.yaml
rbac:
roleNamespaces:
"manager-role-infrastructure": "prod-infra"
"manager-rolebinding-infrastructure": "prod-infra"
"manager-role-users": "prod-users"
"manager-rolebinding-users": "prod-users"
使用自定义命名空间安装:
helm install my-operator ./dist/chart -f custom-values.yaml
或使用 --set:
helm install my-operator ./dist/chart \
--set 'rbac.roleNamespaces[manager-role-infrastructure]=prod-infra' \
--set 'rbac.roleNamespaces[manager-role-users]=prod-users'
Helper roles and optional values
设置 rbac.helpers.enabled: true 可为自定义资源创建 admin、editor 和 viewer 角色。
values.yaml 中的可选字段使用 Helm 条件。注释掉它们即可从已部署清单中排除。
旗帜
| Flag | 描述 |
|---|---|
| --manifests | 包含 Kubernetes 清单的 YAML 文件路径(默认:dist/install.yaml) |
| --output-dir string | Chart 的输出目录(默认:dist) |
| --force | 重新生成除 Chart.yaml 外被保留的文件(values.yaml、NOTES.txt、_helpers.tpl、.helmignore、test-chart.yml、network-policy/allow-metrics-traffic.yaml、network-policy/allow-webhook-traffic.yaml) |
Examples
你可以在 testdata/project-v4-with-plugins 中找到示例项目。
Kustomize v2
(默认脚手架)
Kustomize 插件允许您为使用语言基础插件 base.go.kubebuilder.io/v4 的所有 kustomize 清单搭建框架。此插件用于在 go/v4 插件(默认脚手架)构建的项目中生成 config/ 目录下的清单。
像 Operator-sdk 这样的项目使用 Kubebuilder 项目作为库,并提供了与其他语言(如 Ansible 和 Helm)协作的选项。Kustomize 插件帮助它们在不同语言之间保持一致的配置。它还简化了在默认脚手架基础上执行更改的插件的创建,消除了在多个语言插件之间进行手动更新的需要。这种方法允许创建与不同项目和语言配合使用的“辅助“插件。
Examples
您可以通过查看 Kubebuilder 项目的 testdata 目录下的示例 project-v4-* 中提供的 config/ 目录来检查 kustomize 内容。
如何使用它
如果您希望您的语言插件使用 kustomize,请使用 Bundle Plugin 来指定您的语言插件由特定于语言的插件和 kustomize 组成,以用于其配置,如下所示:
import (
...
kustomizecommonv2 "sigs.k8s.io/kubebuilder/v4/pkg/plugins/common/kustomize/v2"
golangv4 "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4"
...
)
// 为由 Kubebuilder go/v4 创建的 Golang 项目提供的捆绑插件
gov4Bundle, _ := plugin.NewBundle(plugin.WithName(golang.DefaultNameQualifier),
plugin.WithVersion(plugin.Version{Number: 4}),
plugin.WithPlugins(kustomizecommonv2.Plugin{}, golangv4.Plugin{}), // 搭建 config/ 目录和所有 kustomize 文件
)
您还可以单独使用 kustomize/v2,通过:
kubebuilder init --plugins=kustomize/v2
$ ls -la
total 24
drwxr-xr-x 6 camilamacedo86 staff 192 31 Mar 09:56 .
drwxr-xr-x 11 camilamacedo86 staff 352 29 Mar 21:23 ..
-rw------- 1 camilamacedo86 staff 129 26 Mar 12:01 .dockerignore
-rw------- 1 camilamacedo86 staff 367 26 Mar 12:01 .gitignore
-rw------- 1 camilamacedo86 staff 94 31 Mar 09:56 PROJECT
drwx------ 6 camilamacedo86 staff 192 31 Mar 09:56 config
或与基础语言插件结合使用:
# 提供与 go/v4 插件相同的结构,但使用 kustomize/v2 进行组合。
kubebuilder init --plugins=kustomize/v2,base.go.kubebuilder.io/v4 --domain example.org --repo example.org/guestbook-operator
子命令
kustomize 插件实现了以下子命令:
- 初始化 (
$ kubebuilder init [选项]) - 创建 API (
$ kubebuilder create api [OPTIONS]) - 创建 webhook (
$ kubebuilder create api [选项])
Create API and Webhook
create api 子命令的实现为每个 API 生成特定的 kustomize 清单。有关更多信息,请参见 这里。create webhook 的情况也是如此。
受影响的文件
以下架构将由此插件创建或更新:
config/*
进一步的资源
- 检查 kustomize 的 插件实现
- 查看kustomize 文档
- 请查看kustomize 仓库。
扩展 Kubebuilder
Kubebuilder 提供了一种可扩展的架构,用于使用插件搭建项目。这些插件允许您自定义 CLI 行为或集成新功能。
概述
Kubebuilder 的命令行界面(CLI)可以通过自定义插件进行扩展,使您能够:
- 建立新的脚手架。
- 增强现有的。
- 为 Kubebuilder 的脚手架添加新命令和功能。
这种灵活性使您能够创建根据特定需求量身定制的项目设置。
Why use the Kubebuilder style?
Kubebuilder 和 SDK 都是广泛采用的项目,它们利用了 controller-runtime 项目。它们都允许用户使用 操作员模式 构建解决方案,并遵循共同标准。
采纳这些标准可以带来显著的 benefits,例如在维护共同标准方面联合力量,利用 Kubebuilder 提供的功能,以及社区所做的贡献。这使您能够专注于您的插件和用例的特定需求和要求。
然后,您还将能够使用当前或将来这些项目可能提供的自定义插件和选项,以及其他决定遵循相同标准的项目。
扩展选项
扩展 Kubebuilder 可以通过两种主要方式实现:
-
扩展 CLI 功能和插件: 您可以导入并在现有的 Kubebuilder 插件上进行构建,以 扩展其功能和插件。当您需要为已从 Kubebuilder 的脚手架系统受益的工具添加特定功能时,这非常有用。例如,Operator SDK 利用 kustomize 插件 来为 Ansible 或 Helm 等工具提供语言支持。这样,项目可以专注于维护仅与特定语言相关的内容。
-
创建外部插件:您可以构建独立的插件作为二进制文件。这些插件可以用任何语言编写,并应遵循 Kubebuilder 识别的执行模式。有关更多信息,请参见 创建外部插件。
有关如何扩展 Kubebuilder 的更多详情,请参考以下章节:
扩展命令行界面功能和插件
Kubebuilder 提供了一种可扩展的架构,用于使用插件搭建项目。这些插件允许您自定义 CLI 行为或集成新功能。
在本指南中,我们将探讨如何扩展 CLI 功能、创建自定义插件以及打包多个插件。
创建自定义插件
要创建自定义插件,您需要实现 Kubebuilder 插件接口。
此接口允许您的插件挂钩到 Kubebuilder 的命令(init、create api、create webhook 等),并添加自定义逻辑。
自定义插件示例
您可以创建一个插件,它生成特定语言的框架和所需的配置文件,使用 捆绑插件。此示例演示如何将 Go 语言插件与 Kustomize 插件结合使用:
import (
kustomizecommonv2 "sigs.k8s.io/kubebuilder/v4/pkg/plugins/common/kustomize/v2"
golangv4 "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4"
)
mylanguagev1Bundle, _ := plugin.NewBundle(
plugin.WithName("mylanguage.kubebuilder.io"),
plugin.WithVersion(plugin.Version{Number: 1}),
plugin.WithPlugins(kustomizecommonv2.Plugin{}, mylanguagev1.Plugin{}),
)
这个结构使您能够通过 Kustomize 建立一个通用的配置基础,并通过 mylanguagev1 处理特定于语言的文件。
您还可以使用您的插件来构建特定的资源,例如 CRD 和控制器,使用 create api 和 create webhook 子命令。
插件子命令
插件负责实现当子命令被调用时将执行的代码。您可以通过实现 插件接口 来创建一个新的插件。
除了作为 Base 之外,插件还应实现 SubcommandMetadata 接口,以便可以通过 CLI 运行。可选地,可以为目标命令设置自定义帮助文本;此方法可以是无操作的,这将保留由 cobra 命令构造函数设置的默认帮助文本。
Kubebuilder CLI 插件将脚手架和 CLI 功能包装在方便的 Go 类型中,这些类型由 kubebuilder 二进制文件或任何导入它们的二进制文件执行。更具体地说,一个插件配置以下 CLI 命令之一的执行:
init:初始化项目结构。create api:生成一个新的 API 和控制器。创建 webhook: 建立一个新的 webhook。编辑: 编辑项目结构。
以下是使用 init 子命令与自定义插件的示例:
kubebuilder init --plugins=mylanguage.kubebuilder.io/v1
这将使用 mylanguage 插件初始化一个项目。
插件密钥
插件通过 <name>/<version> 形式的键来识别。指定要运行的插件的方式有两种:
-
设置
kubebuilder init --plugins=<plugin key>,将初始化一个为键为<plugin key>的插件配置的项目。 -
在生成的 项目配置文件 中的
layout: <插件键>。在运行命令时(除了初始化命令,它会生成此文件),命令会查看该值以选择要运行的插件。
默认情况下,<plugin key> 将为 go.kubebuilder.io/vX,其中 X 是某个整数。
要查看完整的实现示例,请查看 Kubebuilder 的原生 go.kubebuilder.io 插件。
插件命名
插件名称必须是 DNS1123 标签,并且应为完全合格的名称,即它们有一个后缀,如 .example.com。例如,使用 kubebuilder 命令的基础 Go 脚手架名称为 go.kubebuilder.io。合格的名称可以避免插件名称之间的冲突;go.kubebuilder.io 和 go.example.com 都可以生成 Go 代码,并且可以由用户指定。
插件版本管理
插件的 Version() 方法返回一个 plugin.Version 对象,包含一个整数值和可选的阶段字符串,它可以是“alpha“或“beta“。该整数表示插件的当前版本。两个不同的整数值表示插件版本之间的不兼容性。阶段字符串表示插件的稳定性:
alpha:应该用于那些经常更改并可能在使用之间出现故障的插件。beta:应适用于仅在小幅度上进行更改的插件,例如错误修复。
标准文本
Kubebuilder 内部插件使用模板来生成代码文件。Kubebuilder 通过模板化来搭建插件的文件。例如,在创建新项目时,go/v4 插件会使用其实现中定义的模板来生成 go.mod 文件。
您可以通过定义自己的模板并使用 Kubebuilder 的机械库 来生成文件,从而扩展您自定义插件的功能。该库允许您:
- 定义文件 I/O 行为。
- 将标记添加到搭建的文件中。
- 为您的脚手架指定模板。
示例:样板文本
例如,go/v4 通过定义一个实现了 machinery 接口 的对象来搭建 go.mod 文件。原始模板被设置为 Template.SetTemplateDefaults 方法中的 TemplateBody 字段:
/*******************************************************************************
版权所有 2022 Kubernetes 作者。
根据 Apache 许可证,版本 2.0("许可证")授权;
除非遵守许可证,否则您不得使用此文件。
您可以在以下网址获取许可证的副本:
http://www.apache.org/licenses/LICENSE-2.0
除非适用法律要求或书面达成协议,否则根据许可证分发的软件是以"原样"基础分发的,
不提供任何种类的保证或条件,明示或暗示。
有关许可证的具体语言,请参见许可证以了解管理权限和
根据许可证的限制。
******************************************************************************/
package templates
import (
"sigs.k8s.io/kubebuilder/v4/pkg/machinery"
)
var _ machinery.Template = &GoMod{}
// GoMod生成一个定义项目依赖的文件
type GoMod struct {
machinery.TemplateMixin
machinery.RepositoryMixin
ControllerRuntimeVersion string
}
// SetTemplateDefaults实现了machinery.Template
func (f *GoMod) SetTemplateDefaults() error {
if f.Path == "" {
f.Path = "go.mod"
}
f.TemplateBody = goModTemplate
f.IfExistsAction = machinery.OverwriteFile
return nil
}
const goModTemplate = `module {{ .Repo }}
go 1.26.0
require (
sigs.k8s.io/controller-runtime {{ .ControllerRuntimeVersion }}
)
`
实现该机械接口的对象随后将传递给脚手架的执行:
// Scaffold 实现了 cmdutil.Scaffolder
func (s *initScaffolder) Scaffold() error {
log.Println("Writing scaffold for you to edit...")
// 初始化将把样板文件写入磁盘的机器架构。
// 样板文件需要作为一个单独步骤进行搭建,因为它将被
// 其他文件使用,包括在此命令调用中搭建的文件。
scaffold := machinery.NewScaffold(s.fs,
machinery.WithConfig(s.config),
)
...
return scaffold.Execute(
...
&templates.GoMod{
ControllerRuntimeVersion: ControllerRuntimeVersion,
},
...
)
}
示例:在插件中覆盖文件
假设在调用某个子命令时,你想覆盖已有文件。
例如,要修改 Makefile 并添加自定义构建步骤,在模板的定义中可以使用以下选项:
f.IfExistsAction = machinery.OverwriteFile
通过使用这些选项,您的插件可以控制由 Kubebuilder 的默认脚手架生成的某些文件。
定制现有支架
Kubebuilder 提供了实用函数,帮助您修改默认的脚手架。通过使用 插件工具,您可以向 Kubebuilder 生成的文件插入、替换或附加内容,从而完全控制脚手架的过程。
这些工具使您能够:
- 插入内容:在文件的特定位置添加内容。
- 替换内容:搜索并替换文件的特定部分。
- 追加内容:在文件末尾添加内容,而不删除或更改现有内容。
Example
如果您需要在生成的文件中插入自定义内容,可以使用插件工具提供的 InsertCode 函数:
pluginutil.InsertCode(filename, target, code)
这种方法使您能够在构建自定义插件时扩展和修改生成的脚手架。
有关更多细节,请参考 Kubebuilder 插件工具。
捆绑插件
插件可以被打包以组合成更复杂的脚手架。插件包是多个插件的组合,这些插件按照预定义的顺序执行。例如:
myPluginBundle, _ := plugin.NewBundle(
plugin.WithName("myplugin.example.com"),
plugin.WithVersion(plugin.Version{Number: 1}),
plugin.WithPlugins(pluginA.Plugin{}, pluginB.Plugin{}, pluginC.Plugin{}),
)
该捆绑包将按照指定的顺序为每个插件执行 init 子命令:
pluginApluginB- 插件C
以下命令将运行捆绑的插件:
kubebuilder init --plugins=myplugin.example.com/v1
命令行系统
插件是通过 CLI 对象运行的,该对象将插件类型映射到子命令,并调用该插件的方法。例如,编写一个将 Init 插件注入到 CLI 的程序,然后调用 CLI.Run() 将调用插件的 SubcommandMetadata、UpdatesMetadata 和 Run 方法,使用用户在 kubebuilder init 中传递给程序的信息。以下是一个示例:
package cli
import (
log "log/slog"
"github.com/spf13/cobra"
"sigs.k8s.io/kubebuilder/v4/pkg/cli"
cfgv3 "sigs.k8s.io/kubebuilder/v4/pkg/config/v3"
"sigs.k8s.io/kubebuilder/v4/pkg/plugin"
kustomizecommonv2 "sigs.k8s.io/kubebuilder/v4/pkg/plugins/common/kustomize/v2"
"sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang"
deployimagev1alpha1 "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/deploy-image/v1alpha1"
golangv4 "sigs.k8s.io/kubebuilder/v4/pkg/plugins/golang/v4"
)
var (
// 以下是您自己的二进制文件中可能包含的命令示例
//
commands = []*cobra.Command{
myExampleCommand.NewCmd(),
}
alphaCommands = []*cobra.Command{
myExampleAlphaCommand.NewCmd(),
}
)
// GetPluginsCLI 返回配置在您的 CLI 二进制文件中使用的基于插件的 CLI。
func GetPluginsCLI() (*cli.CLI) {
// 打包插件,用于构建由 Kubebuilder go/v4 创建的 Golang 项目脚手架。
gov3Bundle, _ := plugin.NewBundleWithOptions(plugin.WithName(golang.DefaultNameQualifier),
plugin.WithVersion(plugin.Version{Number: 3}),
plugin.WithPlugins(kustomizecommonv2.Plugin{}, golangv4.Plugin{}),
)
c, err := cli.New(
// 添加你的 CLI 二进制文件的名称
cli.WithCommandName("example-cli"),
// 添加你的 CLI 二进制文件的版本。
cli.WithVersion(versionString()),
// 注册可以通过你的 CLI 工具使用的插件选项,以进行脚手架。请注意,我们在这里使用的示例是由 Kubebuilder 实现和提供的插件。
cli.WithPlugins(
gov3Bundle,
&deployimagev1alpha1.Plugin{},
),
// 定义您的二进制文件将使用的默认插件。这意味着如果没有提供信息,例如当用户运行 `kubebuilder init` 时,将使用这个插件。
cli.WithDefaultPlugins(cfgv3.Version, gov3Bundle),
// 定义默认的项目配置版本,CLI在未通过--project-version标志提供时将使用该版本。
cli.WithDefaultProjectVersion(cfgv3.Version),
// 将您自己的命令添加到命令行接口 (CLI)
cli.WithExtraCommands(commands...),
// 将您自己的 alpha 命令添加到 CLI 中
cli.WithExtraAlphaCommands(alphaCommands...),
// 为你的命令行界面添加补全选项
cli.WithCompletion(),
)
if err != nil {
log.Fatal(err)
}
return c
}
// versionString 返回 CLI 版本
func versionString() string {
// 返回您的二进制项目版本
}
该程序可以通过以下方式构建和运行:
默认行为:
# 使用默认 Init 插件 "go.example.com/v1" 初始化项目。
# This key is automatically written to a PROJECT config file.
$ my-bin-builder init
# 使用 "go.example.com/v1" 的 CreateAPI 和
# CreateWebhook plugin methods. This key was read from the config file.
$ my-bin-builder create api [flags]
$ my-bin-builder create webhook [flags]
使用 --plugins 选择一个插件:
# 使用 "ansible.example.com/v1" Init 插件初始化项目。
# Like above, this key is written to a config file.
$ my-bin-builder init --plugins ansible
# 使用 "ansible.example.com/v1" 的 CreateAPI 创建 API 和 webhook
# and CreateWebhook plugin methods. This key was read from the config file.
$ my-bin-builder create api [flags]
$ my-bin-builder create webhook [flags]
输入应在项目文件中跟踪。
CLI 负责管理 项目文件配置,该配置表示由 CLI 工具搭建的项目的配置。
在扩展 Kubebuilder 时,建议确保您的工具或 外部插件 正确使用 PROJECT 文件 来跟踪相关信息。这确保了其他外部工具和插件可以与项目正确集成。它还允许工具特性帮助用户重新构建他们的项目,例如 Kubebuilder 提供的 项目升级助手,确保 PROJECT 文件中跟踪的信息可以用于多种目的。
例如,插件可以检查它们是否支持项目设置,并根据跟踪的输入重新执行命令。
Example
通过运行以下命令使用 Deploy Image 插件来搭建 API 及其控制器:
kubebyilder create api --group example.com --version v1alpha1 --kind Memcached --image=memcached:memcached:1.6.26-alpine3.19 --image-container-command="memcached,--memory-limit=64,-o,modern,-v" --image-container-port="11211" --run-as-user="1001" --plugins="deploy-image/v1-alpha" --make=false
以下条目将被添加到项目文件中:
...
plugins:
部署图像.go.kubebuilder.io/v1-alpha:
resources:
- domain: testproject.org
group: example.com
kind: Memcached
选项:
容器命令: memcached,--memory-limit=64,-o,modern,-v
容器端口: "11211"
image: memcached:memcached:1.6.26-alpine3.19
以用户身份运行: "1001"
version: v1alpha1
- domain: testproject.org
group: example.com
kind: Busybox
选项:
image: busybox:1.36.1
version: v1alpha1
...
通过检查 PROJECT 文件,可以了解插件是如何使用的以及提供了哪些输入。这不仅可以基于跟踪的数据重新执行命令,还能够创建可以依赖这些信息的功能或插件。
为 Kubebuilder 创建外部插件
概述
Kubebuilder 的功能可以通过外部插件进行扩展。这些插件是可执行文件(可以用任何语言编写),遵循 Kubebuilder 认可的执行模式。Kubebuilder 通过 stdin 和 stdout 与这些插件进行交互,实现无缝通信。
为什么使用外部插件?
外部插件使第三方解决方案维护者能够将其工具与 Kubebuilder 集成。与 Kubebuilder 自身的插件类似,这些插件可以选择性使用,为用户提供工具选择的灵活性。通过在自己的代码库中开发插件,维护者确保更新与其 CI 流水线保持一致,并能够管理其责任范围内的任何变更。
如果您对这种类型的集成感兴趣,建议与第三方解决方案的维护者进行合作。Kubebuilder 的维护者始终乐意提供支持,以扩展其功能。
如何编写外部插件
Kubebuilder 与外部插件之间的通信通过标准输入/输出进行。可以使用任何语言创建插件,只要它遵循 PluginRequest 和 PluginResponse 结构。
PluginRequest包含从CLI收集的数据以及之前执行的插件。Kubebuilder通过stdin将这些数据作为JSON对象发送给外部插件。
Fields:
apiVersion:PluginRequest schema 的版本。args:传递给插件的命令行参数。command:正在执行的子命令(例如init、create api、create webhook、edit)。universe:文件路径到内容的映射,在插件链中更新。pluginChain(可选):按执行顺序列出的插件键数组。外部插件可检查此项,以便根据已运行的其他插件(例如go.kubebuilder.io/v4或kustomize.common.kubebuilder.io/v2)调整行为。config(可选):当前项目的序列化 PROJECT 文件配置。用于检查元数据、现有资源或插件特定设置。在 PROJECT 文件存在之前——通常是第一次init——Kubebuilder 会省略此字段,因此插件应检查其是否存在。
注意: 只要 Kubebuilder 可访问 PROJECT 文件(例如在 create api、create webhook、edit 或后续 init 运行期间),PluginRequest 就会包含 config 字段。在非常第一次 init 运行时该字段会被省略,因为 PROJECT 文件尚不存在。
示例 PluginRequest(通过 kubebuilder init --plugins sampleexternalplugin/v1 --domain my.domain 触发):
{
"apiVersion": "v1alpha1",
"args": ["--domain", "my.domain"],
"command": "init",
"universe": {},
"pluginChain": ["go.kubebuilder.io/v4", "kustomize.common.kubebuilder.io/v2", "sampleexternalplugin/v1"]
}
create api 的 PluginRequest 示例(包含 config):
{
"apiVersion": "v1alpha1",
"args": ["--group", "crew", "--version", "v1", "--kind", "Captain"],
"command": "create api",
"universe": {},
"pluginChain": ["go.kubebuilder.io/v4", "kustomize.common.kubebuilder.io/v2", "sampleexternalplugin/v1"],
"config": {
"domain": "my.domain",
"repo": "github.com/example/my-project",
"projectName": "my-project",
"version": "3",
"layout": ["go.kubebuilder.io/v4"],
"multigroup": false,
"resources": []
}
}
示例 PluginRequest(通过 kubebuilder init --plugins sampleexternalplugin/v1 --domain my.domain 触发):
{
"apiVersion": "v1alpha1",
"args": [],
"command": "edit",
"universe": {}
}
插件响应
PluginResponse 包含插件对项目所做的修改。该数据以 JSON 格式序列化,并通过 stdout 返回给 Kubebuilder。
示例 PluginResponse:
{
"apiVersion": "v1alpha1",
"command": "edit",
"metadata": {
"description": "`edit` 子命令会添加用于监控 operator 的 Prometheus 实例配置。",
"examples": "kubebuilder edit --plugins sampleexternalplugin/v1"
},
"universe": {
"config/prometheus/prometheus.yaml": "# Prometheus CR manifest...",
"config/prometheus/kustomization.yaml": "resources:\n - prometheus.yaml\n",
"config/default/kustomization_prometheus_patch.yaml": "# Instructions for enabling Prometheus..."
},
"error": false,
"errorMsgs": []
}
在你的外部插件中避免直接输出到 stdout。由于 Kubebuilder 和插件之间的通信是通过 stdin 和 stdout 使用结构化 JSON 进行的,任何意外的输出(例如调试日志)都可能导致错误。如果需要,请将日志写入文件。
如何使用外部插件
前提条件
- Kubebuilder CLI 版本 > 3.11.0
- 外部插件的可执行文件
- 使用
${EXTERNAL_PLUGINS_PATH}或默认操作系统路径进行插件路径配置:- Linux:
$HOME/.config/kubebuilder/plugins/${name}/${version}/${name} - macOS:
~/Library/Application Support/kubebuilder/plugins/${name}/${version}/${name}
- Linux:
示例: 对于在 Linux 上的插件 foo.acme.io 版本 v2,路径将是 $HOME/.config/kubebuilder/plugins/foo.acme.io/v2/foo.acme.io。
可用子命令
外部插件可以支持以下 Kubebuilder 子命令:
init: 项目初始化create api: 创建 Kubernetes API 定义的脚手架创建 webhook: 创建 Kubernetes webhooks 的骨架编辑: 更新项目配置
可选子命令以增强用户体验:
metadata: 提供使用--help标志的插件描述和示例。flags:通知Kubebuilder支持的标志,以便提前检测错误。
More about `flags` subcommand
外部插件中的 flags 子命令允许通过告知 Kubebuilder 插件支持的标志来提前进行错误检测。如果识别到不支持的标志,Kubebuilder 可以在调用插件执行之前发出错误。如果插件没有实现 flags 子命令,Kubebuilder 将把所有标志传递给插件,因此处理任何无效标志的责任将由外部插件承担。
配置插件路径
设置环境变量 $EXTERNAL_PLUGINS_PATH 以指定自定义插件二进制文件路径:
export EXTERNAL_PLUGINS_PATH=您训练的数据截至2023年10月。
否则,Kubebuilder 将根据您的操作系统在默认路径中搜索插件。
示例命令行指令
您现在可以通过调用 CLI 命令来使用它:
# 为现有项目添加 Prometheus 监控
kubebuilder edit --plugins sampleexternalplugin/v1
# 使用 Prometheus 监控更新现有项目
kubebuilder edit --plugins sampleexternalplugin/v1
# 显示 init 子命令的帮助信息
kubebuilder init --plugins sampleexternalplugin/v1 --help
# 显示 edit 子命令的帮助信息
kubebuilder edit --plugins sampleexternalplugin/v1 --help
# 插件链式示例:先使用 go/v4 插件,再应用外部插件
kubebuilder edit --plugins go/v4,sampleexternalplugin/v1
进一步的资源
创建自定义标记
概述
将 Kubebuilder 用作库时,你可能需要脚手架生成原生标记系统不支持的扩展名文件。本指南说明如何为任意文件扩展名创建自定义标记支持。
何时使用自定义标记
自定义标记在以下情况有用:
- 你正在为 Kubebuilder 原生不支持的语言构建外部插件
- 你想脚手架生成自定义扩展名的文件(
.rs、.java、.py、.tpl等) - 你需要在非 Go 文件中使用脚手架标记以满足自身用例
- 你的文件扩展名不属于(也不应属于)核心
commentsByExt映射
理解标记
标记是 Kubebuilder 用于脚手架目的的特殊注释。它们指示可在何处插入或修改代码。核心 Kubebuilder 标记系统默认仅支持 .go、.yaml 和 .yml 文件。
Go 文件中的标记示例:
// +kubebuilder:scaffold:imports
实现示例
以下说明如何为 Rust 文件(.rs)实现自定义标记。同一模式可应用于任意文件扩展名。
定义你的标记类型
// pkg/markers/rust.go
package markers
import (
"fmt"
"path/filepath"
"strings"
)
const RustPluginPrefix = "+rust:scaffold:"
type RustMarker struct {
prefix string
comment string
value string
}
func NewRustMarker(path string, value string) (RustMarker, error) {
ext := filepath.Ext(path)
if ext != ".rs" {
return RustMarker{}, fmt.Errorf("expected .rs file, got %s", ext)
}
return RustMarker{
prefix: formatPrefix(RustPluginPrefix),
comment: "//",
value: value,
}, nil
}
func (m RustMarker) String() string {
return m.comment + " " + m.prefix + m.value
}
func formatPrefix(prefix string) string {
trimmed := strings.TrimSpace(prefix)
var builder strings.Builder
if !strings.HasPrefix(trimmed, "+") {
builder.WriteString("+")
}
builder.WriteString(trimmed)
if !strings.HasSuffix(trimmed, ":") {
builder.WriteString(":")
}
return builder.String()
}
Implementation Reference
上面展示的 formatPrefix 实现改编自 Kubebuilder 内部的 markerPrefix 函数。
在模板生成中使用
package templates
import (
"fmt"
"github.com/yourorg/yourplugin/pkg/markers"
)
func GenerateRustFile(projectName string) (string, error) {
marker, err := markers.NewRustMarker("src/main.rs", "imports")
if err != nil {
return "", err
}
content := fmt.Sprintf(`// Generated by Rust Plugin
%s
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
println!("Hello from %s!");
Ok(())
}
`, marker.String(), projectName)
return content, nil
}
func GenerateCargoToml(projectName string) string {
return fmt.Sprintf(`[package]
name = "%s"
version = "0.1.0"
edition = "2021"
[dependencies]
`, projectName)
}
与外部插件集成
package main
import (
"bufio"
"encoding/json"
"fmt"
"io"
"os"
"sigs.k8s.io/kubebuilder/v4/pkg/plugin/external"
"github.com/yourorg/yourplugin/pkg/markers"
)
func main() {
// 外部插件通过 STDIN/STDOUT 上的 JSON 通信
reader := bufio.NewReader(os.Stdin)
input, err := io.ReadAll(reader)
if err != nil {
returnError(fmt.Errorf("error reading STDIN: %w", err))
return
}
pluginRequest := &external.PluginRequest{}
err = json.Unmarshal(input, pluginRequest)
if err != nil {
returnError(fmt.Errorf("error unmarshaling request: %w", err))
return
}
var response external.PluginResponse
switch pluginRequest.Command {
case "init":
response = handleInit(pluginRequest)
default:
response = external.PluginResponse{
Command: pluginRequest.Command,
Error: true,
ErrorMsgs: []string{fmt.Sprintf("unknown command: %s", pluginRequest.Command)},
}
}
output, err := json.Marshal(response)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to marshal response: %v\n", err)
os.Exit(1)
}
fmt.Printf("%s", output)
}
func handleInit(req *external.PluginRequest) external.PluginResponse {
// Create Rust file with custom markers
marker, err := markers.NewRustMarker("src/main.rs", "imports")
if err != nil {
return external.PluginResponse{
Command: "init",
Error: true,
ErrorMsgs: []string{fmt.Sprintf("failed to create Rust marker: %v", err)},
}
}
fileContent := fmt.Sprintf(`// Generated by Rust Plugin
%s
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
println!("Hello from Rust!");
Ok(())
}
`, marker.String())
// 外部插件使用 "universe" 表示文件变更。
// "universe" 是从文件路径到其文件内容的映射,
// 通过插件链传递以协调文件生成。
universe := make(map[string]string)
universe["src/main.rs"] = fileContent
return external.PluginResponse{
Command: "init",
Universe: universe,
}
}
func returnError(err error) {
response := external.PluginResponse{
Error: true,
ErrorMsgs: []string{err.Error()},
}
output, marshalErr := json.Marshal(response)
if marshalErr != nil {
fmt.Fprintf(os.Stderr, "failed to marshal error response: %v\n", marshalErr)
os.Exit(1)
}
fmt.Printf("%s", output)
}
适配其他语言
要支持其他文件扩展名,请通过更改以下内容来修改标记实现:
- 注释语法(例如,Java 用
//,Python 用#,模板用{{/* ... */}}) - 文件扩展名检查(例如,
.java、.py、.tpl) - 标记前缀(例如,
+java:scaffold:、+python:scaffold:)
有关创建外部插件的更多信息,请参见外部插件。
编写端到端测试
您可以查看 Kubebuilder/v4/test/e2e/utils 包,该包提供了带有丰富方法的 TestContext:
- NewTestContext 有助于定义:
- 一个用于测试项目的临时文件夹。
- 一个临时的控制器管理器镜像。
- Kubectl 执行方式。
- CLI 可执行文件(无论是
kubebuilder、operator-sdk还是您扩展的 CLI)。
一旦定义完毕,您可以使用 TestContext 来:
-
设置测试环境,例如:
- 清理环境并创建临时目录。请参阅 准备。
- 安装所需的 CRD。请参阅 InstallCertManager 和 InstallPrometheusManager。
-
验证插件的行为,例如:
- 触发插件绑定的子命令。请参见 Init、CreateAPI。
- 使用 PluginUtil 来验证生成的输出。请参见 InsertCode、ReplaceInFile 和 UncommentCode。
-
确保搭建的输出正常运行,例如:
- 在您的
Makefile中执行命令。请参见 Make。 - 临时加载测试控制器的图像。请参见 LoadImageToKindCluster。
- 调用 Kubectl 以验证正在运行的资源。请参阅 Kubectl。
- 在您的
-
测试后清理临时资源:
- 卸载先决条件 CRD。请参见 UninstallPrometheusOperManager。
- 删除临时目录。请参见 销毁。
参考文献:
生成测试样本
查看由您的插件生成的示例项目内容非常简单。
例如,Kubebuilder 根据不同的插件生成 示例项目 以验证布局。
您还可以使用 TestContext 从您的插件生成脚手架项目的文件夹。命令与扩展 CLI 功能和插件中提到的类似。
以下是使用 go/v4 插件创建示例项目的一般工作流程(kbc 是 TestContext 的一个实例):
-
初始化一个项目:
By("initializing a project") err = kbc.Init( "--plugins", "go/v4", "--project-version", "3", "--domain", kbc.Domain, "--fetch-deps=false", ) Expect(err).NotTo(HaveOccurred(), "Failed to initialize a project") -
定义API:
By("creating API definition") err = kbc.CreateAPI( "--group", kbc.Group, "--version", kbc.Version, "--kind", kbc.Kind, "--namespaced", "--resource", "--controller", "--make=false", ) Expect(err).NotTo(HaveOccurred(), "Failed to create an API") -
为了搭建 webhook 配置:
By("scaffolding mutating and validating webhooks") err = kbc.CreateWebhook( "--group", kbc.Group, "--version", kbc.Version, "--kind", kbc.Kind, "--defaulting", "--programmatic-validation", ) Expect(err).NotTo(HaveOccurred(), "Failed to create a webhook")
插件版本管理
| 名字 | Example | 描述 |
|---|---|---|
| Kubebuilder 版本 | v2.2.0, v2.3.0, v2.3.1, v4.2.0 | Kubebuilder 项目的标记版本,表示该仓库中源代码的更改。有关二进制版本,请参阅 releases 页面。 |
| 项目版本 | "1", "2", "3" | 项目版本定义了 PROJECT 配置文件的模式。此版本在 PROJECT 文件的 version 中定义。 |
| 插件版本 | 您训练的数据截至2023年10月。 | 表示单个插件的版本,以及它生成的相应框架。该版本在插件键中定义,例如 go.kubebuilder.io/v2。有关更多详细信息,请参见 设计文档。 |
版本递增
有关 Kubebuilder 版本发布如何工作的更多信息,请参阅 semver 文档。
只有在项目文件方案本身引入破坏性更改时,项目版本才应该增加。对 Go 脚手架或 Kubebuilder CLI 的更改不影响项目版本。
同样,新的插件版本的引入可能只会导致 Kubebuilder 发布一个新的次要版本,因为 CLI 本身没有进行破坏性更改。只有在我们移除对旧版本插件的支持时,这才会对 Kubebuilder 造成破坏性更改。有关插件版本管理的更多详细信息,请参阅插件设计文档中的版本控制部分。
介绍插件的更改
对插件所做的更改仅在发生导致与之前插件版本搭建的项目不兼容的更改时,才需要提高插件版本。一旦插件版本 vX 稳定(不带有 “alpha” 或 “beta” 后缀),应该创建一个新的插件包,其中包含版本 v(X+1)-alpha 的新插件。通常通过(语义上)执行 cp -r pkg/plugins/golang/vX pkg/plugins/golang/v(X+1),然后更新版本号和路径来完成这一操作。所有后续对插件的破坏性更改都应在此包中进行;vX 插件将被冻结,不能再进行破坏性更改。
您还必须在您的 PR 中将迁移指南添加到 Kubebuilder 书籍的 migrations 部分。该指南应详细说明用户将项目从 vX 升级到 v(X+1)-alpha 所需的步骤。
Example
Kubebuilder 默认使用插件 go.kubebuilder.io/v4 来搭建项目。
你创建了一个功能,该功能将在 init 所生成的文件 main.go 中添加一个新标记,create api 将使用这个标记来更新该文件。你所引入的变化将导致与使用插件 go.kubebuilder.io/v4 构建的项目一起使用时出现错误,除非用户手动更新他们的项目。
因此,您的更改对插件 go.kubebuilder.io 引入了一个破坏性的变化,只能合并到插件版本 v5-alpha 中。该插件的包应该已经存在。
FAQ
Controller-Runtime FAQ
Kubebuilder 基于 controller-runtime 与 controller-tools 库开发。也可参阅 Controller-Runtime FAQ。
初始化项目时通过 domain 标志传入的值(例如 kubebuilder init --domain example.com)有何作用?
创建项目后,通常需要扩展 Kubernetes API 并定义项目自有的新 API。因此 Kubebuilder 会在定义项目配置的 PROJECT 文件中记录 domain,并用它作为域名创建 API 端点。请务必理解组、版本和种类,真是让人头大!。
该域名用于组后缀,以明确显示资源组类别。例如,如果设置为 --domain=example.com:
kubebuilder init --domain example.com --repo xxx --plugins=go/v4
kubebuilder create api --group mygroup --version v1beta1 --kind Mykind
则最终的资源组为 mygroup.example.com。
如果域字段未设置,默认值为
my.domain。
我希望自定义我的项目,使用 klog 替代 controller-runtime 提供的 zap。我该如何使用 klog 或其他日志记录器作为项目的日志记录器?
在 main.go 中,你可以替换:
opts := zap.Options{
Development: true,
}
opts.BindFlags(flag.CommandLine)
flag.Parse()
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
与:
flag.Parse()
ctrl.SetLogger(klog.NewKlogr())
在执行 make run 后,我看到类似“无法找到领导者选举命名空间:未在集群内运行…“的错误。
可以启用领导者选举。但若用 make run 在集群外本地测试,可能还需要设置 manager 创建领导者选举资源所用的命名空间,如下:
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
MetricsBindAddress: metricsAddr,
Port: 9443,
HealthProbeBindAddress: probeAddr,
LeaderElection: enableLeaderElection,
LeaderElectionID: "14be1926.testproject.org",
LeaderElectionNamespace: "<project-name>-system",
若用 make deploy 在集群中运行项目,可能不希望添加该选项。因此可用环境变量定制行为,仅在开发时加入该选项,例如:
leaderElectionNS := ""
if os.Getenv("ENABLE_LEADER_ELECTION_NAMESPACE") != "false" {
leaderElectionNS = "<project-name>-system"
}
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
MetricsBindAddress: metricsAddr,
Port: 9443,
HealthProbeBindAddress: probeAddr,
LeaderElection: enableLeaderElection,
LeaderElectionNamespace: leaderElectionNS,
LeaderElectionID: "14be1926.testproject.org",
...
当我在旧版本的 Kubernetes 上部署我的项目时,出现错误“open /var/run/secrets/kubernetes.io/serviceaccount/token: permission denied“。我该如何解决这个问题?
如果您遇到错误:
1.6656687258729894e+09 ERROR controller-runtime.client.config unable to get kubeconfig {"error": "open /var/run/secrets/kubernetes.io/serviceaccount/token: permission denied"}
sigs.k8s.io/controller-runtime/pkg/client/config.GetConfigOrDie
/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.13.0/pkg/client/config/config.go:153
main.main
/workspace/main.go:68
runtime.main
/usr/local/go/src/runtime/proc.go:250
当项目运行在较旧的 Kubernetes 版本(可能 <= 1.21)时,可能由该问题引起。原因是 Kubernetes 将挂载的 token 文件权限设为 0600,参见此处的解决方案。变通方法是:
在 manager.yaml 中添加 fsGroup。
安全上下文:
以非root用户身份运行: true
fsGroup: 65532 # 添加此 fsGroup 以使令牌文件可读
不过,请注意,此问题已被修复,如果您在高版本(可能大于或等于1.22)中部署项目,则不会发生该问题。
当我运行 make install 来应用 CRD 清单时,出现错误 过长:必须最多为 262144 字节。如何解决这个问题?为什么会出现这个错误?
在尝试运行 make install 来应用 CRD 清单时,可能会遇到错误 Too long: must have at most 262144 bytes。这个错误是由于 Kubernetes API 强制执行的大小限制所导致。请注意,make install 目标将使用 kubectl apply -f - 来应用位于 config/crd 下的 CRD 清单。因此,当使用 apply 命令时,API 会用 last-applied-configuration 注解该对象,该注解包含整个之前的配置。如果这个配置太大,就会超过允许的字节大小。(更多信息)
可通过以下几种方式规避该场景,例如:
通过从 CRD 中移除描述:
Controller-gen 用于生成 CRD。使用 maxDescLen=0 去掉描述可减小体积,从而可能解决问题。可按如下示例更新 Makefile,再执行 make manifest 重新生成无描述的 CRD,参见:
.PHONY: manifests
manifests: controller-gen ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects.
# Note that the option maxDescLen=0 was added in the default scaffold in order to sort out the issue
# Too long: must have at most 262144 bytes. By using kubectl apply to create / update resources an annotation
# that the K8s API creates to store the latest version of the resource ( kubectl.kubernetes.io/last-applied-configuration).
# However, it has a size limit and if the CRD is too big with so many long descriptions as this one it will cause the failure.
$(CONTROLLER_GEN) rbac:roleName=manager-role crd:maxDescLen=0 webhook paths="./..." output:crd:artifacts:config=config/crd/bases
通过重新设计您的 API:
您可以审查您的 API 设计,看看它是否有超过应有的规格,从而违反单一职责原则。例如,这样您可能需要重新设计它们。
使用 Server-Side Apply 安装 CRD:
该问题出现在用客户端 apply 安装 CRD 时,也可改用 Server-Side Apply 安装或更新 CRD,例如 kubectl apply --server-side -f ...。这样不会把完整清单存入 kubectl.kubernetes.io/last-applied-configuration 注解,从而可避免大小限制失败。
若更新已有 CRD,根据工作流,kubectl replace -f ... 也可能有帮助。
Server-Side Apply Flag vs kubectl Server-Side Apply
请勿将上文的 kubectl apply --server-side 与 Kubebuilder 的 --ssa 标志混淆,二者概念不同:
-
kubectl apply --server-side(上文所述):kubectl 标志,改变 CRD 安装方式以避免注解大小限制,仅与 CRD 安装有关。 -
--ssa标志:Kubebuilder 标志(如kubebuilder create api --ssa),脚手架出具有运行时行为的控制器,用 Server-Side Apply 模式管理资源字段。这关乎控制器运行时行为,尤其适用于与用户或其他控制器共享字段所有权的场景。它不会改变make install如何应用 CRD。
了解 Server-Side Apply 总体概念,参见 Kubernetes Server-Side Apply 文档。
若有不使用 Server-Side Apply 的 API,请确保带有 +kubebuilder:ac:generate=false 标记。若手动向项目添加了 API,请检查每个不应使用 Server-Side Apply 的 kind 是否都有该标记。详见 Server-Side Apply 参考。
如何有效地校验与解析 CRD 中的字段?
为了提高用户体验,建议在编写 CRD 时使用 OpenAPI v3 schema 验证。然而,这种方法有时可能需要额外的解析步骤。例如,请考虑以下代码:
type StructName struct {
// +kubebuilder:validation:Format=日期-时间
TimeField string `json:"timeField,omitempty"`
}
在这种情况下会发生什么?
- 如果用户尝试创建具有无效 timeField 值的 CRD,Kubernetes API 会向其发送错误通知。
- 在开发者端,字符串值需要在使用之前手动解析。
有没有更好的方法?
为了提供更好的用户体验和简化开发人员体验,建议使用预定义类型,如 metav1.Time。例如,考虑以下代码:
type StructName struct {
TimeField metav1.Time `json:"timeField,omitempty"`
}
在这种情况下会发生什么?
- 用户仍然会收到来自 Kubernetes API 的错误通知,提示无效的
timeField值。 - 开发人员可以直接在代码中使用解析后的 TimeField,而无需额外的解析,从而减少错误并提高效率。
用 Podman 或 Buildah 构建镜像时失败,提示 cmd/main.go: no such file or directory。如何解决?
尽管我们尽量让脚手架兼容其他容器工具,但 Kubebuilder 官方测试与支持的是 Docker。
脚手架生成的 .dockerignore 用 !**/*.go 重新包含所有 Go 源文件。Docker 会遵守该模式;Podman 与 Buildah 不会:它们会跳过被忽略的目录,导致 Go 源码进不了构建上下文。上游跟踪见 buildah#6417,报告见 kubebuilder#5181。
若使用 Podman,请在 .dockerignore 中按目录名重新包含源码目录:
!cmd
!api
!internal
再把项目中其他存放 Go 源文件的目录一并加上。
用 Podman 或 Buildah 构建镜像时出现 short-name "golang:1.26" did not resolve to an alias and no unqualified-search registries are defined。如何解决?
尽管我们尽量让脚手架兼容其他容器工具,但 Kubebuilder 官方测试与支持的是 Docker。
Docker 会把短镜像名解析到 Docker Hub。Podman 与 Buildah 则在主机上解析:使用 registries.conf.d 中的短名别名,或 registries.conf 中的 unqualified-search-registries。主机两者皆无时就会报错。例如 Debian 12 默认都不提供。
使用下列任一方案。
覆盖基础镜像。 脚手架 Dockerfile 将其作为构建参数暴露:
ARG BASE_IMAGE=golang:1.26
FROM ${BASE_IMAGE} AS builder
# 来自 Docker Hub
make docker-build IMG=<some-registry>/<project>:tag BASE_IMAGE=docker.io/library/golang:1.26
# 来自自有仓库或镜像站
make docker-build IMG=<some-registry>/<project>:tag BASE_IMAGE=<myregistry>/golang:1.26
make docker-buildx 同样接受 BASE_IMAGE。
或在主机上为 golang 短名添加别名:
# /etc/containers/registries.conf.d/golang.conf
[aliases]
"golang" = "docker.io/library/golang"
匹配的别名无需查阅 unqualified-search-registries,因此该列表为空时也可用。别名还忽略 tag,一条即可覆盖所有 golang 标签。相比把 docker.io 加进搜索列表(会改变主机上所有非限定镜像名的解析方式),更推荐用别名。无根 Podman 请使用 $HOME/.config/containers/registries.conf.d/golang.conf。