编写 kubectl 插件

最近忙的晕头转向,博客停更了 1 个月,感觉对不起党、对不起人民、对不起 CCAV…不过在忙的时候操作 Kubernetes 集群要频繁的使用 kubectl 命令,而在多个 NameSpace 下来回切换每次都得加个 -n 简直让我想打人;索性翻了下 kubectl 的插件机制,顺便写了一个快速切换 NameSpace 的小插件,以下记录一下插件编写过程

一、插件介绍

kubectl 命令从 v1.8.0 版本开始引入了 alpha feature 的插件机制;在此机制下我们可以对 kubectl 命令进行扩展,从而编写一些自己的插件集成进 kubectl 命令中;**kubectl 插件机制是与语言无关的,也就是说你可以用任何语言编写插件,可以是 bashpython 脚本,也可以是 gojava 等编译型语言;所以选择你熟悉的语言即可**,以下是一个用 go 编写的用于快速切换 NameSpace 的小插件,运行截图如下:

所谓: 开局一张图,功能全靠编 😂
swns.gif

当前插件代码放在 mritd/swns 这个项目下面

二、插件加载

kubectl 插件机制目前并不提供包管理器一样的功能,比如你想执行 kuebctl plugin install xxx 这种操作目前还没有实现(个人感觉差个规范);所以一旦我们编写或者下载一个插件后,我们只有正确放在特定目录才会生效;

目前插件根据文档描述只有两部分内容: plugin.yaml 和其依赖的二进制/脚本等可执行文件;根据文档说明,kubectl 会尝试在如下位置查找并加载插件,所以我们只需要将 plugin.yaml 和相关二进制放在在对应位置即可:

  • ${KUBECTL_PLUGINS_PATH}: 如果这个环境变量定义了,那么 kubectl 只会从这里查找;注意: 这个变量可以是多个目录,类似 PATH 变量一样,做好分割即可
  • ${XDG_DATA_DIRS}/kubectl/plugins: 关于这个变量具体请看 XDG System Directory Structure,我了解也不多;如果这个变量没定义则默认为 /usr/local/share:/usr/share
  • ~/.kube/plugins: 这个没啥可说的,我推荐还是将插件放在这个位置比较友好一点

所以最终插件目录结构类似这样:

1
2
3
4
5
6
7
➜  ~ tree .kube
.kube
├── config
└── plugins
└── swns
├── plugin.yaml
└── swns

三、Plugin.yaml

plugin.yaml 这个文件实际上才是插件的核心,在这个文件里声明了插件如何使用、调用的二进制/脚本等重要配置;一个插件可以没有任何脚本/二进制可执行文件,但至少应当有一个 plugin.yaml 描述文件;目前 plugin.yaml 的结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
name: "targaryen"                 # 必填项: 用于 kuebctl 调用的插件名称
shortDesc: "Dragonized plugin" # 必填项: 用于 help 该插件时的简短描述
longDesc: "" # 非必填: 插件的长描述
example: "" # 非必填: 插件的使用样例
command: "./dracarys" # 必填项: 插件实际执行的文件位置,可以相对路径 or 绝对路径,或者在 PATH 里也行
flags: # 非必填: 插件支持的 flag
- name: "heat" # 必填项: 如果你写了支持的 flag,那么此项必填
shorthand: "h" # 非必填: 该选项的缩短形式
desc: "Fire heat" # 必填项: 同样每个 flag 都必须书写描述
defValue: "extreme" # 非必填: 默认值
tree: # 允许定义一些子命令
- ... # 子命令支持同样的设置属性(我想知道子命令的子命令的子命令支不支持...我还没去试过)

四、插件环境变量

在编写插件时,有时插件运行时需要获取到一些参数,比如 kubectl 执行时的全局 flag 等,为了方便插件开发者,kuebctl 的插件机制提供一些预置的环境变量方便我们读取;即如果你用 bash 写插件,那么这些变量你只需要 ${xxxx} 即可拿到,然后做一些你想做的事情;这些变量目前支持如下:

  • KUBECTL_PLUGINS_CALLER: kubectl 二进制文件所在位置;作为插件编写者,我们无需关系 api server 是否能联通,因为配置是否正确应当由使用者决定;在需要时我们只需要直接调用 kubectl 即可;比如在 bash 脚本中执行 get pod
  • KUBECTL_PLUGINS_CURRENT_NAMESPACE: 当前 kuebctl 命令所对应的 NameSpace,插件机制确保了该值一定正确;即这是经过解析了 --namespace 选项或者 kubeconfig 配置后的最终结果;作为插件编写者,我们无需关心处理过程;想详细了解的的可以去看源码,以及 Cobra 库(Kubernetes 用这个库解析命令行参数和配置)
  • KUBECTL_PLUGINS_DESCRIPTOR_*: 插件自己本身位于 plugin.yaml 中的描述信息,比如 KUBECTL_PLUGINS_DESCRIPTOR_NAME 输出 plugin.yaml 下的 name 属性;一般可以用作插件输出自己的帮助文档等
  • KUBECTL_PLUGINS_GLOBAL_FLAG_*: 获取 kubectl 所有全局 flag 值的变量,比如 KUBECTL_PLUGINS_GLOBAL_FLAG_NAMESPACE 能拿到 --namespace 选项的值
  • KUBECTL_PLUGINS_LOCAL_FLAG_*: 同上面类似,只不过这个是获取插件自己本身 flag 的值,个人认为在脚本语言中,比如 bash 等处理选项不怎么好用时,可以考虑直接从变量拿

