k8s之Kubebuilder最佳实践

零、参考资料

参考资料

一、使用 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 记录重要节点

  1. 首先需要在 Reconciler 对象上增加 Recorder 字段
type ApplicationReconciler struct {
    client.Client
    Scheme *runtime.Scheme
    Recorder record.EventRecorder
}
  1. 在启动时初始化该字段
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)
}
  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)
}
代码解释
  1. ctrl.NewControllerManagedBy(mgr):创建一个由控制器管理器 mgr 管理的新控制器。
  2. For(&v1.Application{}):指定控制器的主要资源类型为 v1.Application,即控制器会对 Application 资源的变化做出响应。
  3. Watches(&appsv1.Deployment{}, ...):手动设置 Watch appsv1.Deployment 资源。当 Deployment 资源发生变化时,会触发后面的映射函数。
  4. handler.EnqueueRequestsFromMapFunc(...):定义一个映射函数,用于将 Deployment 资源的变化映射到 Application 资源的请求。具体逻辑如下:
    • 检查 Deployment 资源的标签中是否包含 app 标签。如果不包含,则说明该 DeploymentApplication 无关,返回 nil
    • 如果包含 app 标签,则创建一个 ctrl.Request 对象,其 NamespacedNameDeployment 所在的命名空间和 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.imagespec.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 资源赋予了 getlistwatchcreateupdatepatchdelete 这些操作权限。

二、Marker 的常见类型及应用

Kubebuilder中的Marker大致分为三种类型:CRD、Controller和Webhook,下面分别详细介绍。

(一)CRD(自定义资源定义)部分
  1. 基本示例
    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` 字段获取值。
  1. 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)。
  1. 自定义CRD展示字段
    默认情况下,使用 kubectl get 命令查询CRD时,只会展示 NAMEAGE 两列,例如:[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 配置。

  1. 自定义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

  1. 自定义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/statusapplications/finalizers)的操作权限。比如第一个Marker表示Controller可以对 applications 资源进行 getlistwatchcreateupdatepatchdelete 操作。

(三)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 - namespace1operator - 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 等。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值