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

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

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


<details><summary>他の投稿（クリックで開く）</summary>
[Go勉強(1)　mac＋VSCode＋Go環境を設定](https://qiita.com/oruharo/items/545378eae5c707f717ed)
[Go勉強(2)　kubernetes client-goのexamplesをbuildする](https://qiita.com/oruharo/items/261d065340845149b4ce)
[Go勉強(3)　kubernetes client-goのexamplesを読んでみる](https://qiita.com/oruharo/items/8f98e75264b9d6c7df2a)
[Go勉強(4)　kubernetes client-goでPodのwatcher（TUI）を書いてみる]
(https://qiita.com/oruharo/items/668d708cead0ad261346)
[Go勉強(5)　kubernetes client-goでPodのwatcher（TUI）を書いてみる2]
(https://qiita.com/oruharo/items/b7a131bf3eda36dd08a3)
</details>


# Demo
下のターミナルでツールを起動すると、現在の実行ノードとPodの配置が表示されます。
上のターミナルからデプロイすると、デプロイしたPodがツール画面にリアルタイムに反映されます。
ターミナルの画面サイズに応じて、レイアウトが自動的に変わります。
![quita5.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124599/a3afc68b-c072-2a81-c8be-6ba3f52fb545.gif)

### Source code
https://gist.github.com/oruharo/11abc53c9ad324522fe5b6bdc6620323

### 実行方法(macの場合）
```terminal
$ 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`のAPI`cache.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
<img width="983" alt="スクリーンショット 2019-12-11 7.13.04.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124599/5bd14e22-7ab9-469e-69ce-bd2f679587c8.png">
画面上の部品の階層構造をyamlっぽく書くとこんな感じです。

```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)
         :
}
```

#ハマったとこ
### 罫線文字の右横が描画されない
<img width="983" alt="スクリーンショット 2019-12-11 8.04.15.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124599/575f9702-1400-1ca7-176e-03cbbd5b1c8a.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のデモしたりするのにも便利なので、もうちょっと拡張しようと思います。