首頁 > 軟體

Kubernetes Visitor設計模式及傳送pod建立請求解析

2022-11-27 14:01:51

確立目標

  • 理解kubectl的核心實現之一:Visitor Design Pattern 存取者模式
  • 理解傳送pod建立請求的細節

visitor design pattern

在設計模式中,存取者模式的定義為:

允許一個或者多個操作應用到物件上,解耦操作和物件本身

那麼,對一個程式來說,具體的表現就是:

  • 表面:某個物件執行了一個方法
  • 內部:物件內部呼叫了多個方法,最後統一返回結果

舉個例子,

  • 表面:呼叫一個查詢訂單的介面
  • 內部:先從快取中查詢,沒查到再去熱點資料庫查詢,還沒查到則去歸檔資料庫裡查詢

Visitor

我們來看看kubeadm中的存取者模式的定義:

// Visitor 即為存取者這個物件
type Visitor interface {
	Visit(VisitorFunc) error
}
// VisitorFunc對應這個物件的方法,也就是定義中的「操作」
type VisitorFunc func(*Info, error) error

基本的資料結構很簡單,但從當前的資料結構來看,有兩個問題:

  • 單個操作 可以直接呼叫Visit方法,那多個操作如何實現呢?
  • 在應用多個操作時,如果出現了error,該退出還是繼續應用下一個操作呢?

Chained

以下內容在staging/src/k8s.io/cli-runtime/pkg/resource

VisitorList和EagerVisitorList是將多個物件聚合為一個物件

DecoratedVisitor和ContinueOnErrorVisitor是將多個方法聚合為一個方法

FlattenListVisitor和FilteredVisitor是將物件抽象為多個底層物件,逐個呼叫方法

VisitorList

封裝多個Visitor為一個,出現錯誤就立刻中止並返回

// VisitorList定義為[]Visitor,又實現了Visit方法,也就是將多個[]Visitor封裝為一個Visitor
type VisitorList []Visitor
// 發生error就立刻返回,不繼續遍歷
func (l VisitorList) Visit(fn VisitorFunc) error {
	for i := range l {
		if err := l[i].Visit(fn); err != nil {
			return err
		}
	}
	return nil
}

EagerVisitorList

封裝多個Visitor為一個,出現錯誤暫存下來,全部遍歷完再聚合所有的錯誤並返回

// EagerVisitorList 也是將多個[]Visitor封裝為一個Visitor
type EagerVisitorList []Visitor
// 返回的錯誤暫存到[]error中,統一聚合
func (l EagerVisitorList) Visit(fn VisitorFunc) error {
	errs := []error(nil)
	for i := range l {
		if err := l[i].Visit(func(info *Info, err error) error {
			if err != nil {
				errs = append(errs, err)
				return nil
			}
			if err := fn(info, nil); err != nil {
				errs = append(errs, err)
			}
			return nil
		}); err != nil {
			errs = append(errs, err)
		}
	}
	return utilerrors.NewAggregate(errs)
}

DecoratedVisitor

這裡借鑑了裝飾器的設計模式,將一個Visitor呼叫多個VisitorFunc方法,封裝為呼叫一個VisitorFunc

// 裝飾器Visitor
type DecoratedVisitor struct {
	visitor    Visitor
	decorators []VisitorFunc
}
// visitor遍歷呼叫decorators中所有函數,有失敗立即返回
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)
	})
}

ContinueOnErrorVisitor

// 報錯依舊繼續
type ContinueOnErrorVisitor struct {
	Visitor
}
// 報錯不立即返回,聚合所有錯誤後返回
func (v ContinueOnErrorVisitor) Visit(fn VisitorFunc) error {
	errs := []error{}
	err := v.Visitor.Visit(func(info *Info, err error) error {
		if err != nil {
			errs = append(errs, err)
			return nil
		}
		if err := fn(info, nil); err != nil {
			errs = append(errs, err)
		}
		return nil
	})
	if err != nil {
		errs = append(errs, err)
	}
	if len(errs) == 1 {
		return errs[0]
	}
	return utilerrors.NewAggregate(errs)
}