以上变量我并未都测试,具体以测试为准,删库跑路等情况本人概不负责

五、写一个切换 NameSpace 的插件

前面墨迹一大堆只是为了描述清楚 要写一个插件应该怎么干 的问题,下面开始 这么干

5.1、编写配置

上面已经介绍好了 plugin.yaml 怎么写,那么根据我自己的需求,我写的这个切换 NameSpace 插件的名字暂且叫做 swns;我希望 swns 执行后接受一个 NameSpace 的字符串,然后调用 kuebctl config 去设置当前默认的 NameSpace,这样在后续命令中我就不用再一直加个 -n xxx 参数了;同时我希望使用更方便点,当执行 swns 命令时,如果不提供 NameSpace 的字符串,那我就弹出下拉列表供用户选择;综上需求自己想明白后,就写一个 plugin.yaml,如下:

1
2
3
4
5
name: "swns"
shortDesc: "Switch NameSpace"
longDesc: "Switch Kubernetes current context namespace."
example: "kubectl plugin swns [NAMESPACE]"
command: "./swns"

5.2、编写插件

上面 plugin.yaml 已经定义好了,那么接下来就简单了,撸代码实现了就好;代码如下:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// 注意: 下面的模板语法大括号中间没有空格,此处空格是为了防止博客渲染出错

package main

import (
"fmt"
"os"
"os/exec"
"strings"

"github.com/mritd/promptx"
)

func main() {

// 先拿到当前的 context
cmd := exec.Command("kubectl", "config", "current-context")
cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr

b, err := cmd.Output()
checkAndExit(err)
currentContext := strings.TrimSpace(string(b))

// 如果提供了 NameSpace 字符串,我直接改就行了
if len(os.Args) > 1 {
cmd = exec.Command("kubectl", "config", "set-context", currentContext, "--namespace="+os.Args[1])
cmd.Stdout = os.Stdout
checkAndExit(cmd.Run())
fmt.Printf("Kubernetes namespace switch to %s.\n", os.Args[1])
} else {
// 没提供我就得先把所有的 NameSpace 弄出来
cmd = exec.Command("kubectl", "get", "ns", "-o", "template", "--template", "{ { range .items } }{ { .metadata.name } } { { end } }")
b, err = cmd.Output()
checkAndExit(err)
allNameSpace := strings.Fields(string(b))

// 弄到所有的 NameSpace 后,我在弄一个下拉列表(这是我自己造的一个下拉列表库)
cfg := &promptx.SelectConfig{
ActiveTpl: "» { { . | cyan } }",
InactiveTpl: " { { . | white } }",
SelectPrompt: "NameSpace",
SelectedTpl: "{ { \"» \" | green } }{ {\"NameSpace:\" | cyan } } { { . } }",
DisPlaySize: 9,
DetailsTpl: ` `,
}
s := &promptx.Select{
Items: allNameSpace,
Config: cfg,
}

// 用户选中一个 NameSpace 后我就拿到了想要设置的 NameSpace 字符串
selectNameSpace := allNameSpace[s.Run()]

// 跟上面套路一样,写进去就行了
cmd = exec.Command("kubectl", "config", "set-context", currentContext, "--namespace="+selectNameSpace)
cmd.Stdout = os.Stdout
checkAndExit(cmd.Run())
fmt.Printf("Kubernetes namespace switch to %s.\n", selectNameSpace)
}
}

func checkErr(err error) bool {
if err != nil {
fmt.Println(err)
return false
}
return true
}

func checkAndExit(err error) {
if !checkErr(err) {
os.Exit(1)
}
}

最后编译后放到上面所说的插件加载目录即可

到此,**”全局一张图,功能全靠编”** 图上面也有了,编的的也差不多 😂


本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 国际许可协议进行许可,转载请注明出处。