はじめに
この記事は Go6 Advent Calendar 2019 の12日目の記事です。
Goの勉強記録の一部をAdvent Calenderに投稿させて頂いてます。
※ (12/19)ツールと記事をアップデートしました。
※ (12/29)実行方法に記載したソースのリンクが古いものだったので修正しました。
Kubernetesを操作した時の挙動を観察したくて、ターミナルからコマンドでNode/Podの状態をリアルタイムに表示するツールを書いてみました。
本稿では全体の説明をしたいと思います。 次稿でclient-go
部分について深堀しますので合わせご参照下さい。
他の投稿(クリックで開く)
[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)Demo
下のターミナルでツールを起動すると、現在の実行ノードとPodの配置が表示されます。
上のターミナルからデプロイすると、デプロイしたPodがツール画面にリアルタイムに反映されます。
ターミナルの画面サイズに応じて、レイアウトが自動的に変わります。
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って書くだけ。超簡単)
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)
:
}
#ハマったとこ
罫線文字の右横が描画されない
Tviewから別のライブラリtcellが使われており、さらにライブラリrunewidthが使われています。 runewidth v0.0.4以降を使用すると発生するようですが、三つのライブラリどれがマズいのかはわかりません。ターミナルのロケールをLANG=ja-JP.UTF-8からen-US.UTF-8等に変えれば直ります。
ツールでは対処療法的に以下の一文を追加してrunewidthの動作を誤魔化してます。
runewidth.DefaultCondition.EastAsianWidth = false
メソッドレシーバ
メソッドレシーバと呼ばれる(ui *KubeUI)
の部分。
この*
を抜かして(ui KubeUI)
と書いてたため、uiのコピーが渡されて更新してもオリジナルに反映されず。。
func (ui *KubeUI) AddNode(v1Node *v1.Node) {
↑
}
終わりに
コード書くのに時間かかって、本文が薄めになってしまいました。
作ったツールは前から欲しかった物で、他の人にKubernetesのデモしたりするのにも便利なので、もうちょっと拡張しようと思います。