6- kubebuilder 實戰: status - event

在上篇文章當中我們實現了 NodePool Operator 基本的 CURD 功能,跑了一小段時間之後除了 CURD 之外我們有了更高的需求,想知道一個節點池有多少的節點,現在的資源佔比是多少,這樣可以清晰的知道我們現在的水位線是多少,除此之外也想知道節點池數量發生變化的相關事件信息,什麼時候節點池增加或者是減少了一個節點等。

需求

我們先整理一下需求

能夠通過 kubectl get Nodepool瞭解當前的節點池的以下信息

能夠通過事件信息得知 controller 的錯誤情況以及節點池內節點的變化情況

實現

Status

先修改一下 status 對象,注意要確保下面的 //+kubebuilder:subresource:status註釋存在,這個表示開啓 status 子資源,status 對象修改好之後需要重新執行一遍 make install

// NodePoolStatus defines the observed state of NodePool
type NodePoolStatus struct {
 // status=200 說明正常,其他情況爲異常情況
 Status int `json:"status"`

 // 節點的數量
 NodeCount int `json:"nodeCount"`

 // 允許被調度的容量
 Allocatable corev1.ResourceList `json:"allocatable,omitempty" protobuf:"bytes,2,rep,`
}

//+kubebuilder:object:root=true
//+kubebuilder:resource:scope=Cluster
//+kubebuilder:subresource:status

