揭开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时候的
apiversion
,kind
一一对应的 - 但是在实际的代码编写过程中,实际标记 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() GroupVersionKind
将apiversion,kind
装换为GVK(GroupVerisonKind)结构体
-
SetGroupVersionKind(kind GroupVersionKind)
将GVK(GroupVerisonKind)结构体
装换为apiversion,kind
- 其实就是
GVK
和apiversion,kind
两种表现形式的转换
- 因此总结为
-
schema.ObjecKind
是所有API对象类型meta的抽象 -
metav1.TypeMeta
是schema.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.ObjecKind
和metav1.Object
感觉都不合适,因为他们所能访问的域是有限
- 只用一个,只能对类型或公共属性其中一项操作
- 所以,如果有一个函数需要访问任何API对象的类型和公共属性,那么就要传入同一个对象的两个指针(
schema.ObjecKind
和metav1.Object
)
- 但这个就不太满足我们的要求,我们只想用一个指针
- 所以?有没有一个类型作为API单体对象的统一的基类呢?
- 这就是本节要讨论的:runtime.Object(本章节简称 Object)
- 可以理解为
runtime.Object
对schema.ObjecKind
和metav1.Object
两个接口进行了封装 - 这样只需调用
runtime.Object
即可获取到schema.ObjecKind
和metav1.Object
,从而对所有 API 对象进行操作
- 具体操作见下面代码
- 所以对于
runtime
的要求是可以承接任意 API 对象(具有schema.ObjecKind
和metav1.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 | 总结
至此,前面各章节的总结都可以忘掉了,因为那些总结都是基于当时的知识背景做的总结,可能缺乏全局性的考虑做出错误的结论,所以在此做出通盘的总结,如下图所示:
- runtime.Object是所有API单体对象的根类(interface);
- schema.ObjectKind是对API对象类型的抽象(interface);
- metav1.Object是对API对象公共属性的抽象(interface);
- metav1.ListInterface是对API对象列表公共属性的抽象(interface);
- metav1.TypeMeta是schema.ObjectKind的一个实现,API对象类型继承之;
- metav1.ObjectMeta是metav1.Object的一个实现,API对象类型继承之;
- metav1.ListMeta是metav1.ListInterface的一个实现,API对象列表继承之;