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)
	}
posted @ 2022-11-24 11:37  西门运维  阅读(127)  评论(0编辑  收藏  举报