// NodePool is the Schema for the nodepools API
type NodePool struct {

然後修改 Reconcile 中的邏輯

func (r *NodePoolReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
 // ......

 if len(nodes.Items) > 0 {
  r.Log.Info("find nodes, will merge data""nodes", len(nodes.Items))
+  pool.Status.Allocatable = corev1.ResourceList{}
+  pool.Status.NodeCount = len(nodes.Items)
  for _, n := range nodes.Items {
   n := n

   // 更新節點的標籤和污點信息
   err := r.Update(ctx, pool.Spec.ApplyNode(n))
   if err != nil {
    return ctrl.Result{}, err
   }

+   for name, quantity := range n.Status.Allocatable {
+    q, ok := pool.Status.Allocatable[name]
+    if ok {
+     q.Add(quantity)
+     pool.Status.Allocatable[name] = q
+     continue
+    }
+    pool.Status.Allocatable[name] = quantity
+   }
  }
 }

  // ......
  
+ pool.Status.Status = 200err = r.Status().Update(ctx, pool)
 return ctrl.Result{}, err
}

修改好了之後我們提交一個 NodePool 測試一下

apiVersion: nodes.lailin.xyz/v1
kind: NodePool
metadata:
  name: worker
spec:
  taints:
    - key: node-pool.lailin.xyz
      value: worker
      effect: NoSchedule
  labels:
    "node-pool.lailin.xyz/worker""10"
  handler: runc

可以看到我們現在是有兩個 worker 節點

▶ kubectl get no 
NAME                 STATUS   ROLES                  AGE   VERSION
kind-control-plane   Ready    control-plane,master   29m   v1.20.2
kind-worker          Ready    worker                 28m   v1.20.2
kind-worker2         Ready    worker                 28m   v1.20.2

然後我們看看 NodePool,可以發現已經存在了預期的 status

status:
  allocatable:
    cpu: "8"
    ephemeral-storage: 184026512Ki
    hugepages-1Gi: "0"
    hugepages-2Mi: "0"
    memory: 6129040Ki
    pods: "220"
  nodeCount: 2
  status: 200

現在這樣只能通過查看 yaml 詳情才能看到,當 NodePool 稍微多一些的時候就不太方便,我們現在給 NodePool 增加一些 kubectl 展示的列

+//+kubebuilder:printcolumn:JSONPath=".status.status",name=Status,type=integer
+//+kubebuilder:printcolumn:JSONPath=".status.nodeCount",name=NodeCount,type=integer
//+kubebuilder:object:root=true
//+kubebuilder:resource:scope=Cluster
//+kubebuilder:subresource:status

如上所示只需要添加好對應的註釋,然後執行 make install即可

然後再執行 kubectl get NodePool 就可以看到對應的列了

▶ kubectl get NodePool 
NAME     STATUS   NODECOUNT
worker   200      2

Event

我們在 controller 當中添加 Recorder 用來記錄事件,K8s 中事件有 Normal 和 Warning 兩種類型

// NodePoolReconciler reconciles a NodePool object
type NodePoolReconciler struct {
 client.Client
 Log      logr.Logger
 Scheme   *runtime.Scheme
+ Recorder record.EventRecorder
}

func (r *NodePoolReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
 
+ // 添加測試事件
+ r.Recorder.Event(pool, corev1.EventTypeNormal, "test""test")

 pool.Status.Status = 200
 err = r.Status().Update(ctx, pool)
 return ctrl.Result{}, err
}

添加好之後還需要在 main.go 中加上 Recorder的初始化邏輯

if err = (&controllers.NodePoolReconciler{
  Client:   mgr.GetClient(),
  Log:      ctrl.Log.WithName("controllers").WithName("NodePool"),
  Scheme:   mgr.GetScheme(),
+  Recorder: mgr.GetEventRecorderFor("NodePool"),
 }).SetupWithManager(mgr); err != nil {
  setupLog.Error(err, "unable to create controller""controller""NodePool")
  os.Exit(1)
 }

加好之後我們運行一下,然後在 describe Nodepool 對象就能看到事件信息了

Events:
  Type    Reason  Age   From      Message
  ----    ------  ----  ----      -------
  Normal  test    4s    NodePool  test

監聽更多資源

之前我們所有的代碼都是圍繞着 NodePool 的變化來展開的,但是我們如果修改了 Node 的相關標籤,將 Node 添加到一個 NodePool,Node 上對應的屬性和 NodePool 的 status 信息也不會改變。如果我們想要實現上面的效果就需要監聽更多的資源變化。

在 controller 當中我們可以看到一個 SetupWithManager方法,這個方法說明了我們需要監聽哪些資源的變化

// SetupWithManager sets up the controller with the Manager.
func (r *NodePoolReconciler) SetupWithManager(mgr ctrl.Manager) error {
 return ctrl.NewControllerManagedBy(mgr).
  For(&nodesv1.NodePool{}).
  Complete(r)
}

其中 NewControllerManagedBy是一個建造者模式,返回的是一個 builder 對象,其包含了用於構建的 ForOwnsWatchesWithEventFilter等方法

這裏我們就可以利用 ``Watches方法來監聽 Node 的變化,我們這裏使用handler.Funcs` 自定義了一個入隊器

監聽 Node 對象的更新事件,如果存在和 NodePool 關聯的 node 對象更新就把對應的 NodePool 入隊

// SetupWithManager sets up the controller with the Manager.
func (r *NodePoolReconciler) SetupWithManager(mgr ctrl.Manager) error {
 return ctrl.NewControllerManagedBy(mgr).
  For(&nodesv1.NodePool{}).
  Watches(&source.Kind{Type: &corev1.Node{}}, handler.Funcs{UpdateFunc: r.nodeUpdateHandler}).
  Complete(r)
}

func (r *NodePoolReconciler) nodeUpdateHandler(e event.UpdateEvent, q workqueue.RateLimitingInterface) {
 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 defer cancel()

 oldPool, err := r.getNodePoolByLabels(ctx, e.ObjectOld.GetLabels())
 if err != nil {
  r.Log.Error(err, "get node pool err")
 }
 if oldPool != nil {
  q.Add(reconcile.Request{
   NamespacedName: types.NamespacedName{Name: oldPool.Name},
  })
 }

 newPool, err := r.getNodePoolByLabels(ctx, e.ObjectOld.GetLabels())
 if err != nil {
  r.Log.Error(err, "get node pool err")
 }
 if newPool != nil {
  q.Add(reconcile.Request{
   NamespacedName: types.NamespacedName{Name: newPool.Name},
  })
 }
}

func (r *NodePoolReconciler) getNodePoolByLabels(ctx context.Context, labels map[string]string) (*nodesv1.NodePool, error) {
 pool := &nodesv1.NodePool{}
 for k := range labels {
  ss := strings.Split(k, "node-role.kubernetes.io/")
  if len(ss) != 2 {
   continue
  }
  err := r.Client.Get(ctx, types.NamespacedName{Name: ss[1]}, pool)
  if err == nil {
   return pool, nil
  }

  if client.IgnoreNotFound(err) != nil {
   return nil, err
  }
 }
 return nil, nil
}

總結

今天我們完善了 status & event 和自定義對象 watch 下一篇我們看一下如何對我們的 Operator 進行測試

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/JuRlClCX7TnQKaW-Rr3Z7g