LoginSignup
11
2

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-12-11

はじめに

この記事は Go6 Advent Calendar 2019 の12日目の記事です。
Goの勉強記録の一部をAdvent Calenderに投稿させて頂いてます。
※ (12/19)ツールと記事をアップデートしました。
※ (12/29)実行方法に記載したソースのリンクが古いものだったので修正しました。

Kubernetesを操作した時の挙動を観察したくて、ターミナルからコマンドでNode/Podの状態をリアルタイムに表示するツールを書いてみました。
本稿では全体の説明をしたいと思います。 次稿client-go部分について深堀しますので合わせご参照下さい。

他の投稿(クリックで開く)
Go勉強(1) mac+VSCode+Go環境を設定
Go勉強(2) kubernetes client-goのexamplesをbuildする
Go勉強(3) kubernetes client-goのexamplesを読んでみる
Go勉強(4) kubernetes client-goでPodのwatcher(TUI)を書いてみる
Go勉強(5) kubernetes client-goでPodのwatcher(TUI)を書いてみる2

Demo

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

Source code

実行方法(macの場合)

$ mkdir $HOME/go/src/k8watcher
$ cd    $HOME/go/src/k8watcher
$ curl -O https://gist.githubusercontent.com/oruharo/11abc53c9ad324522fe5b6bdc6620323/raw/615d474595474375da6b44474d88c2a95763e18d/k8watcher.go
$ go mod init
$ go get k8s.io/client-go@master
$ #ビルドせずに実行
$ go run k8watcher
$ #ビルドして実行
$ go build -o k8watcher .
$ ./k8watcher

メイン処理

軽く説明していきます。

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

k8watcher.go
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で行います。(後述)
受け取ったイベントを即チャネルに投げます。

k8watcher.go
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の監視です。

k8watcher.go
    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処理が走って監視が停止します。

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

イベントKubeEvent

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

k8watcher.go
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の箱部分はイベントを受けてから描画します。(後述)

k8watcher.go
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のメインループを実行します。

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

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

k8watcher.go
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 を参考にしました。

k8watcher.go
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

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

ハマったとこ

罫線文字の右横が描画されない

スクリーンショット 2019-12-11 8.04.15.png
Tviewから別のライブラリtcellが使われており、さらにライブラリrunewidthが使われています。
runewidth v0.0.4以降を使用すると発生するようですが、三つのライブラリどれがマズいのかはわかりません。

ターミナルのロケールをLANG=ja-JP.UTF-8からen-US.UTF-8等に変えれば直ります。
ツールでは対処療法的に以下の一文を追加してrunewidthの動作を誤魔化してます。

k8watcher.go
runewidth.DefaultCondition.EastAsianWidth = false

メソッドレシーバ

メソッドレシーバと呼ばれる(ui *KubeUI)の部分。
この*を抜かして(ui KubeUI)と書いてたため、uiのコピーが渡されて更新してもオリジナルに反映されず。。

k8watcher.go
func (ui *KubeUI) AddNode(v1Node *v1.Node) {
         
}

終わりに

コード書くのに時間かかって、本文が薄めになってしまいました。
作ったツールは前から欲しかった物で、他の人にKubernetesのデモしたりするのにも便利なので、もうちょっと拡張しようと思います。

11
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
11
2