k8s源碼分析5-createCmd中的設計模式

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對象

// 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)
	}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章