一篇帶你kubebuilder 實戰(zhàn): status & event
在上篇文章當(dāng)中我們實現(xiàn)了 NodePool Operator 基本的 CURD 功能,跑了一小段時間之后除了 CURD 之外我們有了更高的需求,想知道一個節(jié)點池有多少的節(jié)點,現(xiàn)在的資源占比是多少,這樣可以清晰的知道我們現(xiàn)在的水位線是多少,除此之外也想知道節(jié)點池數(shù)量發(fā)生變化的相關(guān)事件信息,什么時候節(jié)點池增加或者是減少了一個節(jié)點等。
需求
我們先整理一下需求
能夠通過 kubectl get Nodepool了解當(dāng)前的節(jié)點池的以下信息
- 節(jié)點池的狀態(tài),是否異常
- 節(jié)點池現(xiàn)在包含多少個節(jié)點
- 節(jié)點池的資源情況現(xiàn)在有多少 CPU、Memory
能夠通過事件信息得知 controller 的錯誤情況以及節(jié)點池內(nèi)節(jié)點的變化情況
實現(xiàn)
Status
先修改一下 status 對象,注意要確保下面的 //+kubebuilder:subresource:status注釋存在,這個表示開啟 status 子資源,status 對象修改好之后需要重新執(zhí)行一遍 make install
- // NodePoolStatus defines the observed state of NodePool
- type NodePoolStatus struct {
- // status=200 說明正常,其他情況為異常情況
- Status int `json:"status"`
- // 節(jié)點的數(shù)量
- NodeCount int `json:"nodeCount"`
- // 允許被調(diào)度的容量
- Allocatable corev1.ResourceList `json:"allocatable,omitempty" protobuf:"bytes,2,rep,name=allocatable,casttype=ResourceList,castkey=ResourceName"`
- }
- //+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
- // 更新節(jié)點的標(biāo)簽和污點信息
- 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 = 200
- + err = 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
可以看到我們現(xiàn)在是有兩個 worker 節(jié)點
- ▶ 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,可以發(fā)現(xiàn)已經(jīng)存在了預(yù)期的 status
- status:
- allocatable:
- cpu: "8"
- ephemeral-storage: 184026512Ki
- hugepages-1Gi: "0"
- hugepages-2Mi: "0"
- memory: 6129040Ki
- pods: "220"
- nodeCount: 2
- status: 200
現(xiàn)在這樣只能通過查看 yaml 詳情才能看到,當(dāng) NodePool 稍微多一些的時候就不太方便,我們現(xiàn)在給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
如上所示只需要添加好對應(yīng)的注釋,然后執(zhí)行 make install即可
然后再執(zhí)行 kubectl get NodePool 就可以看到對應(yīng)的列了
- ▶ kubectl get NodePool
- NAME STATUS NODECOUNT
- worker 200 2
Event
我們在 controller 當(dāng)中添加 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
監(jiān)聽更多資源
之前我們所有的代碼都是圍繞著 NodePool 的變化來展開的,但是我們?nèi)绻薷牧?Node 的相關(guān)標(biāo)簽,將 Node 添加到一個 NodePool,Node 上對應(yīng)的屬性和 NodePool 的 status 信息也不會改變。如果我們想要實現(xiàn)上面的效果就需要監(jiān)聽更多的資源變化。
在 controller 當(dāng)中我們可以看到一個 SetupWithManager方法,這個方法說明了我們需要監(jiān)聽哪些資源的變化
- // 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 對象,其包含了用于構(gòu)建的 For、Owns、Watches、WithEventFilter等方法
這里我們就可以利用 ``Watches方法來監(jiān)聽 Node 的變化,我們這里使用handler.Funcs`自定義了一個入隊器
監(jiān)聽 Node 對象的更新事件,如果存在和 NodePool 關(guān)聯(lián)的 node 對象更新就把對應(yīng)的 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
- }
總結(jié)
今天我們完善了 status & event 和自定義對象 watch 下一篇我們看一下如何對我們的 Operator 進行測試