FlattenListVisitor

將runtime.ObjectTyper解析成多個runtime.Object,再轉換為多個Info,逐個呼叫VisitorFunc

type FlattenListVisitor struct {
	visitor Visitor
	typer   runtime.ObjectTyper
	mapper  *mapper
}

FilteredVisitor

對Info資源的檢驗

// 過濾的Info
type FilteredVisitor struct {
	visitor Visitor
	filters []FilterFunc
}
func (v FilteredVisitor) Visit(fn VisitorFunc) error {
	return v.visitor.Visit(func(info *Info, err error) error {
		if err != nil {
			return err
		}
		for _, filter := range v.filters {
      // 檢驗Info是否滿足條件,出錯則退出
			ok, err := filter(info, nil)
			if err != nil {
				return err
			}
			if !ok {
				return nil
			}
		}
		return fn(info, nil)
	})
}

Implements

StreamVisitor

最基礎的Visitor

type StreamVisitor struct {
  // 讀取資訊的來源,實現了Read這個介面,這個"流式"的概念,包括了常見的HTTP、檔案、標準輸入等各類輸入
	io.Reader
	*mapper
	Source string
	Schema ContentValidator
}

FileVisitor

檔案的存取,包括標準輸入,底層呼叫StreamVisitor來存取

type FileVisitor struct {
  // 表示檔案路徑或者STDIN
	Path string
	*StreamVisitor
}

URLVisitor

HTTP用GET方法獲取資料,底層也是複用StreamVisitor

type URLVisitor struct {
	URL *url.URL
	*StreamVisitor
  // 提供錯誤重試次數
	HttpAttemptCount int
}

KustomizeVisitor

自定義的Visitor,針對自定義的檔案系統,Customize 客製化,是將C轉成了K

type KustomizeVisitor struct {
	Path string
	*StreamVisitor
}

傳送建立Pod請求的實現細節

kubectl是怎麼向kube-apiserver傳送請求的呢?

send request

// 在RunCreate函數中,關鍵的傳送函數
obj, err := resource.
				NewHelper(info.Client, info.Mapping).
				DryRun(o.DryRunStrategy == cmdutil.DryRunServer).
				WithFieldManager(o.fieldManager).
				Create(info.Namespace, true, info.Object)
// 進入create函數,檢視到
m.createResource(m.RESTClient, m.Resource, namespace, obj, options)
// 對應的實現為
func (m *Helper) createResource(c RESTClient, resource, namespace string, obj runtime.Object, options *metav1.CreateOptions) (runtime.Object, error) {
	return c.Post().
		NamespaceIfScoped(namespace, m.NamespaceScoped).
		Resource(resource).
		VersionedParams(options, metav1.ParameterCodec).
		Body(obj).
		Do(context.TODO()).
		Get()
}
/*
到這裡,我們發現了2個關鍵性的定義:
1. RESTClient 與kube-apiserver互動的RESTful風格的使用者端 這個RESTClient是來自於Builder時的傳入,生成的Result,底層是一個NewClientWithOptions生成的
2. runtime.Object 資源物件的抽象,包括Pod/Deployment/Service等各類資源
3. 我們是傳入的檔案,是FileVisitor來執行的,底層Builder.mapper呼叫Decode來生成obj(Unstructured())
*/

RESTful Client

我們先來看看,與kube-apiserver互動的Client是怎麼建立的

// 從傳入引數來看,資料來源於Info這個結構
r.Visit(func(info *resource.Info, err error) error{})
// 而info來源於前面的Builder,前面部分都是將Builder引數化,核心的生成為Do函數
r := f.NewBuilder().
		Unstructured().
		Schema(schema).
		ContinueOnError().
		NamespaceParam(cmdNamespace).DefaultNamespace().
		FilenameParam(enforceNamespace, &o.FilenameOptions).
		LabelSelectorParam(o.Selector).
		Flatten().
		Do()
