訪問者模式在 Kubernetes 中的使用
要說有哪些為我打開了高效編程之門的文章,我會(huì)說 Design Pattern by Gang of four[1] 是第一個(gè)對(duì)我?guī)椭浅4蟮?,它幫助我更好地理解各種代碼結(jié)構(gòu),更合理地編碼。當(dāng)然,它和其他很多設(shè)計(jì)模式的文章一樣,都是基于 Java 的,因?yàn)樵O(shè)計(jì)模式是很多 Java 開源框架所奉行的原則,比如常見的工廠模式、代理模式和 springframework 中的訪問者模式。
不過也不用擔(dān)心,你學(xué)到的東西始終都會(huì)有所幫助的,我從 Java 中獲得的一些鑰匙似乎也可以在 Kubernetes 中發(fā)揮作用,比如當(dāng)我讀完 kubectl 和 k8s 的源碼后,你會(huì)發(fā)現(xiàn)它們有著類似的設(shè)計(jì)模式,雖然在實(shí)現(xiàn)上有所不同,但是思維方式是類似的。
接下來我們來深入了解下訪問者模式,看看這把鑰匙是如何在 kubectl 和 kubernetes 中工作的,以便提升我們的日常編碼能力。
訪問者模式被認(rèn)為是最復(fù)雜的設(shè)計(jì)模式,并且使用頻率不高,《設(shè)計(jì)模式》的作者評(píng)價(jià)為:大多情況下,你不需要使用訪問者模式,但是一旦需要使用它時(shí),那就真的需要使用了。
訪問者模式
下圖很好地展示了訪問者模式編碼的工作流程。

