文章目录
零、参考资料
一、使用 OwnerReference 做级联删除与调谐触发
1. 什么是 OwnerReference
在 Kubernetes 中,OwnerReference
是一种用于建立资源之间父子关系的机制。它允许一个资源(子资源)明确指定另一个资源(所有者资源)作为其拥有者。通过这种方式,Kubernetes 能够理解资源之间的依赖关系,并基于这些关系执行一些操作,比如级联删除和事件触发。
例如,当你使用 Deployment 创建 Pod 时,Deployment 会先创建一个 ReplicaSet,然后 ReplicaSet 再创建 Pod。在这个过程中,Pod 的 ownerReferences
字段会指向 ReplicaSet,而 ReplicaSet 的 ownerReferences
字段会指向 Deployment。这样,Kubernetes 就知道 Pod 是由 ReplicaSet 管理的,而 ReplicaSet 是由 Deployment 管理的。
以下是一个简单的 Pod 的 ownerReferences
示例:
apiVersion: v1
kind: Pod
metadata:
name: my-pod
ownerReferences:
- apiVersion: apps/v1
kind: ReplicaSet
name: my-replicaset
uid: 12345678-1234-1234-1234-1234567890ab
2. OwnerReference 的特性
2.1 级联删除
-
当一个所有者资源被删除时,Kubernetes 的垃圾回收器(GC)会自动删除所有与之关联的子资源。这是因为子资源的
ownerReferences
字段指向了所有者资源,GC 可以根据这些引用找到并删除相关的子资源。 -
例如,当你删除一个 Deployment 时,Kubernetes 会先删除与该 Deployment 关联的 ReplicaSet,然后再删除由 ReplicaSet 创建的所有 Pod。这样可以确保所有相关资源都被正确清理,避免出现孤儿资源。
2.2 事件触发
-
子资源对象的变更事件可以触发所有者对象的
Reconcile
方法。在 Kubernetes 中,控制器(如 Deployment 控制器)会不断地观察和调谐资源的状态,以确保实际状态与期望状态一致。当子资源发生变更时,控制器会收到通知,并调用Reconcile
方法来重新评估和调整所有者资源的状态。 -
例如,当一个 Pod 因为某种原因失败并被重新创建时,ReplicaSet 会检测到这个变更,并触发 Deployment 控制器的
Reconcile
方法。Deployment 控制器会根据新的 Pod 状态来调整 ReplicaSet 的副本数量,以确保满足 Deployment 的期望副本数。 -
使用 OwnerRefrence 来做资源关联,有两个特性:
Owner 资源被删除,被关联的子资源会被级联删除,利用 K8s 的 GC 来做资源清理;
子资源对象的变更事件变更可以触发 Owner 对象的 Reconcile 方法;
3. 基于上述特性,我们可以优化两个地方
- 1)Application 对象的清理逻辑
- 2)Application 对象调谐的触发
3.1 优化 App 对象的清理逻辑
- 之前 Controller 在清理 Application 时,需要先删除对应的 Deployment,然后才能删除 Application 上的 Finalizer
现在只需要子资源(Deployment) OwnerReferences 设置为 Application,这样删除 Application 时,K8s 的 GC 会自动删除关联的子资源(Deployment) - 创建 Deployment 对象修改如下:
func (r *ApplicationReconciler) generateDeployment(app v1.Application) appsv1.Deployment {
deploy := appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: deploymentName(app.Name),
Namespace: app.Namespace,
Labels: map[string]string{
"app": app.Name,
},
},
Spec: appsv1.DeploymentSpec{
Replicas: ptr.To(int32(1)), // 副本数
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"app": app.Name,
},
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{
"app": app.Name,
},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: app.Name,
Image: app.Spec.Image,
},
},
},
},
},
}
_ = controllerutil.SetControllerReference(&app, &deploy, r.Scheme)
return deploy
}
- 增加下面这一句,为 Deploy 对象设置 Owner
_ = controllerutil.SetControllerReference(&app, &deploy, r.Scheme)
- 这样就可以借助 K8s 的 GC 逻辑,当 Application CR 对象被删除时,关联的 Deployment 对象会自动删除。
3.2 优化 App 对象调谐的触发
- 只需要给 Deployment 添加 OwnerReferences,指定为对应的 Application 对象,然后在 SetupWithManager 中通过 Owns 指定即可, 这样当 Deployment 变化时就会触发关联 Application 对象的调谐,这样就可以移除这部分逻辑了。
完整代码如下:
// SetupWithManager sets up the controller with the Manager.
func (r *ApplicationReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&v1.Application{}).
Owns(&appsv1.Deployment{}).
Named("application").
Complete(r)
}
3.3 配置子资源权限为了 Watch 子资源,以触发调谐,需要给 Controller 赋予足够的权限。
- 在对于位置增加 Marker 赋予 Deployment CRUD 权限。
// +kubebuilder:rbac:groups=core.crd.lixueduan.com,resources=applications,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=core.crd.lixueduan.com,resources=applications/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=core.crd.lixueduan.com,resources=applications/finalizers,verbs=update
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
func (r *ApplicationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
}
二、使用 Finalizers 做资源清理
1. Finalizers 工作原理深入剖析
- 在 Kubernetes 中,Finalizers 作为 API 对象metadata字段中的一个列表,其主要作用是控制对象的删除流程。当一个带有 Finalizers 的对象被请求删除时,Kubernetes 的api-server并不会立即将其从系统中移除,而是会在该对象的metadata.deletionTimestamp字段中记录当前时间。这一操作就像是给对象打上了一个 “已标记删除” 的标签。
- 这样做的原因在于,有些资源的清理工作可能需要在对象被真正删除之前完成。如果api-server立即删除对象,那么相关的控制器(Controller)就无法再获取到该对象的信息,从而无法执行必要的清理操作。
- 只有当所有的 Finalizers 都被移除后,api-server才会真正删除该对象。这就为资源清理提供了一个可靠的机制,确保在对象生命周期结束时,与之相关的所有资源都得到了妥善处理。
2. 注意要点
- Finalizer 名称的唯一性:在使用 Finalizers 时,应确保 Finalizer 的名称在集群中是唯一的,通常使用域名反写的方式来命名,如example.com/finalizer,以避免与其他资源的 Finalizers 冲突。
- 清理操作的幂等性:清理操作应该是幂等的,即多次执行相同的清理操作不会对系统造成额外的影响。这是因为在实际的运行过程中,由于各种原因(如网络问题、控制器重启等),清理操作可能会被多次触发。
- Finalizers 的顺序:虽然 Kubernetes 并不严格要求 Finalizers 的顺序,但在实际应用中,应根据资源清理的依赖关系合理安排 Finalizers 的顺序,确保清理操作能够按照正确的顺序执行。
- 尽量避免无意义的 Reconcile:我们需要在更新 status 前判断是否需要更新:
copyApp := app.DeepCopy()
copyApp.Status.Ready = deploy.Status.ReadyReplicas >= 1
if !reflect.DeepEqual(app, copyApp) { // update when changed
log.Log.Info("sync app status", "app", req.NamespacedName)
if err = r.Client.Status().Update(ctx, copyApp); err != nil {
log.Log.Error(err, "unable to update application status", "app", req.NamespacedName)
return ctrl.Result{}, err
}
}
3. 例子
假设我们定义了一个 VM 对象,在集群中创建一个 VM 对象之后,Controller 都会真正启动一台虚拟机,那么我们删除 VM 对象时自然也需要删除对应的虚拟机。
此时就可以使用 Finalizers,工作流程如下:
- 1)用户创建 VM 对象
- 2)Controller 检测到没有 Finalizers 则添加
- 3)Controller 检测到 VM 对象创建,创建虚拟机
- 4)用户删除 VM 对象
- 5)api-server 检测到 VM 对象有 Finalizers,并未立即删除该对象,而是修改 metadata.deletionTimestamp 为当前时间,表示该对接处于删除中
- 6)Controller 检测到 VM 对象 metadata.deletionTimestamp 不为 0,知道该对象已经在删除中,于是执行清理操作,删除虚拟机,清理完成后删除 VM 对象上的 Finalizers
- 7) 检测到 VM 对象 metadata.deletionTimestamp 不为 0,且 Finalizers 为空,该对象被真正删除
代码逻辑大概是这样的:
- 对于 DeletionTimestamp 为 0 的对象,说明是新建的,如果没有 Finalizers 就添加
- 对于 DeletionTimestamp 不为 0 的对象,说明已经在删除中了,执行资源清理工作,清理完成后移除 Finalizers,让该对象能够成功被删除
4. 代码
package main
import (
"context"
"reflect"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/tools/record"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
yourAppV1 "your-app/api/v1" // 请替换为实际的应用 API 包路径
)
const (
// AppFinalizer 是自定义的 Finalizer 名称,用于控制应用资源的删除流程
AppFinalizer = "your-app.example.com/finalizer"
)
// ApplicationReconciler 结构体用于处理应用资源的协调逻辑
type ApplicationReconciler struct {
client.Client
Scheme *runtime.Scheme
Recorder record.EventRecorder
}
// Reconcile 方法是核心的协调逻辑,处理应用资源的创建、更新和删除操作
func (r *ApplicationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
// 从上下文获取日志记录器,并添加应用的命名空间和名称信息
logger := ctrl.Log.FromContext(ctx)
log := logger.WithValues("application", req.NamespacedName)
log.Info("start reconcile")
// 从 Kubernetes API 服务器中获取指定的应用资源
var app yourAppV1.Application
err := r.Get(ctx, req.NamespacedName, &app)
if err != nil {
// 记录获取应用资源时的错误信息
log.Error(err, "unable to fetch application")
// 忽略资源未找到的错误,因为这种错误无法通过立即重试解决
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// 检查应用资源的删除时间戳是否为零值
if app.ObjectMeta.DeletionTimestamp.IsZero() {
// 如果删除时间戳为零值,说明应用资源未被标记为删除
// 检查应用资源是否已经包含我们自定义的 Finalizer
if!controllerutil.ContainsFinalizer(&app, AppFinalizer) {
// 如果不包含 Finalizer,则添加 Finalizer 并更新应用资源
controllerutil.AddFinalizer(&app, AppFinalizer)
if err = r.Update(ctx, &app); err != nil {
// 记录添加 Finalizer 失败的错误信息
log.Error(err, "unable to add finalizer to application")
return ctrl.Result{}, err
}
}
} else {
// 如果删除时间戳不为零值,说明应用资源正在被删除
// 检查应用资源是否包含我们自定义的 Finalizer
if controllerutil.ContainsFinalizer(&app, AppFinalizer) {
// 如果包含 Finalizer,则执行外部资源的清理操作
if err = r.deleteExternalResources(&app); err != nil {
// 记录清理外部资源失败的错误信息
log.Error(err, "unable to cleanup application")
// 如果清理失败,返回错误以便重试
return ctrl.Result{}, err
}
// 清理完成后,从应用资源中移除 Finalizer 并更新
controllerutil.RemoveFinalizer(&app, AppFinalizer)
if err = r.Update(ctx, &app); err != nil {
return ctrl.Result{}, err
}
}
// 由于应用资源正在被删除,停止协调过程
return ctrl.Result{}, nil
}
// 这里可以添加你的其他协调逻辑,例如创建或更新相关的 Kubernetes 资源
// 复制应用资源,以便修改状态信息
copyApp := app.DeepCopy()
// 假设这里根据 Deployment 的就绪副本数来设置应用的状态
// 请根据实际情况替换 deploy 为正确的 Deployment 对象
// 例如:deploy, err := r.getDeployment(ctx, req.NamespacedName)
// 这里只是示例,实际使用时需要实现 getDeployment 方法
// 假设 deploy 已经正确获取
deploy := &yourAppV1.Deployment{} // 请替换为实际的 Deployment 对象
// 如果就绪副本数大于等于 1,则将应用状态设置为就绪
copyApp.Status.Ready = deploy.Status.ReadyReplicas >= 1
// 比较原始应用资源和副本的状态信息是否有变化
if!reflect.DeepEqual(app, copyApp) {
// 如果有变化,则记录日志并更新应用资源的状态
log.Info("sync app status", "app", req.NamespacedName)
if err = r.Status().Update(ctx, copyApp); err != nil {
// 记录更新应用状态失败的错误信息
log.Error(err, "unable to update application status", "app", req.NamespacedName)
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}
// deleteExternalResources 方法用于删除与应用相关的外部资源
// 该方法应确保删除操作是幂等的,即多次执行相同的删除操作不会产生额外的影响
func (r *ApplicationReconciler) deleteExternalResources(app *yourAppV1.Application) error {
// 在这里实现删除与应用相关的外部资源的逻辑
// 例如删除云存储中的文件、关闭数据库连接等
// 确保删除操作是幂等的,并且可以安全地多次调用
return nil
}
三、使用 Event 记录重要节点
- 首先需要在 Reconciler 对象上增加 Recorder 字段
type ApplicationReconciler struct {
client.Client
Scheme *runtime.Scheme
Recorder record.EventRecorder
}
- 在启动时初始化该字段
if err = (&controller.ApplicationReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Recorder: mgr.GetEventRecorderFor("application-controller"),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Application")
os.Exit(1)
}
- 记录Event:
if app.ObjectMeta.DeletionTimestamp.IsZero() {
// The object is not being deleted, so if it does not have our finalizer,
// then lets add the finalizer and update the object. This is equivalent
// to registering our finalizer.
if !controllerutil.ContainsFinalizer(&app, AppFinalizer) {
controllerutil.AddFinalizer(&app, AppFinalizer)
if err = r.Update(ctx, &app); err != nil {
log.Error(err, "unable to add finalizer to application")
return ctrl.Result{}, err
}
// .. 添加
// finalizer 时记录一个事件
r.Recorder.Eventf(&app, corev1.EventTypeNormal, "AddFinalizer", fmt.Sprintf("add finalizer %s", AppFinalizer))
}
} else {
// The object is being deleted
if controllerutil.ContainsFinalizer(&app, AppFinalizer) {
// ..
// 移除 finalizer 时记录一个事件
r.Recorder.Eventf(&app, corev1.EventTypeNormal, "RemoveFinalizer", fmt.Sprintf("remove finalizer %s", AppFinalizer))
}
if !reflect.DeepEqual(app, copyApp) { // update when changed
log.Info("app changed,update app status")
if err = r.Client.Status().Update(ctx, copyApp); err != nil {
log.Error(err, "unable to update application status")
return ctrl.Result{}, err
}
// 状态变化时也记录一个
r.Recorder.Eventf(&app, corev1.EventTypeNormal, "UpdateStatus", fmt.Sprintf("update status from %v to %v", app.Status, copyApp.Status))
}
四、Watching Resource
手动设置 Watch 指定资源
在 Kubernetes 控制器中,除了自动 Watch 带有 OwnerReference
的子资源外,还可以手动设置 Watch 指定的资源。这样做可以让控制器在特定资源发生变化时触发相应的处理逻辑。
代码示例
// SetupWithManager sets up the controller with the Manager.
func (r *ApplicationReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&v1.Application{}).
Watches(&appsv1.Deployment{},
handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []ctrl.Request {
app, ok := obj.GetLabels()["app"]
if !ok { // if no app label,means not owned by app,do nothing
return nil
}
return []ctrl.Request{{NamespacedName: types.NamespacedName{
Namespace: obj.GetNamespace(),
Name: app, // return name is app name,not deployment name
},
}}
})).
Named("application").
Complete(r)
}
代码解释
ctrl.NewControllerManagedBy(mgr)
:创建一个由控制器管理器mgr
管理的新控制器。For(&v1.Application{})
:指定控制器的主要资源类型为v1.Application
,即控制器会对Application
资源的变化做出响应。Watches(&appsv1.Deployment{}, ...)
:手动设置 Watchappsv1.Deployment
资源。当Deployment
资源发生变化时,会触发后面的映射函数。handler.EnqueueRequestsFromMapFunc(...)
:定义一个映射函数,用于将Deployment
资源的变化映射到Application
资源的请求。具体逻辑如下:- 检查
Deployment
资源的标签中是否包含app
标签。如果不包含,则说明该Deployment
与Application
无关,返回nil
。 - 如果包含
app
标签,则创建一个ctrl.Request
对象,其NamespacedName
为Deployment
所在的命名空间和app
标签的值。这个请求会被加入到控制器的工作队列中,触发对相应Application
资源的调谐。
- 检查
使用 Predicates 进行过滤
Predicates 可以让控制器根据事件类型(如创建、更新、删除)和资源字段的变化来定义过滤条件,从而优化控制器的行为,只对特定的资源变化做出响应。
定义 Predicate
package controller
import (
v1 "github.com/lixd/i-operator/api/v1"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/predicate"
)
// Predicate to trigger reconciliation only on size changes in the Busybox spec
var updatePred = predicate.Funcs{
// Only allow updates when the spec.image or spec.enabled of the Application resource changes
UpdateFunc: func(e event.UpdateEvent) bool {
oldObj := e.ObjectOld.(*v1.Application)
newObj := e.ObjectNew.(*v1.Application)
// Trigger reconciliation only if the spec.image or spec.enabled field has changed
return oldObj.Spec.Image != newObj.Spec.Image || oldObj.Spec.Enabled != newObj.Spec.Enabled
},
// Allow create events
CreateFunc: func(e event.CreateEvent) bool {
return true
},
// Allow delete events
DeleteFunc: func(e event.DeleteEvent) bool {
return true
},
// Allow generic events (e.g., external triggers)
GenericFunc: func(e event.GenericEvent) bool {
return true
},
}
代码解释
predicate.Funcs
:定义一个包含多个事件处理函数的结构体,用于处理不同类型的事件。UpdateFunc
:处理资源更新事件。只有当Application
资源的spec.image
或spec.enabled
字段发生变化时,才返回true
,表示触发调谐。否则返回false
,忽略该更新事件。CreateFunc
:处理资源创建事件,返回true
表示允许创建事件触发调谐。DeleteFunc
:处理资源删除事件,返回true
表示允许删除事件触发调谐。GenericFunc
:处理通用事件(如外部触发),返回true
表示允许通用事件触发调谐。
使用 Predicate
// SetupWithManager sets up the controller with the Manager.
func (r *ApplicationReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&v1.Application{}).
Watches(&appsv1.Deployment{},
handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []ctrl.Request {
app, ok := obj.GetLabels()["app"]
if !ok { // if no app label,means not owned by app,do nothing
return nil
}
return []ctrl.Request{{NamespacedName: types.NamespacedName{
Namespace: obj.GetNamespace(),
Name: app, // return name is app name,not deployment name
},
}}
}), builder.WithPredicates(updatePred)).
Named("application").
Complete(r)
}
代码解释
在 Watches
方法中,使用 builder.WithPredicates(updatePred)
选项指定了前面定义的 updatePred
作为过滤条件。这样,当 Deployment
资源发生变化时,只有满足 updatePred
中定义的条件的事件才会触发调谐逻辑,从而避免了不必要的调谐操作。
总结
通过手动设置 Watch 指定资源和使用 Predicates 进行过滤,可以让 Kubernetes 控制器更加灵活和高效地处理资源变化,减少无效的调谐触发,提高系统的性能和稳定性。
五、Kubebuilder 支持的标记元数据(Marker)
一、Kubebuilder 中 Marker 的基本概念
在Kubebuilder中,很多功能都依赖于标记元数据(Marker)来实现。Marker的具体表现形式是一种特殊格式的注释,即 //+ kubebuilder
开头的注释。这些注释的作用是告知 controller-tools
生成额外的信息,从而实现Kubernetes相关资源(如自定义资源定义CRD、控制器Controller、Webhook等)的特定配置和功能。
例如,在配置权限时就使用了Marker:// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
,它表示为 apps
组下的 deployments
资源赋予了 get
、list
、watch
、create
、update
、patch
、delete
这些操作权限。
二、Marker 的常见类型及应用
Kubebuilder中的Marker大致分为三种类型:CRD、Controller和Webhook,下面分别详细介绍。
(一)CRD(自定义资源定义)部分
- 基本示例
在api/v1/application_types.go
文件中,对Application
自定义资源进行了如下Marker配置:
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:shortName="app"
// +kubebuilder:printcolumn:name="Image",type="string",JSONPath=`.spec.image`
// +kubebuilder:printcolumn:name="Enabled",type=boolean,JSONPath=`.spec.enabled`
// +kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.ready`
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
// Application is the Schema for the applications API.
type Application struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec ApplicationSpec `json:"spec,omitempty"`
Status ApplicationStatus `json:"status,omitempty"`
}
- `// +kubebuilder:object:root=true`:表示该资源是根对象,即它是一个顶级的自定义资源。
- `// +kubebuilder:subresource:status`:将 `status` 作为子资源,这样在更新 `Application` 对象时,推荐使用 `app.Status().Update()` 做局部更新,以避免并发冲突。
- `// +kubebuilder:resource:shortName="app"`:定义了当前CRD的短名称。原本 `Application` 的完整名称在使用 `kubectl` 命令操作时可能较长,这里定义了短名称 `app` 后,操作会更方便。
- `// +kubebuilder:printcolumn` 系列:用于定义执行 `kubectl get` 命令时展示的字段。
- `name`:是 `kubectl` 命令中展示的表头名字,比如 `Image`、`Enabled` 等。
- `type`:指定数据的类型,取值包括 `boolean`(布尔型)、`date`(日期型)、`integer`(整型)、`number`(数字型)、`string`(字符串型)。
- `JSONPath`:用于配置这个字段的取值方式,例如 `JSONPath=`.spec.image`` 表示从资源的 `spec.image` 字段获取值。
- CRD Validation(CRD校验)
CRDs支持使用OpenAPI v3 schema格式的校验规范。例如:
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=Lion;Wolf;Dragon
type Alias string
// +kubebuilder:validation:Minimum=1
// +kubebuilder:validation:Maximum=3
// +kubebuilder:validation:ExclusiveMaximum=false
type Rank int32
- 对于 `Name` 字段,通过 `// +kubebuilder:validation:MaxLength=15` 和 `// +kubebuilder:validation:MinLength=1` 限定了字符串的最大长度为15,最小长度为1。
- `Knights` 字段是一个字符串切片,`// +kubebuilder:validation:MaxItems=500` 表示切片中最多有500个元素,`// +kubebuilder:validation:MinItems=1` 表示最少有1个元素,`// +kubebuilder:validation:UniqueItems=true` 表示切片中的元素必须唯一。
- `Alias` 类型通过 `// +kubebuilder:validation:Enum=Lion;Wolf;Dragon` 限定了其取值只能是 `Lion`、`Wolf` 或 `Dragon` 之一。
- `Rank` 类型(`int32`)通过 `// +kubebuilder:validation:Minimum=1` 和 `// +kubebuilder:validation:Maximum=3` 限定了取值范围在1到3之间,`// +kubebuilder:validation:ExclusiveMaximum=false` 表示最大值3是包含在范围内的(即可以取到3)。
- 自定义CRD展示字段
默认情况下,使用kubectl get
命令查询CRD时,只会展示NAME
和AGE
两列,例如:[root@bench ~]# kubectl get applications NAME AGE demo 3s
。
要增加自定义列,可以在对应资源上增加// +kubebuilder:printcolumn
形式的注释。比如添加了// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
等自定义列配置后,需要重新生成manifest并安装(使用make install
命令)。
重新生成的CRD yaml中会增加additionalPrinterColumns
相关信息,如:
- additionalPrinterColumns:
- jsonPath: .spec.image
name: Image
type: string
- jsonPath: .spec.enabled
name: Enabled
type: boolean
- jsonPath: .status.ready
name: Ready
type: string
- jsonPath: .metadata.creationTimestamp
name: Age
type: date
再次使用 kubectl get applications
命令时,就会展示自定义的列:[root@bench ~]# kubectl get applications NAME IMAGE ENABLED READY AGE demo nginx:1.22 true true 16m
。
需要注意的是,配置自定义列之后,默认的 Age
列如果想要展示,需要手动加上对应的 printcolumn
配置。
- 自定义CRD Shortname
默认使用CRD的完整名称操作,如[root@bench ~]# kubectl get applications
。通过添加// +kubebuilder:resource:shortName="app"
注释并重新生成manifest安装后,CRD的配置中会增加shortNames
字段:
spec:
group: core.crd.lixueduan.com
names:
kind: Application
listKind: ApplicationList
plural: applications
shortNames:
- app
此时就可以使用短名称 app
来操作,如 [root@bench ~]# kubectl get app NAME IMAGE ENABLED READY AGE demo nginx:1.22 true true 16m
。
- 自定义CRD Subresources
除了status
作为子资源(默认),当正确指定replicas
字段的specpath
以及statuspath
后,可以使用kubectl scale
命令方便地修改CRD的replicas
值,类似Deployment
资源。对应的Marker为://+kubebuilder:subresource:scale:specpath=.spec.replicas,statuspath=.status.replicas,selectorpath=.status.selector
。
(二)Controller部分
在Controller中,由于需要访问集群中的资源对象,所以默认生成了RBAC(Role-Based Access Control,基于角色的访问控制)相关的Marker。例如在 internal/controller/application_controller.go
文件中:
// +kubebuilder:rbac:groups=core.crd.lixueduan.com,resources=applications,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=core.crd.lixueduan.com,resources=applications/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=core.crd.lixueduan.com,resources=applications/finalizers,verbs=update
func (r *ApplicationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
//....
}
这些Marker定义了Controller对 core.crd.lixueduan.com
组下 applications
资源及其子资源(applications/status
和 applications/finalizers
)的操作权限。比如第一个Marker表示Controller可以对 applications
资源进行 get
、list
、watch
、create
、update
、patch
、delete
操作。
(三)Webhook部分
Webhook的Marker主要用于定义Webhook的相关信息,如路径(path
)、失败策略(failurePolicy
)、组(group
)等。例如在 internal/webhook/v1/application_webhook.go
文件中:
// +kubebuilder:webhook:path=/mutate-core-crd-lixueduan-com-v1-application,mutating=true,failurePolicy=fail,sideEffects=None,groups=core.crd.lixueduan.com,resources=applications,verbs=create;update,versions=v1,name=mapplication-v1.lixueduan.com,admissionReviewVersions=v1
type ApplicationCustomDefaulter struct {
// TODO(user): Add more fields as needed for defaulting
}
// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.
// NOTE: The 'path' attribute must follow a specific pattern and should not be modified directly here.
// Modifying the path for an invalid path can cause API server errors; failing to locate the webhook.
// +kubebuilder:webhook:path=/validate-core-crd-lixueduan-com-v1-application,mutating=false,failurePolicy=fail,sideEffects=None,groups=core.crd.lixueduan.com,resources=applications,verbs=create;update,versions=v1,name=vapplication-v1.lixueduan.com,admissionReviewVersions=v1
type ApplicationCustomValidator struct {
// TODO(user): Add more fields as needed for validation
}
- 对于 `ApplicationCustomDefaulter` 类型对应的Webhook,`// +kubebuilder:webhook:path=/mutate-core-crd-lixueduan-com-v1-application` 定义了Webhook的路径,`mutating=true` 表示这是一个可变更的Webhook(会修改资源对象),`failurePolicy=fail` 表示如果Webhook调用失败,请求将被拒绝,`sideEffects=None` 表示没有副作用,`groups=core.crd.lixueduan.com` 等定义了该Webhook作用的资源组、资源、操作动词等信息。
- `ApplicationCustomValidator` 类型对应的Webhook是一个验证Webhook(`mutating=false`),用于验证资源的创建和更新操作,其配置也类似,定义了路径、失败策略等相关信息。
六、其他
1. Controller Scope(控制器作用域)
设置 CRD Scope(自定义资源定义作用域)
在使用 Kubebuilder 创建 API 时,可以通过 --namespaced
参数来指定自定义资源定义(CRD)的作用域,它可以是命名空间(namespace)范围或集群(cluster)范围。
- 创建时指定:当你执行
kubebuilder create api --group core --version v1 --kind Application --namespaced=true
命令时,明确指定了Application
这个 CRD 是命名空间范围的。如果不设置--namespaced
参数,默认会将其视为命名空间范围的 CRD。 - 已创建后修改:如果 CRD 已经创建好,也可以通过更新 Marker 的方式来修改其作用域。例如:
//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
//+kubebuilder:resource:scope=Cluster,shortName=mc
这里的 //+kubebuilder:resource:scope=Cluster
表明将该 CRD 的作用域设置为集群范围,同时还定义了短名称 mc
。
Controller 作用域调整
控制器默认是集群范围的,也就是说,默认情况下控制器会监视(Watch)所有命名空间下的 CRD 对象。不过,你可以通过配置来让控制器只监视某些特定命名空间的对象。
- 默认配置:没有进行相关配置时,代码如下:
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
// 其他配置...
})
这种情况下,控制器会监视所有命名空间下的相关对象。
- 指定单个命名空间:
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
// 其他配置...
Cache: cache.Options{
DefaultNamespaces: map[string]cache.Config{"operator - namespace": cache.Config{}},
},
})
这里通过 DefaultNamespaces
配置,让控制器只监视 operator - namespace
这个命名空间下的对象。
- 指定多个命名空间:
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
// 其他配置...
Cache: cache.Options{
DefaultNamespaces: map[string]cache.Config{
"operator - namespace1": cache.Config{},
"operator - namespace2": cache.Config{},
},
},
})
在这个配置中,控制器会监视 operator - namespace1
和 operator - namespace2
这两个命名空间下的对象。
2. Use External Resource(使用外部资源)
外部 CRD
在某些情况下,我们可能需要为外部的 API 对象定义一个控制器。对于外部的 CRD,可以使用 create api
命令,并通过一些特定的标志来指定相关信息。
kubebuilder create api --group <theirgroup> --version <theirversion> --kind <theirKind> --controller --resource=false --external-api-path=<their Golang path import> --external-api-domain=<theirdomain>
--resource=false
:表示不创建新的资源对象,因为我们是在使用外部已经定义好的资源。--external-api-path
:指定外部资源的 Go 语言导入路径。--external-api-domain
:指定外部资源的域名。
创建 Webhook 时也是类似的操作,例如:
kubebuilder create webhook --group certmanager --version v1 --kind Issuer --defaulting --programmatic-validation --external-api-path=github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1 --external-api-domain=cert-manager.io
K8s Core API
对于 Kubernetes 的核心 API 对象(如 Pod、Service、Deployment 等),不需要从外部导入,可直接使用。
- 创建 Controller:
kubebuilder create api --group core --version v1 --kind Pod --controller=true --resource=false
这里为 Pod
对象创建了一个控制器,--resource=false
表示不创建新的资源对象。
- 创建 Webhook:
kubebuilder create webhook --group core --version v1 --kind Pod --programmatic-validation
为 Pod
对象创建了一个验证 Webhook。
3. PROJECT 配置文件详解
在项目根目录会生成一个名为 PROJECT
的文件,它记录了 Kubebuilder 项目的配置信息。以下是对文件中各个字段的详细解释:
domain: crd.lixueduan.com
layout:
- go.kubebuilder.io/v4
projectName: i - operator
repo: github.com/lixd/i - operator
resources:
- api:
crdVersion: v1
namespaced: true
controller: true
domain: crd.lixueduan.com
group: core
kind: Application
path: github.com/lixd/i - operator/api/v1
version: v1
webhooks:
defaulting: true
validation: true
webhookVersion: v1
version: "3"
- layout:定义了全局插件,这里只有一个
go.kubebuilder.io/v4
插件,它决定了项目的整体结构和构建方式。 - domain:项目域名,通常用于标识项目的所属范围或组织。
- projectName:项目名称,方便对项目进行识别和管理。
- repo:项目的 Go module 定义的地址,用于在 Go 语言中引用和管理项目的依赖。
- version:项目版本,当前为 3,表示项目使用的 Kubebuilder 版本。
- resources:项目中定义的资源列表,下面是每个资源的详细配置:
- api.crdVersion:CRD 对象的 Kubernetes API version,指定了 CRD 使用的 Kubernetes API 版本。
- api.namespaced:API 的 RBAC 权限范围,可以是
namespaced
(命名空间范围)或者cluster
(集群范围)。 - controller:是否为 CRD 对象创建 Controller,如果为
true
,则会生成对应的控制器代码。 - domain:资源的 domain,通常与项目的 domain 保持一致。
- group:资源的 group,是 GVK(Group、Version、Kind)中的 G,用于对资源进行分类。
- kind:资源的 kind,是 GVK 中的 K,代表资源的具体类型,如
Application
。 - version:资源的 version,是 GVK 中的 V,指定资源的版本。
- path:资源路径,格式为
<repo>/api/<kind>
,用于定位资源的代码文件。 - webhooks:Webhook 相关配置:
- defaulting:是否创建 Mutating Webhook(变更式 Webhook),用于在资源创建或更新时对其进行默认值设置。
- validation:是否创建 Validating Webhook(验证式 Webhook),用于在资源创建或更新时对其进行验证。
- webhookVersion:Webhook 对象的 Kubernetes API version。
通过这个 PROJECT
文件,我们可以清晰地了解当前项目的大致情况,包括创建了哪些 CR 对象,对象的 domain、GVK 信息,以及是否配置了 Webhook 等。