12
2

More than 3 years have passed since last update.

Go勉強(5) kubernetes client-goでPodのwatcher(TUI)を書いてみる2

Last updated at Posted at 2019-12-15

はじめに

この記事は Kubernetes3 Advent Calendar 2019 の16日目の記事です。
Go6のCalendarに投稿した以下の記事の内容を変更してクロスポストさせて頂いています。
Go勉強(4) kubernetes client-goでPodのwatcher(TUI)を書いてみる

Kubernetesを操作した時の挙動を観察したくて、ターミナルからコマンドでNode/Podの状態をリアルタイムに表示するシンプルなツールを書いてみました。

Source code

Demo

下のターミナルでツールを起動すると、現在の実行ノードとPodの配置が表示されます。
上のターミナルからデプロイすると、デプロイしたPodがツール画面にリアルタイムに反映されます。
ターミナルの画面サイズに応じて、レイアウトが自動的に変わります。
quita5.gif

メイン処理

監視役・イベント・画面の三種類のオブジェクトで構成しています。
監視役KubeWatcherが Channelを介してイベントKubeEventを画面KubeUIに投げます。
監視役は非同期に動作します。(メソッドの前にgoって書くだけ。超簡単:thumbsup_tone2:

func main() {
            : 中略
    //
    //   KubeUi <----------- KubeWatcher
    //            kubeEvent

    eventChan := make(chan *KubeEvent, 10)
    kubeWatcher := NewKubeWatcher(eventChan)
    kubeUI := NewKubeUI(eventChan)

    go kubeWatcher.Run()
    kubeUI.Run()
}

監視役KubeWatcher

client-goのAPIcache.NewInformerを使います。
kubernetesの各種オブジェクトに変化(追加・変更・削除)があるとイベントを送られてきます。

以下はNodeの監視の設定です。 監視の開始はnodesController.Runで行います。(後述)
受け取ったイベントを即チャネルに投げます。

func (kw *KubeWatcher) Run() {
            : 中略
    watchNodes := cache.NewListWatchFromClient(
        clientset.CoreV1().RESTClient(), "nodes", v1.NamespaceAll, fields.Everything())

    _, nodesController := cache.NewInformer(
        watchNodes, &v1.Node{}, 0,
        cache.ResourceEventHandlerFuncs{
            AddFunc:    func(obj interface{}) { kw.Sender <- NewKubeEvent(NodeAdd, obj) },
            DeleteFunc: func(obj interface{}) { kw.Sender <- NewKubeEvent(NodeDelete, obj) },
            UpdateFunc: func(old, new interface{}) { kw.Sender <- NewKubeEvent(NodeUpdate, new) },
        },
    )

次はPodの監視です。

    watchPods := cache.NewListWatchFromClient(
        clientset.CoreV1().RESTClient(), string(v1.ResourcePods), v1.NamespaceAll, fields.Everything())
    _, podsController := cache.NewInformer(
        watchPods, &v1.Pod{}, 0,
        cache.ResourceEventHandlerFuncs{
            AddFunc:    func(obj interface{}) { kw.Sender <- NewKubeEvent(PodAdd, obj) },
            DeleteFunc: func(obj interface{}) { kw.Sender <- NewKubeEvent(PodDelete, obj) },
            UpdateFunc: func(old, new interface{}) { kw.Sender <- NewKubeEvent(PodUpdate, new) },
        },
    )

NodeとPodsの監視を非同期で実行します。
実行中は無限ループで待ちます。 それが中断されたらダミーチャネルのclose処理が走って監視が停止します。

    stop := make(chan struct{})
    defer close(stop)
    go nodesController.Run(stop)
    go podsController.Run(stop)
    for {
        time.Sleep(time.Second)
    }
}

イベントKubeEvent

イベントの種類(Nodeの追加・変更・削除、Podの追加・変更・削除)と変更後のオブジェクトを持ちます。

type KubeEvent struct {
    eventType EventType
    newObj    interface{}
}
type EventType int
const (
    NodeAdd EventType = iota
    NodeUpdate
    NodeDelete
    PodAdd
    PodUpdate
    PodDelete
)

画面KubeUI

TUI用のライブラリTviewを利用させてもらいました。 超簡単にいい感じになります。
https://github.com/rivo/tview
https://godoc.org/github.com/rivo/tview
スクリーンショット 2019-12-11 7.13.04.png
画面上の部品の階層構造をyamlっぽく書くとこんな感じです。

rootView:         # 縦方向のGrid
  headView:       # ヘッダー
  nodeGrid:       # 横方向のGrid      
    - nodeView:     # Nodeの箱
    - nodeView:     #   :
  bottomView:     # フッター

NewメソッドにUIの大枠を実装しています。 Nodeの箱部分はイベントを受けてから描画します。(後述)

func NewKubeUI(rec chan *KubeEvent) *KubeUI {

    runewidth.DefaultCondition.EastAsianWidth = false
    ui := &KubeUI{
        Receiver:        rec,
        Nodes:           []*v1.Node{},
        Pods:            []*v1.Pod{},
        nodeViews:       []*tview.Table{},
        app:             tview.NewApplication(),
        nodeColumnCount: 1,
    }

    rootView := tview.NewGrid().SetRows(3, -1, 2)
    rootView.SetBackgroundColor(tcell.NewHexColor(0xe0e0e0))

    headView := tview.NewTextView()
    headView.SetDynamicColors(true).SetBackgroundColor(tcell.NewHexColor(0x303030))
    headView.SetBorderPadding(1, 1, 2, 0)
    headView.SetText("[#306ee3]⎈ [white]Kubernetes Watcher")

    ui.nodeGrid = tview.NewGrid()
    ui.nodeGrid.SetBorderPadding(1, 1, 2, 2)
    ui.nodeGrid.SetBackgroundColor(tcell.NewHexColor(0xf6f6f4))
    ui.nodeGrid.SetGap(1, 2)

    bottomView := tview.NewTextView()
    bottomView.SetDynamicColors(true).SetBackgroundColor(tcell.NewHexColor(0xe0e0e0))
    bottomView.SetText("[#306ee3] watching...     CTRL+C -> Exit")

    ui.app.SetAfterDrawFunc(func(screen tcell.Screen) {
        ui.drowNodeGrid(false)
    })

    ui.app.SetRoot(
        rootView.
            AddItem(headView, 0, 0, 1, 1, 0, 0, false).
            AddItem(ui.nodeGrid, 1, 0, 1, 1, 0, 0, false).
            AddItem(bottomView, 2, 0, 1, 1, 0, 0, false),
        true,
    )
    return ui
}

イベント受信を非同期で起動しつつ、UIのメインループを実行します。

func (ui *KubeUI) Run() {
    go ui.EventReciever()
    if err := ui.app.Run(); err != nil {
        panic(err)
    }
}

Channelからイベントを受け取って、Node/Pod部分を描画するメソッドを呼び出します。

func (ui *KubeUI) EventReciever() {
    for {
        kubeEvent := <-ui.Receiver
        switch kubeEvent.eventType {
        case NodeAdd:
            ui.AddNode(kubeEvent.newObj.(*v1.Node))
        case NodeUpdate:
            ui.UpdateNode(kubeEvent.newObj.(*v1.Node))
        case NodeDelete:
            ui.RemoveNode(kubeEvent.newObj.(*v1.Node))
        case PodAdd:
            ui.AddPod(kubeEvent.newObj.(*v1.Pod))
        case PodUpdate:
            ui.UpdatePod(kubeEvent.newObj.(*v1.Pod))
        case PodDelete:
            ui.RemovePod(kubeEvent.newObj.(*v1.Pod))
        }
    }
}

Nodeを追加する部分はこんな感じです。
非同期に他のgoroutineから処理するためtview.Applicationapp.Draw()メソッドで強制的に描画してます。
https://godoc.org/github.com/rivo/tview#hdr-Concurrency を参考にしました。

func (ui *KubeUI) AddNode(v1Node *v1.Node) {
    nodeView := tview.NewTable()
    nodeView.SetBackgroundColor(tcell.NewHexColor(0x454545))
    nodeView.Select(0, 0).SetFixed(1, 1).SetSelectable(true, false)
    nodeView.SetBorder(true).SetBorderPadding(1, 1, 1, 1)
    for _, nodeAddress := range v1Node.Status.Addresses {
        if nodeAddress.Type == v1.NodeInternalIP {
            nodeView.SetTitleAlign(tview.AlignLeft).SetTitle(nodeAddress.Address)
            break
        }
    }
    ui.Nodes = append(ui.Nodes, v1Node)
    ui.nodeViews = append(ui.nodeViews, nodeView)
    ui.drawPods(v1Node.Name)
    ui.drowNodeGrid(true)
}

Podのステータスを判定する部分です。
こちらはKubectlのソースコードを参考に実装しました。
https://github.com/kubernetes/kubernetes/blob/master/pkg/printers/internalversion/printers.go#printPod

func podStats(pod *v1.Pod) string {
    reason := string(pod.Status.Phase)
         :
}

終わりに

Informer辺りを詳しく調べたかったのですが間に合わず。。

12
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
2