1、設計模式之建造者模式
- 建造者(Builder)模式:指將一個複雜對象的構造與它的表示分離
- 使同樣的構建過程可以創建不同的對象,這樣的設計模式被稱爲建造者模式
- 它是將一個複雜的對象分解爲多個簡單的對象,然後一步一步構建而成
- 它將變與不變相分離,即產品的組成部分是不變的,但每一部分是可以靈活選擇的。
- 更多用來 針對複雜對象的創建
優點
- 封裝性好,構建和表示分離。
- 擴展性好,各個具體的建造者相互獨立,有利於系統的解耦。
- 客戶端不必知道產品內部組成的細節,建造者可以對創建過程逐步細化,而不對其它模塊產生任何影響,便於控制細節風險。
缺點:
- 產品的組成部分必須相同,這限制了其使用範圍。
- 如果產品的內部變化複雜,如果產品內部發生變化,則建造者也要同步修改,後期維護成本較大。
kubectl 中的創建者模式
- kubectl中的 Builder對象
特點1 針對複雜對象的創建,字段非常多
-
kubectl中的 Builder對象,可以看到字段非常多
-
如果使用Init函數構造參數會非常多
-
而且參數是不固定的,即可以根據用戶傳入的參數情況構造不同對象
-
位置 D:\go_path\src\github.com\kubernetes\kubernetes\staging\src\k8s.io\cli-runtime\pkg\resource\builder.go
type Builder struct {
categoryExpanderFn CategoryExpanderFunc
// mapper is set explicitly by resource builders
mapper *mapper
// clientConfigFn is a function to produce a client, *if* you need one
clientConfigFn ClientConfigFunc
restMapperFn RESTMapperFunc
// objectTyper is statically determinant per-command invocation based on your internal or unstructured choice
// it does not ever need to rely upon discovery.
objectTyper runtime.ObjectTyper
// codecFactory describes which codecs you want to use
negotiatedSerializer runtime.NegotiatedSerializer
// local indicates that we cannot make server calls
local bool
errs []error
paths []Visitor
stream bool
stdinInUse bool
dir bool
labelSelector *string
fieldSelector *string
selectAll bool
limitChunks int64
requestTransforms []RequestTransform
resources []string
namespace string
allNamespace bool
names []string
resourceTuples []resourceTuple
defaultNamespace bool
requireNamespace bool
flatten bool
latest bool
requireObject bool
singleResourceType bool
continueOnError bool
singleItemImplied bool
schema ContentValidator
// fakeClientFn is used for testing
fakeClientFn FakeClientFunc
}
特點2 開頭的方法返回要創建對象的指針
func NewBuilder(restClientGetter RESTClientGetter) *Builder {
categoryExpanderFn := func() (restmapper.CategoryExpander, error) {
discoveryClient, err := restClientGetter.ToDiscoveryClient()
if err != nil {
return nil, err
}
return restmapper.NewDiscoveryCategoryExpander(discoveryClient), err
}
return newBuilder(
restClientGetter.ToRESTConfig,
(&cachingRESTMapperFunc{delegate: restClientGetter.ToRESTMapper}).ToRESTMapper,
(&cachingCategoryExpanderFunc{delegate: categoryExpanderFn}).ToCategoryExpander,
)
}
特點3 所有的方法都返回的是建造對象的指針
- k8s.io\kubectl\pkg\cmd\create\create.go
r := f.NewBuilder().
Unstructured().
Schema(schema).
ContinueOnError().
NamespaceParam(cmdNamespace).DefaultNamespace().
FilenameParam(enforceNamespace, &o.FilenameOptions).
LabelSelectorParam(o.Selector).
Flatten().
Do()
- 調用時看着像鏈式調用,鏈上的每個方法都返回這個要建造對象的指針
- 如
func (b *Builder) Schema(schema ContentValidator) *Builder {
b.schema = schema
return b
}
func (b *Builder) ContinueOnError() *Builder {
b.continueOnError = true
return b
}
- 看起來就是設置構造對象的各種屬性
2、visitor訪問者模式簡介
- 訪問者模式(Visitor Pattern)是一種將數據結構與數據操作分離的設計模式,
- 指封裝一些作用於某種數據結構中的各元素的操作,
- 可以在不改變數據結構的前提下定義作用於這些元素的新的操作,
- 屬於行爲型設計模式。
訪問者模式主要適用於以下應用場景:
- (1)數據結構穩定,作用於數據結構的操作經常變化的場景。
- (2)需要數據結構與數據操作分離的場景。
- (3)需要對不同數據類型(元素)進行操作,而不使用分支判斷具體類型的場景。
訪問者模式的優點
- (1)解耦了數據結構與數據操作,使得操作集合可以獨立變化。
- (2)可以通過擴展訪問者角色,實現對數據集的不同操作,程序擴展性更好。
- (3)元素具體類型並非單一,訪問者均可操作。
- (4)各角色職責分離,符合單一職責原則。
訪問者模式的缺點
- (1)無法增加元素類型:若系統數據結構對象易於變化,
- 經常有新的數據對象增加進來,
- 則訪問者類必須增加對應元素類型的操作,違背了開閉原則。
- (2)具體元素變更困難:具體元素增加屬性、刪除屬性等操作,
- 會導致對應的訪問者類需要進行相應的修改,
- 尤其當有大量訪問者類時,修改範圍太大。
- (3)違背依賴倒置原則:爲了達到“區別對待”,
- 訪問者角色依賴的是具體元素類型,而不是抽象。
kubectl 中的訪問者模式
- 在kubectl中多個Visitor是來訪問一個數據結構的不同部分
- 這種情況下,數據結構有點像一個數據庫,而各個Visitor會成爲一個個小應用
Visitor接口和VisitorFunc定義
- 位置 D:\go_path\src\github.com\kubernetes\kubernetes\staging\src\k8s.io\cli-runtime\pkg\resource\interfaces.go
// Visitor lets clients walk a list of resources.
type Visitor interface {
Visit(VisitorFunc) error
}
// VisitorFunc implements the Visitor interface for a matching function.
// If there was a problem walking a list of resources, the incoming error
// will describe the problem and the function can decide how to handle that error.
// A nil returned indicates to accept an error to continue loops even when errors happen.
// This is useful for ignoring certain kinds of errors or aggregating errors in some way.
type VisitorFunc func(*Info, error) error
- result的Visit方法
func (r *Result) Visit(fn VisitorFunc) error {
if r.err != nil {
return r.err
}
err := r.visitor.Visit(fn)
return utilerrors.FilterOut(err, r.ignoreErrors...)
}
- 具體的visitor的visit方法定義,參數都是一個VisitorFunc的fn
// Visit in a FileVisitor is just taking care of opening/closing files
func (v *FileVisitor) Visit(fn VisitorFunc) error {
var f *os.File
if v.Path == constSTDINstr {
f = os.Stdin
} else {
var err error
f, err = os.Open(v.Path)
if err != nil {
return err
}
defer f.Close()
}
// TODO: Consider adding a flag to force to UTF16, apparently some
// Windows tools don't write the BOM
utf16bom := unicode.BOMOverride(unicode.UTF8.NewDecoder())
v.StreamVisitor.Reader = transform.NewReader(f, utf16bom)
return v.StreamVisitor.Visit(fn)
}
kubectl create中 通過Builder模式創建visitor並執行的過程
FilenameParam解析 -f 文件參數,創建一個visitor
- 位置 D:\go_path\src\github.com\kubernetes\kubernetes\staging\src\k8s.io\cli-runtime\pkg\resource\builder.go
validate校驗-f參數
func (o *FilenameOptions) validate() []error {
var errs []error
if len(o.Filenames) > 0 && len(o.Kustomize) > 0 {
errs = append(errs, fmt.Errorf("only one of -f or -k can be specified"))
}
if len(o.Kustomize) > 0 && o.Recursive {
errs = append(errs, fmt.Errorf("the -k flag can't be used with -f or -R"))
}
return errs
}
- -k代表使用 Kustomize配置
- 如果 -f -k都存在報錯 only one of -f or -k can be specified
kubectl create -f rule.yaml -k rule.yaml
error: only one of -f or -k can be specified
- -k不支持遞歸 -R
kubectl create -k rule.yaml -R
error: the -k flag can't be used with -f or -R
調用path解析文件
recursive := filenameOptions.Recursive
paths := filenameOptions.Filenames
for _, s := range paths {
switch {
case s == "-":
b.Stdin()
case strings.Index(s, "http://") == 0 || strings.Index(s, "https://") == 0:
url, err := url.Parse(s)
if err != nil {
b.errs = append(b.errs, fmt.Errorf("the URL passed to filename %q is not valid: %v", s, err))
continue
}
b.URL(defaultHttpGetAttempts, url)
default:
if !recursive {
b.singleItemImplied = true
}
b.Path(recursive, s)
}
}
- 遍歷 -f傳入的paths
- 如果 是- 代表從標準輸入傳入
- 如果是http開頭的代表從遠端http接口讀取,調用b.URL
- 默認是文件 ,調用b.Path解析
b.Path調用ExpandPathsToFileVisitors生成visitor
func ExpandPathsToFileVisitors(mapper *mapper, paths string, recursive bool, extensions []string, schema ContentValidator) ([]Visitor, error) {
var visitors []Visitor
err := filepath.Walk(paths, func(path string, fi os.FileInfo, err error) error {
if err != nil {
return err
}
if fi.IsDir() {
if path != paths && !recursive {
return filepath.SkipDir
}
return nil
}
// Don't check extension if the filepath was passed explicitly
if path != paths && ignoreFile(path, extensions) {
return nil
}
visitor := &FileVisitor{
Path: path,
StreamVisitor: NewStreamVisitor(nil, mapper, path, schema),
}
visitors = append(visitors, visitor)
return nil
})
if err != nil {
return nil, err
}
return visitors, nil
}
底層調用的StreamVisitor,把對應的visit方法註冊到visitor中
- 位置 D:\go_path\src\github.com\kubernetes\kubernetes\staging\src\k8s.io\cli-runtime\pkg\resource\visitor.go
func (v *StreamVisitor) Visit(fn VisitorFunc) error {
d := yaml.NewYAMLOrJSONDecoder(v.Reader, 4096)
for {
ext := runtime.RawExtension{}
if err := d.Decode(&ext); err != nil {
if err == io.EOF {
return nil
}
return fmt.Errorf("error parsing %s: %v", v.Source, err)
}
// TODO: This needs to be able to handle object in other encodings and schemas.
ext.Raw = bytes.TrimSpace(ext.Raw)
if len(ext.Raw) == 0 || bytes.Equal(ext.Raw, []byte("null")) {
continue
}
if err := ValidateSchema(ext.Raw, v.Schema); err != nil {
return fmt.Errorf("error validating %q: %v", v.Source, err)
}
info, err := v.infoForData(ext.Raw, v.Source)
if err != nil {
if fnErr := fn(info, err); fnErr != nil {
return fnErr
}
continue
}
if err := fn(info, nil); err != nil {
return err
}
}
}
- 用jsonYamlDecoder解析文件
- ValidateSchema會解析文件中的字段進行校驗,比如我們把spec故意寫成aspec
kubectl apply -f rule.yaml
error: error validating "rule.yaml": error validating data: [ValidationError(PrometheusRule): unknown field "aspec" in com.coreos.monitoring.v1.PrometheusRule, ValidationError(PrometheusRule): missing required field "spec" in com.coreos.monitoring.v1.PrometheusRule]; if you choose to ignore these errors, turn validation off with --validate=false
- infoForData將解析結果轉換爲Info對象
創建Info。object 就是k8s的對象
- D:\go_path\src\github.com\kubernetes\kubernetes\staging\src\k8s.io\cli-runtime\pkg\resource\mapper.go
- m.decoder.Decode解析出object 和gvk對象
- 其中object代表就是k8s的對象
- gvk是 Group/Version/Kind的縮寫
func (m *mapper) infoForData(data []byte, source string) (*Info, error) {
obj, gvk, err := m.decoder.Decode(data, nil, nil)
if err != nil {
return nil, fmt.Errorf("unable to decode %q: %v", source, err)
}
name, _ := metadataAccessor.Name(obj)
namespace, _ := metadataAccessor.Namespace(obj)
resourceVersion, _ := metadataAccessor.ResourceVersion(obj)
ret := &Info{
Source: source,
Namespace: namespace,
Name: name,
ResourceVersion: resourceVersion,
Object: obj,
}
if m.localFn == nil || !m.localFn() {
restMapper, err := m.restMapperFn()
if err != nil {
return nil, err
}
mapping, err := restMapper.RESTMapping(gvk.GroupKind(), gvk.Version)
if err != nil {
return nil, fmt.Errorf("unable to recognize %q: %v", source, err)
}
ret.Mapping = mapping
client, err := m.clientFn(gvk.GroupVersion())
if err != nil {
return nil, fmt.Errorf("unable to connect to a server to handle %q: %v", mapping.Resource, err)
}
ret.Client = client
}
return ret, nil
}
k8s對象object講解
Object k8s對象
- 文檔地址 https://kubernetes.io/zh/docs/concepts/overview/working-with-objects/kubernetes-objects/
- D:\go_path\src\github.com\kubernetes\kubernetes\staging\src\k8s.io\apimachinery\pkg\runtime\interfaces.go
// Object interface must be supported by all API types registered with Scheme. Since objects in a scheme are
// expected to be serialized to the wire, the interface an Object must provide to the Scheme allows
// serializers to set the kind, version, and group the object is represented as. An Object may choose
// to return a no-op ObjectKindAccessor in cases where it is not expected to be serialized.
type Object interface {
GetObjectKind() schema.ObjectKind
DeepCopyObject() Object
}
作用
- Kubernetes 對象 是持久化的實體
- Kubernetes 使用這些實體去表示整個集羣的狀態。特別地,它們描述瞭如下信息:
- 哪些容器化應用在運行(以及在哪些節點上)
- 可以被應用使用的資源
- 關於應用運行時表現的策略,比如重啓策略、升級策略,以及容錯策略
- 操作 Kubernetes 對象,無論是創建、修改,或者刪除, 需要使用 Kubernetes API
期望狀態
- Kubernetes 對象是 “目標性記錄” 一旦創建對象,Kubernetes 系統將持續工作以確保對象存在
- 通過創建對象,本質上是在告知 Kubernetes 系統,所需要的集羣工作負載看起來是什麼樣子的, 這就是 Kubernetes 集羣的 期望狀態(Desired State)
對象規約(Spec)與狀態(Status)
- 幾乎每個 Kubernetes 對象包含兩個嵌套的對象字段,它們負責管理對象的配置: 對象 spec(規約) 和 對象 status(狀態)
- 對於具有 spec 的對象,你必須在創建對象時設置其內容,描述你希望對象所具有的特徵: 期望狀態(Desired State) 。
- status 描述了對象的 當前狀態(Current State),它是由 Kubernetes 系統和組件 設置並更新的。在任何時刻,Kubernetes 控制平面 都一直積極地管理着對象的實際狀態,以使之與期望狀態相匹配。
yaml中的必須字段
- 在想要創建的 Kubernetes 對象對應的 .yaml 文件中,需要配置如下的字段:
- apiVersion - 創建該對象所使用的 Kubernetes API 的版本
- kind - 想要創建的對象的類別
- metadata - 幫助唯一性標識對象的一些數據,包括一個 name 字符串、UID 和可選的 namespace
Do中創建一批visitor
func (b *Builder) Do() *Result {
r := b.visitorResult()
r.mapper = b.Mapper()
if r.err != nil {
return r
}
if b.flatten {
r.visitor = NewFlattenListVisitor(r.visitor, b.objectTyper, b.mapper)
}
helpers := []VisitorFunc{}
if b.defaultNamespace {
helpers = append(helpers, SetNamespace(b.namespace))
}
if b.requireNamespace {
helpers = append(helpers, RequireNamespace(b.namespace))
}
helpers = append(helpers, FilterNamespace)
if b.requireObject {
helpers = append(helpers, RetrieveLazy)
}
if b.continueOnError {
r.visitor = NewDecoratedVisitor(ContinueOnErrorVisitor{r.visitor}, helpers...)
} else {
r.visitor = NewDecoratedVisitor(r.visitor, helpers...)
}
return r
}
helpers代表一批VisitorFunc
- 比如校驗namespace的 RequireNamespace
func RequireNamespace(namespace string) VisitorFunc {
return func(info *Info, err error) error {
if err != nil {
return err
}
if !info.Namespaced() {
return nil
}
if len(info.Namespace) == 0 {
info.Namespace = namespace
UpdateObjectNamespace(info, nil)
return nil
}
if info.Namespace != namespace {
return fmt.Errorf("the namespace from the provided object %q does not match the namespace %q. You must pass '--namespace=%s' to perform this operation.", info.Namespace, namespace, info.Namespace)
}
return nil
}
}
創建帶裝飾器的visitor DecoratedVisitor
if b.continueOnError {
r.visitor = NewDecoratedVisitor(ContinueOnErrorVisitor{r.visitor}, helpers...)
} else {
r.visitor = NewDecoratedVisitor(r.visitor, helpers...)
}
- 對應的visit方法
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的調用
Visit調用鏈分析
- 外層調用 result.Visit方法,內部的func
err = r.Visit(func(info *resource.Info, err error) error {
if err != nil {
return err
}
if err := util.CreateOrUpdateAnnotation(cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag), info.Object, scheme.DefaultJSONEncoder()); err != nil {
return cmdutil.AddSourceToErr("creating", info.Source, err)
}
if err := o.Recorder.Record(info.Object); err != nil {
klog.V(4).Infof("error recording current command: %v", err)
}
if o.DryRunStrategy != cmdutil.DryRunClient {
if o.DryRunStrategy == cmdutil.DryRunServer {
if err := o.DryRunVerifier.HasSupport(info.Mapping.GroupVersionKind); err != nil {
return cmdutil.AddSourceToErr("creating", info.Source, err)
}
}
obj, err := resource.
NewHelper(info.Client, info.Mapping).
DryRun(o.DryRunStrategy == cmdutil.DryRunServer).
WithFieldManager(o.fieldManager).
Create(info.Namespace, true, info.Object)
if err != nil {
return cmdutil.AddSourceToErr("creating", info.Source, err)
}
info.Refresh(obj, true)
}
count++
return o.PrintObj(info.Object)
})
- visitor接口中的調用方法
func (r *Result) Visit(fn VisitorFunc) error {
if r.err != nil {
return r.err
}
err := r.visitor.Visit(fn)
return utilerrors.FilterOut(err, r.ignoreErrors...)
}
- 最終的調用就是前面註冊的各個visitor的 Visit方法
外層VisitorFunc分析
- 如果出錯就返回錯誤
- DryRunStrategy 代表試運行策略
- 默認爲None代表不試運行
- client代表客戶端試運行 ,不發送請求到server
- server點服務端試運行,發送請求,但是如果會改變狀態就話就不做
- 最終調用 Create創建資源 ,然後調用o.PrintObj(info.Object)打印結果
func(info *resource.Info, err error) error {
if err != nil {
return err
}
if err := util.CreateOrUpdateAnnotation(cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag), info.Object, scheme.DefaultJSONEncoder()); err != nil {
return cmdutil.AddSourceToErr("creating", info.Source, err)
}
if err := o.Recorder.Record(info.Object); err != nil {
klog.V(4).Infof("error recording current command: %v", err)
}
if o.DryRunStrategy != cmdutil.DryRunClient {
if o.DryRunStrategy == cmdutil.DryRunServer {
if err := o.DryRunVerifier.HasSupport(info.Mapping.GroupVersionKind); err != nil {
return cmdutil.AddSourceToErr("creating", info.Source, err)
}
}
obj, err := resource.
NewHelper(info.Client, info.Mapping).
DryRun(o.DryRunStrategy == cmdutil.DryRunServer).
WithFieldManager(o.fieldManager).
Create(info.Namespace, true, info.Object)
if err != nil {
return cmdutil.AddSourceToErr("creating", info.Source, err)
}
info.Refresh(obj, true)
}
count++
return o.PrintObj(info.Object)
}