我先前一篇博客其实介绍了另一种Dashboard提供方案, 当时的方案中, 没有涉及到Dashboard的二次开发, 先前的方案适合rbac可用的集群, 当前的这个方案适用性更广, 你仅需要有一个kubeconfig文件, 就可以提供一个 权限控制完善的Dashboard提供用户使用.

Dashboard本身的权限控制功能太弱了, 而前端的页面多而复杂, 想要结合到我们平台上, 需要精简一下前端, 然后改变后端的鉴权方式, 整个过程的修改应该尽量的小, 以便我们日后能随着Dashboard的更新来更新, 不然某天出现了安全问题需要更新就很难打patch了.

本次修改不需要你提前会用Dashboard, 但是要对K8s的API有一定的了解, 然后再读一下Dashboard的代码. 它的前端是Angular, 后端是Golang.

一般写的不错的代码, 风格都差不多, 大多都践行了表驱动法和中间件的思想. 前端页面中的配置文件肯定时放在一起的, 后端肯定是有中间件的, 我们添加鉴权模块就可以了.

先把程序跑起来

首先你要由一个能用的kubeconfig文件, 保证自己的kubectl操作可用.

我的上一篇博客中介绍了kubectl proxy的使用, 如果你有兴趣可以读一下, 相信你对Kubernetes API的概念 会有更深的理解.

前端

前端运行起来比较简单

1
> yarn start:frontend

打开websocket支持

有一点你要注意, 默认去读的配置aio/proxy.conf.json中, 没有显式的开启websocket, 所以shell页面不是的使用方式不是长连接, 如果你想在本地调试时也使用全功能的webshell, 可以改成下面这样.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"/api": {
"target": "http://localhost:9090",
"secure": false,
"ws": true,
"changeOrigin": true
},
"/config": {
"target": "http://localhost:9090",
"secure": false,
"ws": true,
"changeOrigin": true
}
}

后端

我个人不太喜欢Golang一定要编译出二进制文件才能运行, 一般都是go run main.go直接来跑的.

1
2
3
4
5
> cd src/app/backend/
> go run dashboard.go --kubeconfig /home/corvo/.kube/config \
--metrics-provider none \
--disable-settings-authorizer \
--insecure-port 9090

然后打开前端页面, 应该是有个全功能的Dashboard.

前端页面裁剪

侧边栏裁剪

具体的代码在src/app/frontend/chrome/nav/template.html中, 可以调整顺序, 并且把一些不需要的组件去掉

1
2
3
4
5
6
7
8
9
10
11
12
<kd-nav-item class="kd-nav-item"
state="/cronjob"
id="nav-cronjob"
i18n>Cron Jobs
</kd-nav-item>
<!--
<kd-nav-item class="kd-nav-item"
state="/daemonset"
id="nav-daemonset"
i18n>Daemon Sets
</kd-nav-item>
-->

精简serach功能

因为搜索框的触发时, 会搜索所有给定的资源, 因为我们单个NS的pods可能会很多, 会导致搜索框的效率缓慢, 因此, 将搜索逻辑的某些组件也隐藏掉, 当然也可以在这里调整显示顺序

具体的代码在src/app/frontend/search/template.html

1
2
3
4
5
6
7
<kd-job-list (onchange)="onListUpdate($event)"
[hideable]="true"></kd-job-list>
<!--
<kd-daemon-set-list (onchange)="onListUpdate($event)"
[hideable]="true"></kd-daemon-set-list>
<kd-pod-list (onchange)="onListUpdate($event)"
[hideable]="true"></kd-pod-list>

后端增加鉴权

鉴权位置

dashboard的后端使用了go-restful这个库, 然后我们找一下它是如何增加中间件的, 它有filter的概念,

1
2
3
4
5
6
7
8
// 代码来自 https://github.com/emicklei/go-restful/blob/v3/examples/filters/restful-filters.go
restful.Filter(globalLogging)

// Global Filter
func globalLogging(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) {
log.Printf("[global-filter (logger)] %s,%s\n", req.Request.Method, req.Request.URL)
chain.ProcessFilter(req, resp)
}

我们的目的就是在dashboard后端增加几个全局的filter, 进行鉴权以及用户事件记录.

再看下dashboard代码位置, 我们针对所有的API做一次再鉴权就可以了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// src/app/backend/handler/apihandler.go

// CreateHTTPAPIHandler creates a new HTTP handler that handles all requests to the API of the backend.
func CreateHTTPAPIHandler(iManager integration.IntegrationManager, cManager clientapi.ClientManager,
authManager authApi.AuthManager, sManager settingsApi.SettingsManager,
sbManager systembanner.SystemBannerManager) (http.Handler, error) {

apiV1Ws := new(restful.WebService)
}

// src/app/backend/handler/filter.go
func InstallFilters(ws *restful.WebService, manager clientapi.ClientManager) {
ws.Filter(requestAndResponseLogger)
ws.Filter(metricsFilter)
ws.Filter(validateXSRFFilter(manager.CSRFKey()))
ws.Filter(restrictedResourcesFilter)
}

鉴权方案

我们是用OpenID做登录的, 但是我又不想去开发这个登录模块了, 所以复用了keycloak-gatekeeper, 最近再看的时候已经停止维护了…

它的功能主要是过滤非登录用户, 同时给后台传递登录用户的具体信息, 比如邮箱或是用户名. 根据邮箱, 用户名, 判断当前请求中的namespace以及api是否允许用户访问.

20210909211333

kubeconfig配置自动刷新

我们所使用的Kubernetes集群中, kubeconfig的token是会过期的, 需要我们定期去刷新. 这个涉及到Dashboard的另一项改动,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// src/app/backend/dashboard.go
// 此处根据我们传递的kubeconfig文件生成管理器, 不过这个管理器功能太弱了
clientManager := client.NewClientManager(args.Holder.GetKubeConfigFile(), args.Holder.GetApiServerHost())

// app/backend/client/manager.go
// 这里一旦初始化之后, 里面的配置就不能改变了
func NewClientManager(kubeConfigPath, apiserverHost string) clientapi.ClientManager {
result := &clientManager{
kubeConfigPath: kubeConfigPath,
apiserverHost: apiserverHost,
}

result.init()
return result
}

我为了能够按照自己的意愿更新kubeconfig, 因此新增了一个生成器,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func NewXXXClientManager(kubeConfigPath, apiserverHost string) clientapi.ClientManager {
// 你可以认为这个类继承了上面的类, 然后, 我们在初始化的时候多一个定时器, 定时刷新配置
result := &xxxClientManager{
cm: clientManager{
kubeConfigPath: kubeConfigPath,
apiserverHost: apiserverHost,
},
rawConfigPath: kubeConfigPath,
}
result.init()
return result
}

func (icm *xxxClientManager) init() {
icm.refresh() // 首先刷新一次配置
tickRefresh := time.Tick(8 * time.Hour)
go func() {

for {
select {
case <-tickRefresh:
icm.refresh()
}
}
}()
}

总结

我只是分享下我们的方案, 希望维护PaaS平台的各位由所收获吧. 我个人感觉开发能力比较足的PaaS平台, 都直接内嵌WebShell了, 我们实在没什么前端, 所以就拼凑一下了, 目前方案稳定运行半个月, 稳定性方面应该没什么问题..