一分钟掌握kom:彻底分离k8s资源操作与管理逻辑,拒绝不优雅代码

124 阅读5分钟

导读

对k8s集群进行操作时,往往要结合一些业务逻辑。比如判断某个用户有没有访问权限,有没有操作权限,能不能看到其他人部署的资源等需求。那么这时候往往是要把权限处理代码跟k8s操作代码写在一起了。非常不优雅,也难以复用。

基于 kom 的callback机制分离业务与k8s操作

1. 工具简介

kom 提供了get,list,create,update,patch,delete,exec,logs,watch九种callback处理链,支持设置多个自定义callback方法,而且可以排序,可以精确控制callback的处理顺序。后面将使用一个例子,来展示如何使用。


2. 安装和运行

2.1 集成 kom

在项目中引入 kom 依赖:

import (
    _ "github.com/weibaohui/kom/callbacks" // 导入回调机制
    "github.com/weibaohui/kom"
)

3. callback机制

  • 通过自定义回调函数,当执行完某项操作后,会调用对应的回调函数。
  • 如果回调函数返回true,则继续执行后续操作,否则终止后续操作。
  • 当前支持的callback有:get,list,create,update,patch,delete,exec,logs,watch.
  • 内置的callback名称有:"kom:get","kom:list","kom:create","kom:update","kom:patch","kom:watch","kom:delete","kom:pod:exec","kom:pod:logs"
  • 支持回调函数排序,默认按注册顺序执行,可以通过kom.DefaultCluster().Callback().After("kom:get")或者.Before("kom:get")设置顺序。
  • 支持删除回调函数,通过kom.DefaultCluster().Callback().Delete("kom:get")
  • 支持替换回调函数,通过kom.DefaultCluster().Callback().Replace("kom:get",cb)
// 为Get获取资源注册回调函数
kom.DefaultCluster().Callback().Get().Register("get", cb)
// 为List获取资源注册回调函数
kom.DefaultCluster().Callback().List().Register("list", cb)
// 为Create创建资源注册回调函数
kom.DefaultCluster().Callback().Create().Register("create", cb)
// 为Update更新资源注册回调函数
kom.DefaultCluster().Callback().Update().Register("update", cb)
// 为Patch更新资源注册回调函数
kom.DefaultCluster().Callback().Patch().Register("patch", cb)
// 为Delete删除资源注册回调函数
kom.DefaultCluster().Callback().Delete().Register("delete", cb)
// 为Watch资源注册回调函数
kom.DefaultCluster().Callback().Watch().Register("watch",cb)
// 为Exec Pod内执行命令注册回调函数
kom.DefaultCluster().Callback().Exec().Register("exec", cb)
// 为Logs获取日志注册回调函数
kom.DefaultCluster().Callback().Logs().Register("logs", cb)
// 删除回调函数
kom.DefaultCluster().Callback().Get().Delete("get")
// 替换回调函数
kom.DefaultCluster().Callback().Get().Replace("get", cb)
// 指定回调函数执行顺序,在内置的回调函数执行完之后再执行
kom.DefaultCluster().Callback().After("kom:get").Register("get", cb)
// 指定回调函数执行顺序,在内置的回调函数执行之前先执行
// 案例1.在Create创建资源前,进行权限检查,没有权限则返回error,后续创建动作将不再执行
// 案例2.在List获取资源列表后,进行特定的资源筛选,从列表(Statement.Dest)中删除不符合要求的资源,然后返回给用户
kom.DefaultCluster().Callback().Before("kom:create").Register("create", cb)

// 自定义回调函数
func cb(k *kom.Kubectl) error {
    stmt := k.Statement
    gvr := stmt.GVR
    ns := stmt.Namespace
    name := stmt.Name
    // 打印信息
    fmt.Printf("Get %s/%s(%s)\n", ns, name, gvr)
    fmt.Printf("Command %s/%s(%s %s)\n", ns, name, stmt.Command, stmt.Args)
    return nil
	// return fmt.Errorf("error") 返回error将阻止后续cb的执行
}

4.举个例子

