Kubernetes Visitor 模式

语言: CN / TW / HK

C T O

   

云原生开发  

Go Rust Python Istio containerd CoreDNS Envoy etcd Fluentd Harbor Helm Jaeger Kubernetes Open Policy Agent Prometheus Rook TiKV TUF Vitess Argo Buildpacks CloudEvents CNI Contour Cortex CRI-O Falco Flux gRPC KubeEdge Linkerd NATS Notary OpenTracing Operator Framework SPIFFE SPIRE     Thanos

Kubernetes Visitor 模式

说到那些为我打开了高效编程大门的人,我想说的是四人组设计模式(Design Pattern by Gang of four)是第一个推动我的人

https://www.gofpatterns.com/index.php

帮助我更好地理解各种代码结构,更合理地编码。当然是基于 Java 的,还有很多其他设计模式的文章,因为设计模式是很多 Java 开源框架所奉行的原则,比如 springframework 中常见的 factory 模式、 proxy 模式和 visitor 模式。

幸运的是,您所学到的和牢记于心的东西将会得到回报——我从 Java 得到的“ key ”似乎也适用于 Kubernetes 。读完 Kubectlk8s 的源代码后,您会发现它们拥有相似的设计模式,尽管实现方式不同。

好了,让我们回到主题,深入探究 Visitor 模式,看看“ key ”在 KubectlKubernetes 中是如何工作的,以促进日常的编码和开发。

什么是Visitor模式

Visitor 模式编码的工作流是最好的答案。

Gof 中,还解释了为什么引入 Visitor 模式。

在设计跨类层次结构的异构对象集合的操作时, Visitor 模式非常有用。 Visitor 模式允许在不更改集合中任何对象的类的情况下定义操作。为了实现这一点, Visitor 模式建议在一个单独的类(称为 Visitor 类)中定义操作。这将操作与它所操作的对象集合分开。对于每一个要定义的新操作,都会创建一个新的 Visitor 类。因为操作要跨一组对象执行,所以 Visitor 需要一种访问这些对象的公共成员的方法。这个需求可以通过实现以下两个设计思想来解决。

在实际场景中解释它:基于接口特性,动态地解耦对象和它们的动作,以实现更稳定的代码、更少的维护和更快的添加新函数(添加一个新的 ConcreteVisitor )的迭代。

Go 中, Visitor 模式的应用程序可以做同样的改进,因为界面是它的主要特性之一。

Kubectl和Kubernetes中的访客模式

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

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

kubectl 将这些命令组合到 APIServer 接收到的数据中,发起一个请求,然后返回结果,实际执行一个构建器方法,该方法封装了各种 Visitor 来处理请求的参数和结果,最后得到我们在命令行上看到的结果。

构建器方法: https://github.com/kubernetes/kubernetes/blob/ea0764452222146c47ec826977f49d7001b0ea8c/staging/src/k8s.io/kubectl/pkg/cmd/util/factory_client_access.go#L94

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

这里的大多数 Visitor 都是在 Visitor 中定义的。 Go ,源文件的名称不言自明。

这段代码有 700 多行,它使用了 builder 模式( builder.go )和 visitor 模式来连接 Visitor ,并通过调用它们各自的 VisitorFunc 方法来实现函数,同时在 builder.go 中封装了 VisitorFunc 的具体实现。

visitor.go: https://github.com/kubernetes/kubernetes/blob/cea1d4e20b4a7886d8ff65f34c6d4f95efcb4742/staging/src/k8s.io/cli-runtime/pkg/resource/visitor.go
builder.go: https://github.com/kubernetes/kubernetes/blob/fafbe3aa51473a70980e04ae19f7db2d32d7365b/staging/src/k8s.io/cli-runtime/pkg/resource/builder.go
VisitorFunc: https://github.com/kubernetes/kubernetes/blob/ea0764452222146c47ec826977f49d7001b0ea8c/staging/src/k8s.io/cli-runtime/pkg/resource/interfaces.go#L103
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)
}

那些执行 Visit 方法的 visitor 被认为是合格的 visitor 。让我们来看看一些典型的 visitor

Selector

