k9s项目pod视图源码分析
k9s
终端可视化的k8s管理工具
核心组件
- 快速命令行应用 github.com/spf13/cobra
- 终端彩色显示 github.com/mattn/go-colorable
- 配置信息存/取 github.com/adrg/xdg
- 终端ui
- k8s client
目录结构
├─cmd //命令入口
├─internal
│ ├─client //**操作客户端
│ ├─color //调色盘-存放一些常用的颜色
│ ├─config //各种配置信息,可以读取文件/环境变量 (默认视图、主题样式、日志格式、快捷键、插件等)
│ ├─dao //**类似操作数据库获取数据
│ ├─health //资源健康检测记录
│ ├─model //** 数据结构的深层封装(定义一些监听事件、刷新事件、过滤方法、其它辅助方法)
│ ├─perf //Benchmark性能测试
│ ├─port //端口转发
│ ├─render //**生成表头(header),渲染每一行给定的数据(render)
│ ├─tchart //自定义图表 tview 组件
│ ├─ui //** 自封装的ui组件(菜单、主视图、logo、路由、下拉选择、表格、树形、面包屑) 并包含了样式变动的监听事件(StylesChanged) 部分包含updete(更新显示)、部分实现了StackListener接口
│ │ └─dialog //封装的弹窗
│ ├─view //**各种资源的表格视图
│ ├─watch //k8s informer
│ └─xray //**各种资源的树形依赖视图 渲染树形节点(Render)
├─plugins //扩展插件
│ ├─kubectl
│ └─kubectl-plugins
└─skins //适配的皮肤样式
流程代码分析
- 程序入口
func main() {
// cmd/root.go:56
// run
cmd.Execute()
}
- 默认命令 k9s 入口
func run(cmd *cobra.Command, args []string) {
// 1. 确保日志目录存在
config.EnsurePath(*k9sFlags.LogFile, config.DefaultDirMod)
// 2. 日志初始化
f, _ := os.Create("app.log")
log.Logger = log.Output(zerolog.ConsoleWriter{Out: f})
zerolog.SetGlobalLevel(zerolog.DebugLevel)
// 3. 加载配置信息
// 4. 实例化ViewApp
app := view.NewApp(loadConfiguration())
// 5. 初始化应用-go
if err := app.Init(version, *k9sFlags.RefreshRate); err != nil {
panic(fmt.Sprintf("app init failed -- %v", err))
}
// 6. 事件循环-go
if err := app.Run(); err != nil {
panic(fmt.Sprintf("app run failed %v", err))
}
if view.ExitStatus != "" {
panic(fmt.Sprintf("view exit status %s", view.ExitStatus))
}
}
- 初始化
func (a *App) Init(version string, rate int) error {
a.version = model.NormalizeVersion(version)
ctx := context.WithValue(context.Background(), internal.KeyApp, a)
// 5.1 视图页面堆栈初始化
// Content PageStack 每次跳转的时候会调用 Start 开启视图页面内的数据刷新
if err := a.Content.Init(ctx); err != nil {
return err
}
// 5.2 将面包屑导航和菜单导航都加入监听事件中
// 每次跳转都会触发更新
a.Content.Stack.AddListener(a.Crumbs())
a.Content.Stack.AddListener(a.Menu())
// 5.3 UI APP初始化
// 设置按键绑定、视图监听、样式监听、Tview设置主页面
a.App.Init()
// 5.4 捕获全局按键事件
a.SetInputCapture(a.keyboard)
// 5.5 全局按键事件注册和5.3中类似,当按键不同
a.bindKeys()
if a.Conn() == nil {
return errors.New("No client connection detected")
}
// 5.6 获取当前命名空间
ns := a.Config.ActiveNamespace()
// 5.7 创建k8s informer
a.factory = watch.NewFactory(a.Conn())
ok, err := a.isValidNS(ns)
if !ok && err == nil {
return fmt.Errorf("Invalid namespace %s", ns)
}
// 5.8 初始化 informer 加载各种资源信息
a.initFactory(ns)
// 5.9 辅助信息监听刷新
a.clusterModel = model.NewClusterInfo(a.factory, a.version)
a.clusterModel.AddListener(a.clusterInfo())
a.clusterModel.AddListener(a.statusIndicator())
if a.Conn().ConnectionOK() {
a.clusterModel.Refresh()
a.clusterInfo().Init()
}
// 5.9 实例并初始化命令-go
a.command = NewCommand(a)
if err := a.command.Init(); err != nil {
return err
}
// 5.10 输入命令时提示
a.CmdBuff().SetSuggestionFn(a.suggestCommand())
// 5.11 初始化页面布局
a.layout(ctx)
// 5.12 监听结束退出信号
a.initSignals()
return nil
}
- 初始化命令
// Init initializes the command.
func (c *Command) Init() error {
// 5.9.1 初始化
c.alias = dao.NewAlias(c.app.factory)
// 5.9.2 定义资源组
if _, err := c.alias.Ensure(); err != nil {
log.Error().Err(err).Msgf("command init failed!")
return err
}
// 5.9.3 注册子页面 组/版本/资源 -视图
// 一个接口:包含 viewerFn 实例化视图页面
// enterFn 在这个页面里,回车确认时执行的方法
customViewers = loadCustomViewers()
return nil
}
- 事件循环
// Run starts the application loop.
func (a *App) Run() error {
// 6.1 初次触发当前显示页面的 Start 刷新数据
a.Resume()
go func() {
<-time.After(splashDelay)
a.QueueUpdateDraw(func() {
a.Main.SwitchToPage("main")
})
}()
// 6.2 执行第一个命令 -go
if err := a.command.defaultCmd(); err != nil {
return err
}
a.SetRunning(true)
// 6.3 事件循环交互 Tview 自带
if err := a.Application.Run(); err != nil {
return err
}
return nil
}
- 默认执行的命令
func (c *Command) defaultCmd() error {
// 6.2.1 k8s连接失败,结束
if !c.app.Conn().ConnectionOK() {
return c.run("ctx", "", true)
}
// 6.2.2 获取默认页面视图 po
view := c.app.Config.ActiveView()
if view == "" { //和默认一个效果
return c.run("pod", "", true)
}
tokens := strings.Split(view, " ")
cmd := view
if len(tokens) == 1 {
if !isContextCmd(tokens[0]) {
cmd = tokens[0] + " " + c.app.Config.ActiveNamespace()
}
}
// 6.2.3 切换页面视图 po all
if err := c.run(cmd, "", true); err != nil {
log.Error().Err(err).Msgf("Default run command failed %q", cmd)
c.app.cowCmd(err.Error())
return err
}
return nil
}
- 执行命令
// Exec the Command by showing associated display.
// 切换页面
func (c *Command) run(cmd, path string, clearStack bool) error {
// 6.2.3.1 判断是否是特殊命令并执行
if c.specialCmd(cmd, path) {
return nil
}
cmds := strings.Split(cmd, " ")
// 6.2.3.2 通过命令获取标准资源表示方式 组/版本/资源
// 通过标准表示方式获取注册的视图页面接口变量值
// 一个接口:包含 viewerFn 实例化视图页面
// enterFn 在这个页面里,回车确认时执行的方法
gvr, v, err := c.viewMetaFor(cmds[0])
if err != nil {
return err
}
// 执行命令
// 6.2.3.5 c.componentFor(gvr, path, v) 使用 MetaViewer 接口的 viewerFn 方法实列化视图组件
// gvr v1/pods
return c.exec(cmd, gvr, c.componentFor(gvr, path, v), clearStack)
}
- 执行命令
func (c *Command) exec(cmd, gvr string, comp model.Component, clearStack bool) (err error) {
// 6.2.3.5.1 激活当前视图
c.app.Config.SetActiveView(cmd)
// 6.2.3.5.2 保存记录
if err := c.app.Config.Save(); err != nil {
log.Error().Err(err).Msg("Config save failed!")
}
if clearStack {
c.app.Content.Stack.Clear()
}
// 6.2.3.5.3 注入当前页面视图组件到 main 主视图
if err := c.app.inject(comp); err != nil {
return err
}
// 6.2.3.5.4 记录历史命令更好的提示或返回返回页面
c.app.cmdHistory.Push(cmd)
return
}
- 页面跳转
// 初始化子组件并进入
func (a *App) inject(c model.Component) error {
ctx := context.WithValue(context.Background(), internal.KeyApp, a)
// 6.2.3.5.3.1 具体页面初始化
// 注册页面按键子命令
// folder view file:browser.go l:46
if err := c.Init(ctx); err != nil {
log.Error().Err(err).Msgf("component init failed for %q", c.Name())
dialog.ShowError(a.Styles.Dialog(), a.Content.Pages, err.Error())
}
// 6.2.3.5.3.2 类似 vue-router push() 切换路由 并会调用 c.Start()开启数据刷新
a.Content.Push(c)
return nil
}