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对象
- 文档地址 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)
}
如对您有帮助,支持下呗!
微信
支付宝