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