访问者模式在 Kubernetes 中的使用

k8s技术圈

共 7012字,需浏览 15分钟

 · 2022-02-13

要说有哪些为我打开了高效编程之门的文章,我会说 Design Pattern by Gang of four[1] 是第一个对我帮助非常大的,它帮助我更好地理解各种代码结构,更合理地编码。当然,它和其他很多设计模式的文章一样,都是基于 Java 的,因为设计模式是很多 Java 开源框架所奉行的原则,比如常见的工厂模式、代理模式和 springframework 中的访问者模式。

不过也不用担心,你学到的东西始终都会有所帮助的,我从 Java 中获得的一些钥匙似乎也可以在 Kubernetes 中发挥作用,比如当我读完 kubectl 和 k8s 的源码后,你会发现它们有着类似的设计模式,虽然在实现上有所不同,但是思维方式是类似的。

接下来我们来深入了解下访问者模式,看看这把钥匙是如何在 kubectl 和 kubernetes 中工作的,以便提升我们的日常编码能力。

访问者模式被认为是最复杂的设计模式,并且使用频率不高,《设计模式》的作者评价为:大多情况下,你不需要使用访问者模式,但是一旦需要使用它时,那就真的需要使用了。

访问者模式

下图很好地展示了访问者模式编码的工作流程。

在 Gof 中,也有关于为什么引入访问者模式的解释。

访问者模式在设计跨类层级结构的异构对象集合的操作时非常有用。访问者模式允许在不更改集合中任何对象的类的情况下定义操作,为达到该目的,访问者模式建议在一个称为访问者类(visitor)的单独类中定义操作,这将操作与它所操作的对象集合分开。对于要定义的每个新的操作,都要创建一个新的访问者类。由于操作将在一组对象上执行,因此访问者需要一种访问这些对象的公共成员的方法。

用一个实际的场景来解释:基于接口的特点,动态地解耦对象和它们的动作,以实现更多的 SOLID 原则,更少的维护,增加新功能(增加新的 ConcreteVisitor)迭代更快。

在 Go 中,访问者模式的应用可以做同样的改进,因为 Interface 接口是它的主要特性之一。

K8s 中的访问者模式

Kubernetes 是一个容器编排平台,上面有各种不同的资源,而 kubectl 是一个命令行工具,它使用以下命令格式来操作资源。

kubectl get {resourcetype} {resource_name}
kubectl edit {resourcetype} {resource_name}

kubectl 将这些命令组合成 APIServer 需要的数据,发起一个请求,并返回结果,实际上是执行了一个 builder[2] 方法,它封装了各种访问者来处理请求的参数和结果,最后得到我们在命令行上看到的结果。

func (f *factoryImpl) NewBuilder() *resource.Builder {
  return resource.NewBuilder(f.clientGetter)
}

这里的大部分访问者都是在 visitor.go[3] 中定义的,通过源文件的文件名也可以看出来是访问者模式。

关于这部分代码,大概有700多行,它使用建造者模式(builder.go[4])和访问者模式连接访问者,并通过调用各自的 VisitorFunc[5] 方法来实现对应的功能,同时在 builder.go 中封装了 VisitorFunc 的具体实现。

type VisitorFunc func(*Info, error) error
type Visitor interface {
  Visit(VisitorFunc) error
}
type Info struct {
  Namespace   string
  Name        string
  OtherThings string
}
func (info *Info) Visit(fn VisitorFunc) error {
  return fn(info, nil)
}

由于 Go 接口的特性,只要实现了 Visit 方法的都会被认为是合格的访问者,接下来我们来看看几个典型的访问者。

Selector

在 kubectl 中,我们默认访问的是 default 这个命名空间,但是可以使用 -n/-namespace 选项来指定我们要访问的命名空间,也可以使用 -l/-label 来筛选指定标签的资源,该命令如下所示:

kubectl get pod pod1 -n test -l abc=true

我们就可以通过 Selector[6] 访问者来查看对应的实现。

首先定义 Selector 的结构体:

type Selector struct {
  Client        RESTClient
  Mapping       *meta.RESTMapping
  Namespace     string
  LabelSelector string
  FieldSelector string
  LimitChunks   int64
}

然后当然我们需要去实现 Visit 方法,以便最终能够构建出合理的 Info 对象供 API 访问。

list, err := helper.List(
    r.Namespace,
    r.ResourceMapping().GroupVersionKind.GroupVersion().String(),
    &options,
)
if err != nil {
    return nil, EnhanceListError(err, options, r.Mapping.Resource.String())
}
resourceVersion, _ := metadataAccessor.ResourceVersion(list)

info := &Info{
    Client:  r.Client,
    Mapping: r.Mapping,

    Namespace:       r.Namespace,
    ResourceVersion: resourceVersion,

    Object: list,
}

if err := fn(info, nil); err != nil {
    return nil, err
}

DecoratedVisitor

DecoratedVisitor[7] 包含一个 Visitor 和一组装饰器(VisitorFunc),在执行 Visit 方法时按顺序执行所有装饰器。

// DecoratedVisitor will invoke the decorators in order prior to invoking the visitor function
// passed to Visit. An error will terminate the visit.
type DecoratedVisitor struct {
 visitor    Visitor
 decorators []VisitorFunc
}

// NewDecoratedVisitor will create a visitor that invokes the provided visitor functions before
// the user supplied visitor function is invoked, giving them the opportunity to mutate the Info
// object or terminate early with an error.
func NewDecoratedVisitor(v Visitor, fn ...VisitorFunc) Visitor {
 if len(fn) == 0 {
  return v
 }
 return DecoratedVisitor{v, fn}
}