在 Gof 中,也有關(guān)于為什么引入訪問者模式的解釋。
訪問者模式在設(shè)計(jì)跨類層級(jí)結(jié)構(gòu)的異構(gòu)對(duì)象集合的操作時(shí)非常有用。訪問者模式允許在不更改集合中任何對(duì)象的類的情況下定義操作,為達(dá)到該目的,訪問者模式建議在一個(gè)稱為訪問者類(visitor)的單獨(dú)類中定義操作,這將操作與它所操作的對(duì)象集合分開。對(duì)于要定義的每個(gè)新的操作,都要?jiǎng)?chuàng)建一個(gè)新的訪問者類。由于操作將在一組對(duì)象上執(zhí)行,因此訪問者需要一種訪問這些對(duì)象的公共成員的方法。
用一個(gè)實(shí)際的場(chǎng)景來解釋:基于接口的特點(diǎn),動(dòng)態(tài)地解耦對(duì)象和它們的動(dòng)作,以實(shí)現(xiàn)更多的 SOLID 原則,更少的維護(hù),增加新功能(增加新的 ConcreteVisitor)迭代更快。
在 Go 中,訪問者模式的應(yīng)用可以做同樣的改進(jìn),因?yàn)?Interface 接口是它的主要特性之一。
K8s 中的訪問者模式
Kubernetes 是一個(gè)容器編排平臺(tái),上面有各種不同的資源,而 kubectl 是一個(gè)命令行工具,它使用以下命令格式來操作資源。
kubectl?get?{resourcetype}?{resource_name}
kubectl?edit?{resourcetype}?{resource_name}
…
kubectl 將這些命令組合成 APIServer 需要的數(shù)據(jù),發(fā)起一個(gè)請(qǐng)求,并返回結(jié)果,實(shí)際上是執(zhí)行了一個(gè) builder[2] 方法,它封裝了各種訪問者來處理請(qǐng)求的參數(shù)和結(jié)果,最后得到我們?cè)诿钚猩峡吹降慕Y(jié)果。
func?(f?*factoryImpl)?NewBuilder()?*resource.Builder?{
??return?resource.NewBuilder(f.clientGetter)
}
這里的大部分訪問者都是在 visitor.go[3] 中定義的,通過源文件的文件名也可以看出來是訪問者模式。
關(guān)于這部分代碼,大概有700多行,它使用建造者模式(builder.go[4])和訪問者模式連接訪問者,并通過調(diào)用各自的 VisitorFunc[5] 方法來實(shí)現(xiàn)對(duì)應(yīng)的功能,同時(shí)在 builder.go 中封裝了 VisitorFunc 的具體實(shí)現(xiàn)。
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 接口的特性,只要實(shí)現(xiàn)了 Visit 方法的都會(huì)被認(rèn)為是合格的訪問者,接下來我們來看看幾個(gè)典型的訪問者。
Selector
在 kubectl 中,我們默認(rèn)訪問的是 default 這個(gè)命名空間,但是可以使用 -n/-namespace 選項(xiàng)來指定我們要訪問的命名空間,也可以使用 -l/-label 來篩選指定標(biāo)簽的資源,該命令如下所示:
kubectl?get?pod?pod1?-n?test?-l?abc=true
我們就可以通過 Selector[6] 訪問者來查看對(duì)應(yīng)的實(shí)現(xiàn)。
首先定義 Selector 的結(jié)構(gòu)體:
type?Selector?struct?{
??Client????????RESTClient
??Mapping???????*meta.RESTMapping
??Namespace?????string
??LabelSelector?string
??FieldSelector?string
??LimitChunks???int64
}
然后當(dāng)然我們需要去實(shí)現(xiàn) Visit 方法,以便最終能夠構(gòu)建出合理的 Info 對(duì)象供 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] 包含一個(gè) Visitor 和一組裝飾器(VisitorFunc),在執(zhí)行 Visit 方法時(shí)按順序執(zhí)行所有裝飾器。
//?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 中初始化訪問者時(shí),訪問者將被添加到由結(jié)果處理的訪問者列表中,你可以在命名空間被處理后直接調(diào)用 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()
}
在代碼中也有一些類似的訪問者,用于處理不同的邏輯,這種設(shè)計(jì)模式的一個(gè)明顯的好處是操作簡(jiǎn)單?;旧?,所有的資源對(duì)象都符合這種基于 GKV 的操作,所以在添加訪問者時(shí),不需要修改 visitor.go,相反,只要實(shí)現(xiàn)了 VisitorFunc 接口,就可以直接添加新的 go 文件,然后在構(gòu)建器構(gòu)建期間添加相關(guān)邏輯即可。
練習(xí)
我和同事們定制了很多 CRD,編寫了一些 Operator,并在 Kubernetes 集群中運(yùn)行提供不同的服務(wù),比如安全、RBAC 自動(dòng)添加、SA 自動(dòng)創(chuàng)建等功能。
這些 CRD 都有不同的字段屬性,例如:
GroupRbac:包含組名、電子郵件和用戶列表 Identity:包含組名,以及相關(guān)的角色綁定狀態(tài)
由于厭倦了重復(fù)的使用 kubectl get grouprbac xxx 和 kubectl get identity xxx,所以我決定創(chuàng)建一個(gè) kubectl 插件,用 kubectl groupget {groupName} 來獲取它們。當(dāng)然我們可以直接使用最簡(jiǎn)單的 Bash 來實(shí)現(xiàn),但是如果增加更多的資源,那么慢慢就會(huì)變得難以維護(hù)和擴(kuò)展了,所以我決定使用 Go 來實(shí)現(xiàn)它。
現(xiàn)在讓我們回到訪問者模式上面來,在處理資源訪問時(shí),我定義了一組訪問者,它們可以用來訪問不同的資源,代碼結(jié)構(gòu)如下所示:
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?{
????//?...
??}
}
每次得到的結(jié)果都存儲(chǔ)在各自的結(jié)果中,并要最終收集和處理,而每當(dāng)有新的資源要添加時(shí),我只需要定義一個(gè)新的訪問者,編寫相應(yīng)的 Visit 訪問方法,可能還要稍微調(diào)整最終的顯示邏輯即可,是不是超級(jí)方便!
func?FetchAll(c?*cobra.Command,?visitors?[]Visitor)?error?{
??//?...
??for?_,?visitor?:=?range?visitors?{
????v.Visit(func(*Info,?error)?error?{
????//...
???})
??}
//?...
}
總結(jié)
我們從來沒有停止過探索編寫更易于閱讀、維護(hù)和擴(kuò)展的代碼的方法,我相信學(xué)習(xí)、理解和實(shí)踐設(shè)計(jì)模式是可以讓我們更接近目標(biāo)的途徑之一,希望本文對(duì)你的也有所幫助。
原文鏈接: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
