概述 #
自定义资源(CustomResourceDefinitions,简称CRD)是K8S的一个重要功能。K8S中有一些资源属于K8S内部自定义的资源类型,如Pod、ConfigMap等,但是即使有了这些核心的资源类型,还是不能满足所有用户的需求的,因为考虑到不同用户的需求不一样,K8S需要另一种可扩展的方式提供给用户。所以K8S就提供了这种叫做CRD的资源类型,允许用户定义自己的资源类型,同时还享受着K8S平台一系列便利:用户可以使用K8S的API,实现对自定义资源的CRUDW操作。有了这个扩展之后,更多的功能就可以在K8S上展开了,比如istio、knative等都是通过CRD来实现与K8S平台的互通,同时K8S自身的一些核心组件比如controller manager、kubelet、ingress等,本质上也是K8S资源的监听者。
所以在K8S看来,其实一个自定义的资源类型,和用户通过CRD来定义的资源类型,处理起来并没有太多不一样的地方,在本文中通过对定义CRD之后生成的一些文件,来分析在K8S中实现一个资源类型,都需要哪些操作。
如果还不熟悉CRD的创建流程,可以先看看这篇文章:https://www.cn18k.com/2018/04/04/Kubernetes-Deep-Dive-Code-Generation-for-CustomResources/ ,本文中将不讲述如何创建CRD生成代码的流程。
核心数据结构 #
要实现一个K8S资源类型,熟悉K8S的人不难想到会涉及到如下操作。
- 资源的注册:该资源如何注册到K8S系统中。
- 不同版本资源的转换:一个资源,如果有多个不同的版本,那么必然涉及到这些版本之间如何进行转换的。
- 资源数据的序列化:资源的数据最终是存储在etcd中的,但是在应答给客户端时,又有不同的格式输出,如yaml、json等。
在这里,K8S提供了一个核心的scheme数据结构进行这些操作的封装,这个数据结构定义在K8S目录的vendor/k8s.io/apimachinery/pkg/runtime/scheme.go中,它有如下成员。
-
gvkToType map[schema.GroupVersionKind]reflect.Type:根据组类型映射得到该资源的反射类型的map。
-
typeToGVK map[reflect.Type][]schema.GroupVersionKind:根据反射类型得到组类型的map,注意到这里的值是一个数组,也就意味着一个反射类型可能对应多个组类型。
-
unversionedKinds map[string]reflect.Type:
-
fieldLabelConversionFuncs map[string]map[string]FieldLabelConversionFunc:
-
defaulterFuncs map[reflect.Type]func(interface{}):
-
converter *conversion.Converter:转换器,负责同个类型不同版本之间的转换。
可以看到,Scheme结构体中,存储了关于组版本类型与对应资源类型的信息,因此它可以同时很方便的用于以下用途。
- 根据资源类型得到其组版本信息,这是ObjectTyper接口需要的操作。
- 进行一个资源在不同组版本之间的转化操作,这是ObjectConvertor接口需要的操作。
- 根据传入的组版本信息,创建一个对应的资源数据返回,这是ObjectCreater接口需要的操作。
- 给资源赋予默认值,这是ObjectDefaulter接口需要的操作。
在CRD的生成代码中,首先会生成如下几个全局变量,一般在clientset/versioned/scheme/register.go中:
var Scheme = runtime.NewScheme()
var Codecs = serializer.NewCodecFactory(Scheme)
var ParameterCodec = runtime.NewParameterCodec(Scheme)
其中:
- Scheme:就是我们前面提到的Scheme结构体,它是最先生成的,因为它是后面其它全局变量的基础。
- Codecs:进行序列化和反序列化操作的结构体,之所以需要Scheme是因为它实现了前面分析过的几种与类型相关的接口。
- ParameterCodec:进行不同类型之间转换的操作结构体,用到Scheme也是因为它实现了前面分析过的几种与类型相关的接口。
流程 #
有了前面的核心数据准备,可以继续展开CRD核心流程的分析了。
在clientset/versioned/scheme/register.go中,init函数里会进行如下操作。
- AddToGroupVersion添加该CRD组版本类型到scheme中,在这个函数中,会将一系列的资源与这个组版本挂钩:
// AddToGroupVersion registers common meta types into schemas.
func AddToGroupVersion(scheme *runtime.Scheme, groupVersion schema.GroupVersion) {
scheme.AddKnownTypeWithName(groupVersion.WithKind(WatchEventKind), &WatchEvent{})
scheme.AddKnownTypeWithName(
schema.GroupVersion{Group: groupVersion.Group, Version: runtime.APIVersionInternal}.WithKind(WatchEventKind),
&InternalEvent{},
)
// Supports legacy code paths, most callers should use metav1.ParameterCodec for now
scheme.AddKnownTypes(groupVersion,
&ListOptions{},
&ExportOptions{},
&GetOptions{},
&DeleteOptions{},
)
scheme.AddConversionFuncs(
Convert_versioned_Event_to_watch_Event,
Convert_versioned_InternalEvent_to_versioned_Event,
Convert_watch_Event_to_versioned_Event,
Convert_versioned_Event_to_versioned_InternalEvent,
)
// Register Unversioned types under their own special group
scheme.AddUnversionedTypes(Unversioned,
&Status{},
&APIVersions{},
&APIGroupList{},
&APIGroup{},
&APIResourceList{},
)
// register manually. This usually goes through the SchemeBuilder, which we cannot use here.
AddConversionFuncs(scheme)
RegisterDefaults(scheme)
}
从上面代码可以看出,任何一个CRD,都进行了以下的注册:
-
注册了与组版本类型相关的WatchEvent事件,注册了WatchEvent相关的内部事件InternalEvent,同时在后面通过调用AddConversionFuncs注册了与这两个事件相关的转换函数。
-
注册了与组版本类型相关的操作选项:ListOptions、ExportOptions、GetOptions、DeleteOptions、CreateOptions、UpdateOptions。
-
注册了与组版本类型无关的类型:Status、APIVersions、APIGroupList、APIGroup、APIResourceList。
- 调用AddToScheme函数,将自定义的资源注册到Scheme中,这些类型都是最初使用者在types.go文件中自定义的资源类型,这些类型一定继承自metav1.TypeMeta。
使用Codecs #
接下来先来看看K8S代码中是怎么使用上前面的Codecs全局变量的。
在生成的CRD相关代码中,会将与客户端REST请求相关参数的参数保存到rest.Config结构体中:
func setConfigDefaults(config *rest.Config) error {
gv := v1.SchemeGroupVersion
config.GroupVersion = &gv
config.APIPath = "/apis"
config.NegotiatedSerializer = serializer.DirectCodecFactory{CodecFactory: scheme.Codecs}
if config.UserAgent == "" {
config.UserAgent = rest.DefaultKubernetesUserAgent()
}
return nil
}
在这里可以看到:
- CRD的api路径都是"apis"开头的。
- Codecs最终保存到config.NegotiatedSerializer中了。
最终,这里保存下来的config.NegotiatedSerializer会存入到Serializers结构体中的Encoder和Decoder成员中,而这两个成员最终在与客户端的REST请求中起作用,比如encoder成员在request.go的Body函数中对用户数据进行编码:
case runtime.Object:
// callers may pass typed interface pointers, therefore we must check nil with reflection
if reflect.ValueOf(t).IsNil() {
return r
}
data, err := runtime.Encode(r.serializers.Encoder, t)
if err != nil {
r.err = err
return r
}
glogBody("Request Body", data)
r.body = bytes.NewReader(data)
r.SetHeader("Content-Type", r.content.ContentType)
至于Decoder这里就不继续阐述了,可以自行搜索,简而言之都是与用户的REST请求、应答相关的地方派上用场。
使用ParameterCodec #
ParameterCodec用在用户请求的VersionedParams中。