k8s Krew 插件开发详解
前言:前面我们已经了解过krew插件的作用以及一些比较常用的插件,接下来我们讲一下如何开发krew插件。
1、熟悉kubens 插件
在开发krew插件之前,我们先通过kubens插件来熟悉krew,我们知道kubens是用来切换命名空间的
$ kubectl-ns # 什么参数都不加,默认是输出所有命名空间并高亮显示当前命名空间
default
flink
kube-public
kube-system
$ kubectl-ns flink # 切换命名空间到flink
Context "kubernetes-admin@kubernetes" modified.
Active namespace is "flink".
$ kubectl-ns -c # 打印当前配置的命名空间
flink
$ kubectl-ns - # 切换回之前的命名空间
Context "kubernetes-admin@kubernetes" modified.
Active namespace is "default".
2、kubens 插件源码解析
我们要开发krew插件,一个很简单的途径就是学习已经写好的比较简单的krew插件,kubens插件是一个使用最普及的插件之一,此插件是用来切换kubeconfig来切换配置的命名空间,相对来说也是比较简单的,我们可以看一下此插件的源代码,照葫芦画瓢来学习开发krew插件。
此插件大部分也是使用go进行编写的,按照go语言cli的惯例,主体代码是存放在 cmd/kubens/main.go
package main
import (
"fmt"
"io"
"os"
"github.com/ahmetb/kubectx/internal/cmdutil"
"github.com/ahmetb/kubectx/internal/env"
"github.com/ahmetb/kubectx/internal/printer"
"github.com/fatih/color"
)
type Op interface {
Run(stdout, stderr io.Writer) error
}
func main() {
cmdutil.PrintDeprecatedEnvWarnings(color.Error, os.Environ())
op := parseArgs(os.Args[1:])
if err := op.Run(color.Output, color.Error); err != nil {
printer.Error(color.Error, err.Error())
if _, ok := os.LookupEnv(env.EnvDebug); ok {
// print stack trace in verbose mode
fmt.Fprintf(color.Error, "[DEBUG] error: %+v\n", err)
}
defer os.Exit(1)
}
}
代码先定义了一个 Op 接口,此接口只有一个 Run方法,熟悉go的应该都知道,只要实现了Run方法就是Op接口。此接口也是代码的主要接口,Run方法就是代码的主要方法。通过op.Run(color.Output, color.Error)我们可以知道op变量就是一个Op接口,parseArgs函数执行过之后会返回一个Op接口。通过parseArgs函数传参 os.Args[1:] 我们可以知道parseArgs函数接收了我们执行命令时传入的除了命令本身之外的所有参数。接下来我们看一下parseArgs函数的代码。
func parseArgs(argv []string) Op {
if len(argv) == 0 {
if cmdutil.IsInteractiveMode(os.Stdout) {
return InteractiveSwitchOp{SelfCmd: os.Args[0]}
}
return ListOp{}
}
if len(argv) == 1 {
v := argv[0]
if v == "--help" || v == "-h" {
return HelpOp{}
}
if v == "--version" || v == "-V" {
return VersionOp{}
}
if v == "--current" || v == "-c" {
return CurrentOp{}
}
if strings.HasPrefix(v, "-") && v != "-" {
return UnsupportedOp{Err: fmt.Errorf("unsupported option '%s'", v)}
}
return SwitchOp{Target: argv[0]}
}
return UnsupportedOp{Err: fmt.Errorf("too many arguments")}
}
if len(argv) == 0 代码首先判断了除了命令本身之外传参是否为0,如果为0就相当于我们执行kubens命令没有带参数。那么这个时候判断了cmdutil.IsInteractiveMode(os.Stdout)函数返回值是否为真,此函数是用来判断是否安装了fzf模块,此模块可以在输出结果的基础上进行选择交互,这样非常的方便。如果没有安装fzf模块,则返回ListOp结构体。我们接下来分析一下ListOp结构体
type ListOp struct{}
func (op ListOp) Run(stdout, stderr io.Writer) error {
kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader)
defer kc.Close()
if err := kc.Parse(); err != nil {
return errors.Wrap(err, "kubeconfig error")
}
ctx := kc.GetCurrentContext()
if ctx == "" {
return errors.New("current-context is not set")
}
curNs, err := kc.NamespaceOfContext(ctx)
if err != nil {
return errors.Wrap(err, "cannot read current namespace")
}
ns, err := queryNamespaces(kc)
if err != nil {
return errors.Wrap(err, "could not list namespaces (is the cluster accessible?)")
}
for _, c := range ns {
s := c
if c == curNs {
s = printer.ActiveItemColor.Sprint(c)
}
fmt.Fprintf(stdout, "%s\n", s)
}
return nil
}
func queryNamespaces(kc *kubeconfig.Kubeconfig) ([]string, error) {
if os.Getenv("_MOCK_NAMESPACES") != "" {
return []string{"ns1", "ns2"}, nil
}
clientset, err := newKubernetesClientSet(kc)
if err != nil {
return nil, errors.Wrap(err, "failed to initialize k8s REST client")
}
var out []string
var next string
for {
list, err := clientset.CoreV1().Namespaces().List(
context.Background(),
metav1.ListOptions{
Limit: 500,
Continue: next,
})
if err != nil {
return nil, errors.Wrap(err, "failed to list namespaces from k8s API")
}
next = list.Continue
for _, it := range list.Items {
out = append(out, it.Name)
}
if next == "" {
break
}
}
return out, nil
}
func newKubernetesClientSet(kc *kubeconfig.Kubeconfig) (*kubernetes.Clientset, error) {
b, err := kc.Bytes()
if err != nil {
return nil, errors.Wrap(err, "failed to convert in-memory kubeconfig to yaml")
}
cfg, err := clientcmd.RESTConfigFromKubeConfig(b)
if err != nil {
return nil, errors.Wrap(err, "failed to initialize config")
}
return kubernetes.NewForConfig(cfg)
}
ListOp结构体是一个空的结构体,因为我们只是执行了命令本身,没有传入什么参数,因为要符合Op接口,所以需要实现Run方法。在Run方法中我们看到kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader)通过new函数生成kubeconfig.kubeconfig对象并调用WithLoader方法加载了默认的kubeconfig配置文件。通过ctx := kc.GetCurrentContext()步骤,获取了kubeconfig的上下文,通过curNs, err := kc.NamespaceOfContext(ctx)代码获取到当前配置的命名空间。再通过ns, err := queryNamespaces(kc) 获取到了所有的命名空间。最后通过for _, c := range ns for循环取出所有的命名空间,再跟之前获取到的当前的命名空间做对比,如果是当前命名空间则通过颜色着重强调打印,否则就正常的打印输出。我们通过分析ListOp结构体以及他的Run方法,发现他实现的就是打印所有命名空间并重点突出当前所在命名空间,main函数后面的模块都是类似的写法。
我们接下来去看parseArgs函数的代码:在判断了参数为0的情况之后,又判断了参数为1的情况,前面几个判断都是检查是否为帮助或版本信息,if v == "--current" || v == "-c" 此判断返回CurrentOp{}结构体,我们看一下CurrentOp{}结构体的Run方法
func (c CurrentOp) Run(stdout, _ io.Writer) error {
kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader)
// 通过new函数生成kc对象并加载kubeconfig配置文件
defer kc.Close()
if err := kc.Parse(); err != nil {
// 通过Parse解析加载的kubeconfig配置文件,查看配置文件格式是否正常
return errors.Wrap(err, "kubeconfig error")
}
ctx := kc.GetCurrentContext() // 通过GetCurrentContext获取配置文件的上下文
if ctx == "" {
return errors.New("current-context is not set")
}
ns, err := kc.NamespaceOfContext(ctx) // 通过解析上下文获取此时配置的命名空间
if err != nil {
return errors.Wrapf(err, "failed to read namespace of \"%s\"", ctx)
}
_, err = fmt.Fprintln(stdout, ns) // 打印获取到的命名空间值
return errors.Wrap(err, "write error")
}
通过代码中的注释,我们可以得知此参数就是为了获取配置文件上下文中配置的命名空间,此参数主要是在命名空间比较多的情况下或者fzf模式下只想知道配置的命名空间值。
我们接下来继续分析parseArgs函数,
if strings.HasPrefix(v, "-") && v != "-" {
return UnsupportedOp{Err: fmt.Errorf("unsupported option '%s'", v)}
}
此判断主要是判断传参中带有"-"符号以及不单单是"-"符号本身的传参,因为前面已经判断过help之类的,除了这些允许带有"-"符号之外的传参都是违法的,直接报错退出。
最终parseArgs函数调用了return SwitchOp{Target: argv[0] ,SwitchOp{}结构体只有 Target字段,这里是把 Target字段配置为我们传的参数。我们分析一下SwitchOp{}结构体的Run方法:
func (s SwitchOp) Run(_, stderr io.Writer) error {
kc := new(kubeconfig.Kubeconfig).WithLoader(kubeconfig.DefaultLoader)
defer kc.Close()
if err := kc.Parse(); err != nil {
return errors.Wrap(err, "kubeconfig error")
}
toNS, err := switchNamespace(kc, s.Target)
// Run方法主要的处理逻辑就在switchNamespace函数中
if err != nil {
return err
}
err = printer.Success(stderr, "Active namespace is \"%s\"", printer.SuccessColor.Sprint(toNS))
//切换成功之后进行输出
return err
}
最后我们分析一下 switchNamespace函数:
func switchNamespace(kc *kubeconfig.Kubeconfig, ns string) (string, error) {
ctx := kc.GetCurrentContext()
if ctx == "" {
return "", errors.New("current-context is not set")
}
curNS, err := kc.NamespaceOfContext(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to get current namespace")
}
f := NewNSFile(ctx)
prev, err := f.Load()
// 通过此步骤获取到我们上次切换的时候的命名空间是什么,用来配合 "-"参数切换回之前的命名空间
// 我们通过查看NewNSFile函数看到此路径为$HOME/.kube/kubens/
// 此目录里面的配置文件就是我们切换之前的命名空间
if err != nil {
return "", errors.Wrap(err, "failed to load previous namespace from file")
}
if ns == "-" {
if prev == "" {
return "", errors.Errorf("No previous namespace found for current context (%s)", ctx)
}
// 如果传参是"-",则切换回原来的命名空间
ns = prev
}
ok, err := namespaceExists(kc, ns)
// namespaceExists函数主要是检查传入的ns是否存在,这个函数就不展开说了
if err != nil {
return "", errors.Wrap(err, "failed to query if namespace exists (is cluster accessible?)")
}
if !ok {
return "", errors.Errorf("no namespace exists with name \"%s\"", ns)
}
if err := kc.SetNamespace(ctx, ns); err != nil {
// SetNamespace方法为改变ctx上下文为我们传入的命名空间
return "", errors.Wrapf(err, "failed to change to namespace \"%s\"", ns)
}
if err := kc.Save(); err != nil {
// Save方法就是用来把上下文保存到配置文件中去的
return "", errors.Wrap(err, "failed to save kubeconfig file")
}
if curNS != ns {
// 最终还要判断一下,我们切换过去的命名空间与之前的命名空间是否相等,
// 如果不相等需要把原命名空间保存到$HOME/.kube/kubens/
// 主要是为了配合后面通过"-"符号再切换回去
if err := f.Save(curNS); err != nil {
return "", errors.Wrap(err, "failed to save the previous namespace to file")
}
}
return ns, nil
}
总结:通过源代码我们可以一步步的看到krew kubens是怎么实现的,整体来说还是非常的简单的,主要是通过定义Op接口,然后通过parseArgs函数去解析传入参数,最后所有传参去实现Op接口的Run方法,通过Run方法去实现各个传参的功能。krew 充分补充了原生kubectl命令的很多不足之处,可以提升大家的工作效率。