揭开apimachinery runtime 的秘密

  • 参考:深入剖析kubernetes的API对象类型定义

1 | schema

  • 上一节我们学习到 metav1.TypeMeta 就是 sechema.ObjectKind 接口的实现
  • 回顾 metav1.TypeMeta 结构体
type TypeMeta struct {
  // Kind 是一个字符串值,表示此对象所代表的 REST 资源。//服务器可以从客户端向其提交请求的端点推断出这一点。//无法更新。
	Kind string `json:"kind,omitempty" protobuf:"bytes,1,opt,name=kind"`
  // APIVersion定义对象表示的版本化架构。服务器应将已识别的架构转换为最新的内部值,并且可能会拒绝未识别的值。
	APIVersion string `json:"apiVersion,omitempty" protobuf:"bytes,2,opt,name=apiVersion"`
}
  • 可以看出该结构体的字段与我们编写yaml时候的 apiversionkind 一一对应的
  • 但是在实际的代码编写过程中,实际标记 API 对象采用的是 GVK 模式

  • 就是 Group、Version、Kind
  • Group和 Version才是xxx.yaml的apiVersion字段
  • 那么可能有个疑问? —— 为什么创建 pod 的时候 apiversion 只写了 v1?
  • 因为这个省略了核心组core,这个是由于历史原因导致的,最开始 API 种类少,所以没有 group 概念,后来产生了
  • 因此解析不带 group 的,会默认认为是 核心 core group
  • Kind 就是我们的 资源 种类,如 Pod、Deployment 等
  • 查看下面代码可以更清晰明白
  • schema.ObjecKind 所定义的方法目的就是
  • GroupVersionKind() GroupVersionKindapiversion,kind 装换为 GVK(GroupVerisonKind)结构体
  • SetGroupVersionKind(kind GroupVersionKind)GVK(GroupVerisonKind)结构体装换为 apiversion,kind
  • 其实就是 GVKapiversion,kind 两种表现形式的转换
  • 因此总结为
  • schema.ObjecKind是所有API对象类型meta的抽象
  • metav1.TypeMetaschema.ObjecKind的一个实现,API对象类型通过继承metav1.TypeMeta实现schema.ObjecKind
// 代码源自k8s.io/apimachinery/pkg/runtime/schema/interfaces.go
// ObjectKind是接口,两个接口函数是GroupVersionKind类型的setter和getter
type ObjectKind interface {
    SetGroupVersionKind(kind GroupVersionKind)
    GroupVersionKind() GroupVersionKind
}
// GroupVersionKind才是kubernetes的API对象类型真身,他包括Kind、Version和Group。其中Kind和
// Version还比较好理解,Group又是什么?其实Group/Version才是xxx.yaml的apiVersion字段。
// 在kuberentes中API对象是分组的,像Pod、Service、ConfigMap都属于core分组,而core分组的对象
// 无需在apiVersion字段明文写出来,系统会默认将这类的对象归为core分组,正如文章开始那个Pod的例子。
// 详情可以看下面的代码实现。
type GroupVersionKind struct {
    Group   string
    Version string
    Kind    string
}
// 这个函数在metav1.TypeMeta实现GroupVersionKind()接口的时候调用了,该函数调用了ParseGroupVersion
// 实现从apiVersion解析Group和Version。
func FromAPIVersionAndKind(apiVersion, kind string) GroupVersionKind {
    if gv, err := ParseGroupVersion(apiVersion); err == nil {
        return GroupVersionKind{Group: gv.Group, Version: gv.Version, Kind: kind}
    }
    return GroupVersionKind{Kind: kind}
}
// 从apiVersion解析Group和Version。
func ParseGroupVersion(gv string) (GroupVersion, error) {
    // 这种不报错是什么道理?什么情况下会有对象没有Group和Version?
    if (len(gv) == 0) || (gv == "/") {
        return GroupVersion{}, nil
    }
    // 数apiVersion中有几个‘/’
    switch strings.Count(gv, "/") {
    // 没有'/',就像文章开始的Pod的例子,那么Group就是空字符串,系统默认会把空字符串归为core
    case 0:
        return GroupVersion{"", gv}, nil
    // 有一个'/',那么就以'/'分割apiVersion,左边为Group,右边为Version。
    case 1:
        i := strings.Index(gv, "/")
        return GroupVersion{gv[:i], gv[i+1:]}, nil
    // 其他则为格式错误。
    default:
        return GroupVersion{}, fmt.Errorf("unexpected GroupVersion string: %v", gv)
    }
}