// 大致看一下這些函數,我們可以在Unstructured()中看到getClient函數,其實這就是我們要找的函數
func (b *Builder) getClient(gv schema.GroupVersion) (RESTClient, error) 
// 從返回值來看,client包括預設的REST client和設定選項
NewClientWithOptions(client, b.requestTransforms...)
// 這個Client會在kubernetes專案中大量出現,它是與kube-apiserver互動的核心元件,以後再深入。

Object

Object這個物件是怎麼獲取到的呢?因為我們的資料來源是來自檔案的,那麼我們最直觀的想法就是FileVisitor

func (v *FileVisitor) Visit(fn VisitorFunc) error {
	// 省略讀取這塊的程式碼,底層呼叫的是StreamVisitor的邏輯
	return v.StreamVisitor.Visit(fn)
}
func (v *StreamVisitor) Visit(fn VisitorFunc) error {
	d := yaml.NewYAMLOrJSONDecoder(v.Reader, 4096)
	for {
		// 這裡就是返回info的地方
		info, err := v.infoForData(ext.Raw, v.Source)
  }
}
// 再往下一層看,來到mapper層,也就是kubernetes的資源物件對映關係
func (m *mapper) infoForData(data []byte, source string) (*Info, error){
  // 這裡就是我們返回Object的地方,其中GVK是Group/Version/Kind的縮寫,後續我們會涉及
  obj, gvk, err := m.decoder.Decode(data, nil, nil)
}

這時,我們想回頭去看,這個mapper是在什麼時候被定義的?

// 在Builder初始化中,我們就找到了
func (b *Builder) Unstructured() *Builder {
	b.mapper = &mapper{
		localFn:      b.isLocal,
		restMapperFn: b.restMapperFn,
		clientFn:     b.getClient,
    // 我們查詢資源用到的是這個decoder
		decoder:      &metadataValidatingDecoder{unstructured.UnstructuredJSONScheme},
	}
	return b
}
// 逐層往下找,對應的Decode方法的實現,就是對應的資料解析成data:
func (s unstructuredJSONScheme) decode(data []byte) (runtime.Object, error) {
	// 細節暫時忽略
}

Post

瞭解了REST ClientObject的大致產生邏輯後,我們再回過頭來看傳送的方法

// RESTful介面風格中,POST請求對應的就是CREATE方法
c.Post().
		NamespaceIfScoped(namespace, m.NamespaceScoped).
		Resource(resource).
		VersionedParams(options, metav1.ParameterCodec).
		Body(obj).
		Do(context.TODO()). 
		Get() 
// Do方法,傳送請求
err := r.request(ctx, func(req *http.Request, resp *http.Response) {
		result = r.transformResponse(resp, req)
	})
// Get方法,獲取請求的返回結果,用來列印狀態
switch t := out.(type) {
	case *metav1.Status:
		if t.Status != metav1.StatusSuccess {
			return nil, errors.FromObject(t)
		}
	}

站在前人的肩膀上,向前輩致敬,Respect!

Summary

通過Visitor的設計模式,從傳入的引數中解析出內容,然後在Factory進行NewBuilder的時候進行設定實現RESTClient,mapper,obj的生成,Do()拿到Result,組裝好POST請求傳送到ApiServer。

到這裡我們對kubectl的功能有了初步的瞭解,以下是關鍵內容所在:

命令列採用了cobra庫,主要支援7個大類的命令;

掌握Visitor設計模式,這個是kubectl實現各類資源物件的解析和校驗的核心;

初步瞭解RESTClientObject這兩個物件,它們是貫穿kubernetes的核心概念;

呼叫邏輯

  • cobra匹配子命令
  • 用Visitor模式構建Builder
  • 用RESTClient將Object傳送到kube-apiserver

以上就是Kubernetes Visitor設計模式及傳送pod建立請求解析的詳細內容,更多關於Kubernetes Visitor傳送pod請求的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com