Use of visitor pattern in Kubernetes

To say which articles have opened the door to efficient programming for me, I will say that Design Pattern by Gang of four[1] is the first one that is very helpful to me. It helps me better understand various code structures and code more reasonably. Of course, like many other design pattern articles, it is based on Java, because design pattern is the principle pursued by many Java open source frameworks, such as common factory pattern, proxy pattern and visitor pattern in spring framework.

But don't worry. What you learn will always be helpful. It seems that some of the keys I get from Java can also play a role in Kubernetes. For example, after I read the source code of kubectl and k8s, you will find that they have similar design patterns. Although they are different in implementation, they have similar ways of thinking.

Next, let's take a deep look at the visitor mode and see how this key works in kubectl and kubernetes, so as to improve our daily coding ability.

Visitor mode is considered to be the most complex design mode and is not used frequently. The author of design mode commented that in most cases, you don't need to use visitor mode, but once you need to use it, you really need to use it.

Visitor mode

The following figure shows the workflow of visitor pattern coding well.

In Gof, there is also an explanation of why the visitor pattern is introduced.

The visitor pattern is useful in designing the operation of heterogeneous collections of objects across class hierarchies. Visitor mode allows operations to be defined without changing the class of any object in the collection. To achieve this, visitor mode recommends that operations be defined in a separate class called visitor, which separates the operation from the collection of objects it operates on. For each new operation to be defined, a new accessor class is created. Since the operation will be performed on a set of objects, visitors need a way to access the public members of these objects.

Explain with a practical scenario: Based on the characteristics of the interface, dynamically decouple the objects and their actions, so as to realize more SOLID principles, less maintenance, adding new functions (adding new ConcreteVisitor) and faster iteration.

In Go, the application of visitor mode can make the same improvement, because Interface interface is one of its main features.

Visitor mode in K8s

Kubernetes is a container orchestration platform with various resources, while kubectl is a command-line tool that uses the following command format to manipulate resources.

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

kubectl combines these commands into the data required by APIServer, initiates a request and returns the results. In fact, it executes a builder[2] method, which encapsulates the parameters and results of various visitors to process the request, and finally obtains the results we see on the command line.

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

Most of the visitors here are in visitor The visitor mode defined in go [3] can also be seen from the file name of the source file.

About this part of code, there are about 700 lines. It uses builder mode (builder.go[4]) and visitor mode to connect visitors, and calls their respective VisitorFunc[5] methods to realize the corresponding functions. At the same time, in builder Go encapsulates the specific implementation of 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)
}

Due to the characteristics of the Go interface, anyone who implements the Visit method will be considered as a qualified visitor. Next, let's take a look at some typical visitors.

Selector

In kubectl, we access the default namespace by default, but we can use the - n/-namespace option to specify the namespace we want to access, or use - l/-label to filter the resources with the specified label. The command is as follows:

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

We can view the corresponding implementation through the Selector[6] visitor.

First define the structure of the Selector:

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

Then, of course, we need to implement the Visit method so that we can finally build a reasonable Info object for API access.

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] contains a Visitor and a set of decorators (VisitorFunc). All decorators are executed in sequence when the Visit method is executed.

// 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)
 })
}

In builder When the visitor is initialized in go, the visitor will be added to the visitor list processed by the result. You can call the Get method directly after the namespace is processed.

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()
}

There are similar visitors in the code to deal with different logic. One obvious advantage of this design pattern is that it is easy to operate. Basically, all resource objects comply with this GKV based operation, so there is no need to modify the visitor when adding visitors Go, on the contrary, as long as the VisitorFunc interface is implemented, you can directly add a new go file, and then add relevant logic during the construction of the builder.

practice

My colleagues and I have customized many CRD s, compiled some operators, and run them in Kubernetes cluster to provide different services, such as security, RBAC automatic addition, SA automatic creation and so on.

These CRD s have different field attributes, such as:

  • GroupRbac: contains the group name, email, and user list
  • Identity: contains the group name and related role binding status

Tired of using kubectl get grouprbac xxx and kubectl get identity xxx repeatedly, I decided to create a kubectl plug-in and use kubectl groupget {groupName} to get them. Of course, we can directly use the simplest Bash to implement it, but if more resources are added, it will gradually become difficult to maintain and expand, so I decided to use Go to implement it.

Now let's go back to the visitor mode. When dealing with resource access, I defined a group of visitors, which can be used to access different resources. The code structure is as follows:

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

The results obtained each time are stored in their own results and need to be finally collected and processed. Whenever there are new resources to be added, I just need to define a new visitor, write the corresponding Visit access method, and maybe slightly adjust the final display logic. Is it super convenient!

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

summary

We have never stopped exploring ways to write code that is easier to read, maintain and expand. I believe that learning, understanding and practicing design patterns is one of the ways to get us closer to our goal. I hope this article is also helpful to you.

Original link: https://medium.com/geekculture/visitor-pattern-in-kubernetes-d1b58c6d5cd5

reference material

[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

Added by helz on Sat, 12 Feb 2022 00:15:43 +0200