kubectl 中,我们默认访问默认名称空间。但是有 -n/ -namespace 选项来指定我们想要访问的名称空间,还有 -l/ -label 用于选择合格的资源。命令如下所示。

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

通过 Selector 访问器查看实现。

https://github.com/kubernetes/kubernetes/blob/fafbe3aa51473a70980e04ae19f7db2d32d7365b/staging/src/k8s.io/cli-runtime/pkg/resource/selector.go#L27
type Selector struct {
Client RESTClient
Mapping *meta.RESTMapping
Namespace string
LabelSelector string
FieldSelector string
LimitChunks int64
}

当然,我们实现了 Visit 方法,最终为 api 访问构造了合理的 Info 对象。

https://github.com/kubernetes/kubernetes/blob/fafbe3aa51473a70980e04ae19f7db2d32d7365b/staging/src/k8s.io/cli-runtime/pkg/resource/selector.go#L67
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 包含一个 Visitor 和一组装饰器( VisitorFunc ),在执行 Visit 方法时按顺序执行所有的装饰器。

https://github.com/kubernetes/kubernetes/blob/cea1d4e20b4a7886d8ff65f34c6d4f95efcb4742/staging/src/k8s.io/cli-runtime/pkg/resource/visitor.go#L309
// 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); err != nil {
return err
}
}
return fn(info, nil)
})
}

当在生成器中初始化它时, Visitor 将被添加到由结果处理的 Visitor 列表中。你可以在命名空间处理后调用 get 方法。

https://github.com/kubernetes/kubernetes/blob/fafbe3aa51473a70980e04ae19f7db2d32d7365b/staging/src/k8s.io/cli-runtime/pkg/resource/builder.go#L1119
if b.latest {
// must set namespace prior to fetching
if b.defaultNamespace {
visitors = NewDecoratedVisitor(visitors, SetNamespace(b.namespace))
}
visitors = NewDecoratedVisitor(visitors, RetrieveLatest)
}
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()
}

在代码中,也有一些类似的 Visitor 处理不同的逻辑。这种设计模式的一个明显优点是操作简单。基本上,所有的资源都符合这个基于 gkv 的操作。所以当添加 Visitor 时,不修改 Visitor 。去是必要的。相反,只要实现了 VisitorFunc 接口,就可以直接添加新的 go 文件。然后在构建器构建过程中添加相关逻辑。

实际的Visitor模式

回顾我最近写的代码,我已经不知不觉地发现了 Visitor 模式的踪迹。我和我的同事定制了许多 crd ,编写了 operator ,并在集群中运行它们,提供不同的服务,如安全、 RBAC 自动添加、 SA 自动创建等。

这些 cd 都有不同的字段属性,例如, groupprbac 包含组名、电子邮件和用户列表。标识,包含组名和关联的 Rolebindings 的状态。由于厌倦了重复的 kubectl get groupbac xxxkubectl get identity xxx ,我决定创建一个 kubectl 插件来使用 kubectl groupget {groupName} 获取它们。那么如何?我首先想到的是 Bash ,但如果添加更多的资源,它将很难维护和扩展。

回到 Visitor 模式本身。在处理资源访问时,我定义了一组 Visitor ,可以用来访问不同的资源。代码结构如下所示。

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 {
// ...
}
}

每次获得的结果存储在各自的结果中,并最终被收集和处理。每当添加新资源时,我只需要定义一个新的 Visitor ,编写相应的 visit 方法,并可能调整最终的显示逻辑。超级方便!

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

结束

我们从未停止寻找编写更易于阅读、维护和扩展的代码的方法。我相信学习、理解和实践设计模式是让我们更接近目标的途径之一。

直到我研究了源代码,我才意识到我已经在实践中使用了设计模式。这就像你终于认识了一个每天都在见面的人一个外表变了的老朋友。这种认知让我们感觉更亲近——现在,我知道如何更好地使用 Go 中的设计模式。感谢你的阅读!

更多文档

请关注微信公众号 云原生CTO [1]

参考资料

[1]

参考地址: https://medium.com/geekculture/visitor-pattern-in-kubernetes-d1b58c6d5cd5