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

# はじめに
この記事は Kubernetes3 Advent Calendar 2019 の16日目の記事です。
Go6のCalendarに投稿した以下の記事の内容を変更してクロスポストさせて頂いています。
 [Go勉強(4)　kubernetes client-goでPodのwatcher（TUI）を書いてみる](https://qiita.com/oruharo/items/668d708cead0ad261346)

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

# Source code
https://gist.github.com/oruharo/11abc53c9ad324522fe5b6bdc6620323#file-k8watcher2-go


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

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

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

```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の監視です。

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

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

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

```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の箱部分はイベントを受けてから描画します。（後述）

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

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

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

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

```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

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



# 終わりに

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