假设我们现在需要查看用户自己创建的Pod 列表,只能查看label中带有user:username的Pod,其他的Pod都不能展示。我们看下如何实现。 思路一: 1、强制增加一个查询条件,必须带有user=username 2、增加查询条件的操作,必须要在查询前,附加上,否则已经查出来了,再执行我们的逻辑没有意义。 3、查询出来的结果就是带有user=username的Pod列表

思路二: 1、查询所有的Pod出来 2、逐个过滤Pod,将带有user=username标签的Pod过滤出来,形成一个新的Pod 列表 3、返回过滤后的Pod 列表

通过分析,可以看出来,思路一更好,在查询前就附加条件,对资源的消耗也更小。我们按思路一来实现:

首先编写callback逻辑

先上代码

func List(k8s *kom.Kubectl) error {
	//  在这里可以统一进行权限认证等操作,返回error即可阻断执行
	u := k8s.Statement.Context.Value("user")
	options := k8s.Statement.ListOptions
	if options == nil || len(options) == 0 {
		options = []metav1.ListOptions{
			{
				LabelSelector: fmt.Sprintf("user=%s", u),
			},
		}
	} else {
		opt := options[0]
		if opt.LabelSelector != "" {
			opt.LabelSelector = fmt.Sprintf("%s,user=%s", opt.LabelSelector, u)
		} else {
			opt.LabelSelector = fmt.Sprintf("user=%s", u)
		}
	}
	k8s.Statement.ListOptions = options
	return nil
}

主要分3分方面: 1、从context中拿到用户 u := k8s.Statement.Context.Value("user") 2、看ListOption中是否有LabelSelector,增加标签

这里有问题,就是我们的用户从哪里设置进去?

传递用户信息

我们以gin框架为例,假设用户名为zhangshan,那么我们通过ctx进行传递。 使用context.WithValue(ctx, "user", "zhangsan")进行包裹。 然后,最重要的地方,我们在调用kom的时候,一定要使用.WithContext(ctx)将ctx传入kom,如果不传递,那么callback中,我们将无法获取用户。

传入的ctx,可以在callback中的kom.Kubectl.Statement.Context中将用户信息取出来。

func List(c *gin.Context) {
	ns := c.Param("ns")
	group := c.Param("group")
	kind := c.Param("kind")
	version := c.Param("version")
	ctx := c.Request.Context()
	ctx = context.WithValue(ctx, "user", "zhangsan")
	var list []unstructured.Unstructured

	err := kom.DefaultCluster().WithContext(ctx).Namespace(ns).GVK(group, version, kind).List(&list).Error
	amis.WriteJsonListWithError(c, list, err)
}

注册Callback

func RegisterCallback() {
	queryCallback := kom.DefaultCluster().Callback().List()
	_ = queryCallback.Before("kom:list").Register("user:list", List)
}

我们注册callback,必须要使用Before指定位置,将我们自定义的逻辑,放在底层逻辑执行之前。这样才能放标签先设置,然后使用标签条件查询,这样才能生效

初始化集群

	defaultKubeConfig := os.Getenv("KUBECONFIG")
	if defaultKubeConfig == "" {
		defaultKubeConfig = filepath.Join(homedir.HomeDir(), ".kube", "config")
	}
	_, _ = kom.Clusters().RegisterInCluster()
	_, _ = kom.Clusters().RegisterByPathWithID(defaultKubeConfig, "default")
	kom.Clusters().Show()
	
	// 初始化回调
	callback.RegisterCallback()

在注册k8s集群后,一定记得执行RegisterCallback(),这样才能生效。

5. 总结

通过 kom 的callback机制,可以轻松的进行权限控制,提供了get,list,create,update,patch,delete,exec,logs,watch九种callback处理链。

那么我们按照上面的示例,我们可以对资源进行充分的管控。通过Before\After方法可以控制我们自定义的callback的顺序。这个顺序可以方便的进行执行前的处理,执行后的处理,能够满足几乎所有的处理逻辑。

尤其是可以将对k8s集群资源的操作跟业务逻辑彻底隔离开了,大大提升了灵活性。

引用

github.com/weibaohui/k… github.com/weibaohui/k…