// Visit implements Visitor
func (v DecoratedVisitor) Visit(fn VisitorFunc) error {
 return v.visitor.Visit(func(info *Info, err error) error {
  if err != nil {
   return err
  }
  for i := range v.decorators {
   if err := v.decorators[i](info, nil "i"); err != nil {
    return err
   }
  }
  return fn(info, nil)
 })
}

在 builder.go 中初始化访问者时,访问者将被添加到由结果处理的访问者列表中,你可以在命名空间被处理后直接调用 Get 方法。

if b.latest {
    // must set namespace prior to fetching
    if b.defaultNamespace {
        visitors = NewDecoratedVisitor(visitors, SetNamespace(b.namespace))
    }
    visitors = NewDecoratedVisitor(visitors, RetrieveLatest)
}

// visitor.go: RetrieveLatest updates the Object on each Info by invoking a standard client
// Get.
func RetrieveLatest(info *Info, err error) error {
 if err != nil {
  return err
 }
 if meta.IsListType(info.Object) {
  return fmt.Errorf("watch is only supported on individual resources and resource collections, but a list of resources is found")
 }
 if len(info.Name) == 0 {
  return nil
 }
 if info.Namespaced() && len(info.Namespace) == 0 {
  return fmt.Errorf("no namespace set on resource %s %q", info.Mapping.Resource, info.Name)
 }
 return info.Get()
}

在代码中也有一些类似的访问者,用于处理不同的逻辑,这种设计模式的一个明显的好处是操作简单。基本上,所有的资源对象都符合这种基于 GKV 的操作,所以在添加访问者时,不需要修改 visitor.go,相反,只要实现了 VisitorFunc 接口,就可以直接添加新的 go 文件,然后在构建器构建期间添加相关逻辑即可。

练习

我和同事们定制了很多 CRD,编写了一些 Operator,并在 Kubernetes 集群中运行提供不同的服务,比如安全、RBAC 自动添加、SA 自动创建等功能。

这些 CRD 都有不同的字段属性,例如:

  • GroupRbac:包含组名、电子邮件和用户列表
  • Identity:包含组名,以及相关的角色绑定状态

由于厌倦了重复的使用 kubectl get grouprbac xxxkubectl get identity xxx,所以我决定创建一个 kubectl 插件,用 kubectl groupget {groupName} 来获取它们。当然我们可以直接使用最简单的 Bash 来实现,但是如果增加更多的资源,那么慢慢就会变得难以维护和扩展了,所以我决定使用 Go 来实现它。

现在让我们回到访问者模式上面来,在处理资源访问时,我定义了一组访问者,它们可以用来访问不同的资源,代码结构如下所示:

type VisitorFunc func(*Info, error) error

type GroupRbacVisitor struct {
  visitor Visitor
  results map[string]GroupResult
}

func (v GroupRbacVisitor) Visit(fn VisitorFunc) error {
 return v.visitor.Visit(func(info *Info, err error) error {
   // ...
 }
}

type IdentityVisitor struct {
  visitor Visitor
  results map[string]IdentityResult
}

func (v IdentityVisitor) Visit(fn VisitorFunc) error {
  return v.visitor.Visit(func(info *Info, err error) error {
    // ...
  }
}

每次得到的结果都存储在各自的结果中,并要最终收集和处理,而每当有新的资源要添加时,我只需要定义一个新的访问者,编写相应的 Visit 访问方法,可能还要稍微调整最终的显示逻辑即可,是不是超级方便!

func FetchAll(c *cobra.Command, visitors []Visitor) error {
  // ...
  for _, visitor := range visitors {
    v.Visit(func(*Info, error) error {
    //...
   })
  }
// ...
}

总结

我们从来没有停止过探索编写更易于阅读、维护和扩展的代码的方法,我相信学习、理解和实践设计模式是可以让我们更接近目标的途径之一,希望本文对你的也有所帮助。

原文链接:https://medium.com/geekculture/visitor-pattern-in-kubernetes-d1b58c6d5cd5

参考资料

[1]

Design Pattern by Gang of four: https://www.gofpatterns.com/index.php

[2]

builder: https://github.com/kubernetes/kubernetes/blob/ea0764452222146c47ec826977f49d7001b0ea8c/staging/src/k8s.io/kubectl/pkg/cmd/util/factory_client_access.go#L94

[3]

visitor.go: https://github.com/kubernetes/kubernetes/blob/cea1d4e20b4a7886d8ff65f34c6d4f95efcb4742/staging/src/k8s.io/cli-runtime/pkg/resource/visitor.go

[4]

builder.go: https://github.com/kubernetes/kubernetes/blob/fafbe3aa51473a70980e04ae19f7db2d32d7365b/staging/src/k8s.io/cli-runtime/pkg/resource/builder.go

[5]

VisitorFunc: https://github.com/kubernetes/kubernetes/blob/ea0764452222146c47ec826977f49d7001b0ea8c/staging/src/k8s.io/cli-runtime/pkg/resource/interfaces.go#L103

[6]

Selector: https://github.com/kubernetes/kubernetes/blob/fafbe3aa51473a70980e04ae19f7db2d32d7365b/staging/src/k8s.io/cli-runtime/pkg/resource/selector.go#L27

[7]

DecoratedVisitor: https://github.com/kubernetes/kubernetes/blob/cea1d4e20b4a7886d8ff65f34c6d4f95efcb4742/staging/src/k8s.io/cli-runtime/pkg/resource/visitor.go#L309

浏览 46
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报