访问者模式在 Kubernetes 中的使用
要说有哪些为我打开了高效编程之门的文章,我会说 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 xxx
和 kubectl 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
参考资料
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