はじめに
この記事は Kubernetes3 Advent Calendar 2019 の16日目の記事です。
Go6のCalendarに投稿した以下の記事の内容を変更してクロスポストさせて頂いています。
Go勉強(4) kubernetes client-goでPodのwatcher(TUI)を書いてみる
Kubernetesを操作した時の挙動を観察したくて、ターミナルからコマンドでNode/Podの状態をリアルタイムに表示するシンプルなツールを書いてみました。
Source code
Demo
下のターミナルでツールを起動すると、現在の実行ノードとPodの配置が表示されます。
上のターミナルからデプロイすると、デプロイしたPodがツール画面にリアルタイムに反映されます。
ターミナルの画面サイズに応じて、レイアウトが自動的に変わります。
メイン処理
監視役・イベント・画面の三種類のオブジェクトで構成しています。
監視役KubeWatcher
が Channelを介してイベントKubeEvent
を画面KubeUI
に投げます。
監視役は非同期に動作します。(メソッドの前に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
で行います。(後述)
受け取ったイベントを即チャネルに投げます。
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
画面上の部品の階層構造を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辺りを詳しく調べたかったのですが間に合わず。。