2 | Object

  • 前言知识
  • schema.ObjecKind是所有API对象类型的抽象(上面介绍)
  • metav1.Object作为所有API单体对象公共属性的抽象(第二节介绍)
  • 因此将上面两个组合,我们似乎已经找到了所有API对象的根,就是可以对所有 API 对象获取和操作
  • 但是,考虑一个问题? —— 如果我们只想或只能用一个基类指针指向(获取)任意 API 单体对象呢?
  • schema.ObjecKindmetav1.Object感觉都不合适,因为他们所能访问的域是有限
  • 只用一个,只能对类型或公共属性其中一项操作
  • 所以,如果有一个函数需要访问任何API对象的类型和公共属性,那么就要传入同一个对象的两个指针(schema.ObjecKindmetav1.Object
  • 但这个就不太满足我们的要求,我们只想用一个指针
  • 所以?有没有一个类型作为API单体对象的统一的基类呢?
  • 这就是本节要讨论的:runtime.Object(本章节简称 Object)
  • 可以理解为runtime.Objectschema.ObjecKindmetav1.Object两个接口进行了封装
  • 这样只需调用 runtime.Object 即可获取到 schema.ObjecKindmetav1.Object,从而对所有 API 对象进行操作
  • 具体操作见下面代码
  • 所以对于 runtime 的要求是可以承接任意 API 对象(具有 schema.ObjecKindmetav1.Object
  • GetObjectKind() schema.ObjectKind 就是上面所说的 metav1.TypeMeta的顶级抽象接口,用于获取类型meta元信息
  • 前几节说过,metav1.TypeMeta是所有API对象的父类,毕竟所有的对象都要说明自己是什么类型,无论单体还是列表都会具有,所以都实现了 schema.ObjecKind 接口
  • DeepCopyObject() Object 这个方法是深复制方法,所有 API 对象都会具有
  • 上面两个方法,所有 API 对象都会具有,所以runtime.Object 可以承接所有 API 对象
  • 那现在有个问题,缺少公共属性接口metav1.Object,不应该有一个类似GetObjectMeta()的接口吗?来获取metav1.Object接口。
  • 这里 k8s 采用另一种实现k8s.io/apimachinery/pkg/api/meta/meta.go 定义Accessor()函数,他可以把obj安全的转换为metav1.Object
  • 为什么这么做?
  • 大部分情况,所有 API 对象都会继承 metav1.ObjectMeta 结构体,其正是metav1.Object 接口的实现,直接返回即可
  • 另一种情况,没有直接继承 metav1.ObjectMeta 结构体,可以通过case metav1.ObjectMetaAccessor 来返回metav1.Object 接口
  • 这里具体调用逻辑还不太清晰,暂且这么理解
  • runtime.Object 可以承接所有 API 对象,metav1.Object 可以通过Accessor()函数获取
// 代码源自k8s.io/apimachinery/pkg/runtime/interfaces.go
// 此处可以理解为 Object 接口继承
type Object interface {
    // 有了这个函数,就可以访问对象的类型域
    GetObjectKind() schema.ObjectKind
    // deepcopy是golang深度复制对象的方法,至于什么是深度复制本文就不解释了。这是个不错的函数,
    // 可以通过这个接口复制任何API对象而无需类型依赖。
    // deepcopy是golang深度复制对象的方法,各个类型都实现了此方法 见下面 pod 例子
    DeepCopyObject() Object
    // 就这么两个函数了么?那如果需要访问对象的公共属性域怎么办?不应该有一个类似GetObjectMeta()
    // 的接口么?这一点,kubernetes是通过另一个方式实现的,见下面的代码。
}
 
// 代码源自k8s.io/apimachinery/pkg/api/meta/meta.go,注意是api包,不是apis
// Accessor()函数可以把obj安全的转换为metav1.Object,这样也就避免了每个API对象类型都需要实现
// 类似GetObjectMeta()的接口了。有的读者肯定会问:所有的API对象都继承了metav1.ObjectMeta,
// 这个类型不是实现了GetObjectMeta()么?笔者就要在这里做出说明:笔者提到是类似GetObjectMeta(),
// 如果接口名字是ObjectMeta(),那岂不是继承metav1.ObjectMeta就没用了?一个顶层的类型抽象定义不
// 应该依赖于相对底层类型的实现。
func Accessor(obj interface{}) (metav1.Object, error) {
    // 使用了golang的switch type语法
    switch t := obj.(type) {
    // 因为API对象类型都继承了metav1.ObjectMeta,也就自然实现了metav1.Object。
    case metav1.Object:
        return t, nil
    // 在ObjectMeta章节笔者提到了,metav1.ObjectMeta实现了metav1.ObjectMetaAccessor,
    // 所以API对象也自然实现了metav1.ObjectMetaAccessor。但是API对象会在上一个case就返回
    // 了,这个case是给谁用的呢?笔者也比较疑惑,笔者感觉是那些没有直接继承metav1.ObjectMeta
    // 却实现了metav1.ObjectMetaAccessor的类型,笔者暂时还没找到相关类型定义。
    case metav1.ObjectMetaAccessor:
        if m := t.GetObjectMeta(); m != nil {
            return m, nil
        }
        return nil, errNotObject
    default:
        return nil, errNotObject
    }
}

//deepcopy是golang深度复制对象的方法,各个类型都实现了此方法
//core>v1>zz_generated.deepcopy.go
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Pod) DeepCopyObject() runtime.Object {
	if c := in.DeepCopy(); c != nil {
		return c
	}
	return nil
}
  • 等下,为什么没有看到API对象实现runtime.Object.DeepCopyObject()?那是因为deep copy是具体API对象类型需要实现的,存在类型依赖,作为API对象类型的父类不能实现。此处还是以Pod为例,看看Pod是如何实现DeepCopyObject()的。
// +genclient
// +genclient:method=GetEphemeralContainers,verb=get,subresource=ephemeralcontainers,result=EphemeralContainers
// +genclient:method=UpdateEphemeralContainers,verb=update,subresource=ephemeralcontainers,input=EphemeralContainers,result=EphemeralContainers
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// 上面+k8s:deepcopy-gen:....就是告诉代码生成工具为下面的类型生成runtime.Object接口的
// DeepCopyObject()函数实现。因为所有的API对象类型都要实现DeepCopyObject()函数,这是一个相当
// 大量的重复工作,所以kubernetes用代码生成工具来实现。至于如何实现的不作为本文讨论重点,只要读者
// 知道deep copy的目的就可以了。
type Pod struct {
    ......
}

补充说明(未深入理解)

  • 参考:
  • 如前所述,K8S的资源实现了runtime.object接口,其实大多数资源结构体继承了ObjectMeta也实现了metav1.object接口来获取资源的公共属性。若资源要访问类型之外的公共属性,主要是ObjectMeta结构相关,使用以下的处理方式。Accessor()函数可以把obj安全的转换为metav1.Object,这样也就避免了每个API对象类型都需要实现类似GetObjectMeta()的接口了。meta包中使用了MetadataAccessor 接口聚合了对ObjectMeta和TypeMeta的属性访问和设置。
// meta包中使用了MetadataAccessor 接口聚合了对ObjectMeta和TypeMeta的属性访问和设置。
// MetadataAccessor exposes Interface in a way that can be used with multiple objects.
type MetadataAccessor interface {
	APIVersion(obj runtime.Object) (string, error)
	SetAPIVersion(obj runtime.Object, version string) error

	Kind(obj runtime.Object) (string, error)
	SetKind(obj runtime.Object, kind string) error

	Namespace(obj runtime.Object) (string, error)
	SetNamespace(obj runtime.Object, namespace string) error
	...
}
//k8s.io/apimachinery/pkg/api/meta/meta.go
//实现了对ObjectMeta的属性访问和设置
func Accessor(obj interface{}) (metav1.Object, error) { 
    // 使用了golang的switch type语法
    switch t := obj.(type) {
    // 因为API对象类型都继承了metav1.ObjectMeta,也就自然实现了metav1.Object。
    case metav1.Object:
        return t, nil
    // metav1.ObjectMeta也实现了metav1.ObjectMetaAccessor
    case metav1.ObjectMetaAccessor:
        if m := t.GetObjectMeta(); m != nil {
            return m, nil
        }
        return nil, errNotObject
    default:
        return nil, errNotObject
    }
}

//实现了对TypeMeta的属性访问和设置
type objectAccessor struct {   
	runtime.Object		
}

func (obj objectAccessor) GetKind() string {
	return obj.GetObjectKind().GroupVersionKind().Kind
}

func (obj objectAccessor) SetKind(kind string) {
	gvk := obj.GetObjectKind().GroupVersionKind()
	gvk.Kind = kind
	obj.GetObjectKind().SetGroupVersionKind(gvk)
}
...

func NewAccessor() MetadataAccessor {
	return resourceAccessor{}
}

// resourceAccessor implements ResourceVersioner and SelfLinker.
//聚合objectAccessor结构体方法和Accessor函数实现了MetadataAccessor接口
type resourceAccessor struct{} 

func (resourceAccessor) APIVersion(obj runtime.Object) (string, error) {
	return objectAccessor{obj}.GetAPIVersion(), nil
}

func (resourceAccessor) SetAPIVersion(obj runtime.Object, version string) error {
	objectAccessor{obj}.SetAPIVersion(version)
	return nil
}

func (resourceAccessor) Namespace(obj runtime.Object) (string, error) {
	accessor, err := Accessor(obj)
	if err != nil {
		return "", err
	}
	return accessor.GetNamespace(), nil
}

func (resourceAccessor) SetNamespace(obj runtime.Object, namespace string) error {
	accessor, err := Accessor(obj)
	if err != nil {
		return err
	}
	accessor.SetNamespace(namespace)
	return nil
}

3 | 总结

至此,前面各章节的总结都可以忘掉了,因为那些总结都是基于当时的知识背景做的总结,可能缺乏全局性的考虑做出错误的结论,所以在此做出通盘的总结,如下图所示:

kubernetes ingress 内部无法解析 kubernetes runtime_对象类型

  1. runtime.Object是所有API单体对象的根类(interface);
  2. schema.ObjectKind是对API对象类型的抽象(interface);
  3. metav1.Object是对API对象公共属性的抽象(interface);
  4. metav1.ListInterface是对API对象列表公共属性的抽象(interface);
  5. metav1.TypeMeta是schema.ObjectKind的一个实现,API对象类型继承之;
  6. metav1.ObjectMeta是metav1.Object的一个实现,API对象类型继承之;
  7. metav1.ListMeta是metav1.ListInterface的一个实现,API对象列表继承之;