Skip to main content

client-go与apimachinery

我们知道Kubernetes客户端向Kubernetes API Server发送HTTP请求同样涉及到对资源的序列化/反序列化。 只是不同于服务端,客户端对资源编解码时不需要再进行版本的转化。 在本节中,我们将介绍client-go是如何具体使用apimachinery中提供的序列化工具的。

client-go中的全局Scheme对象

在之前的小节中,我们介绍了apimachinery库的编解码器需要检查scheme中是否注册了相应的kind。 而client-go中的资源客户端支持所有原生Kubernetes资源类型,我们有理由猜测client-go中应该存在一个scheme注册了所有Kubernetes原生kind

事实也的确如此,client-go存在一个全局的Scheme类型的对象Secheme,它被定义在kubernetes/kubernetes/scheme包中, 它注册了Kubernetes中所有原生kind(包括GVK总结的所有三个种类)🎈

client-go/kubernetes/scheme/register.go
var Scheme = runtime.NewScheme()

不过一个值得注意的事情是我们在初识kind已经介绍了Kubernetes所有原生的kind被定义在k8s.io/api库中。 那么client-go中这个全局的Scheme对象是怎么注册上原生kind的呢?

难道是从k8s.io/api库中导入所有版本的kind吗?显然这并不是一个好的办法。 其实官方开发者已经提供了相关的基础代码和组件,旨在为我们提供一种便捷的方式来完成跨库kind注册。

addKnownTypes

首先,在k8s.io/api库中,以API分组为单位,开发者为每个分组预先定义了一个注册函数,这个函数的签名如下所示:

func addKnownTypes(scheme *runtime.Scheme) error

此函数用于将此分组下的所有kind注册进给定的scheme中。我们以core/v1这个API分组为例:

addKnownTypes
k8s.io/api/core/v1/register.go
// Adds the list of known types to the given scheme.
func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(SchemeGroupVersion,
&Pod{},
&PodList{},
&PodStatusResult{},
&PodTemplate{},
&PodTemplateList{},
&ReplicationController{},
&ReplicationControllerList{},
&Service{},
&ServiceProxyOptions{},
&ServiceList{},
&Endpoints{},
&EndpointsList{},
&Node{},
&NodeList{},
&NodeProxyOptions{},
&Binding{},
&Event{},
&EventList{},
&List{},
&LimitRange{},
&LimitRangeList{},
&ResourceQuota{},
&ResourceQuotaList{},
&Namespace{},
&NamespaceList{},
&Secret{},
&SecretList{},
&ServiceAccount{},
&ServiceAccountList{},
&PersistentVolume{},
&PersistentVolumeList{},
&PersistentVolumeClaim{},
&PersistentVolumeClaimList{},
&PodAttachOptions{},
&PodLogOptions{},
&PodExecOptions{},
&PodPortForwardOptions{},
&PodProxyOptions{},
&ComponentStatus{},
&ComponentStatusList{},
&SerializedReference{},
&RangeAllocation{},
&ConfigMap{},
&ConfigMapList{},
)
此注册函数将`core/v1`下所有的*kind*(例如`Pod`、`PodList`)注册进给定的`scheme`中。

也就是说在client-go中,我们只要导入k8s.io/api库中所有分组下的这个注册函数addKnownTypes,并将全局的Scheme对象传入其中执行即可——相比于调用Scheme类型的AddKnownTypes方法一个一个地注册kind,这种按组批量注册的方式确实方便许多。

SchemeBuilder

虽然官方为每个API分组预定义的addKnownTypes函数减轻了我们注册的工作量,但是这种方式仍然需要我们一遍遍地去执行所有导入的注册函数,类似于:

import (
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1"
internalv1alpha1 "k8s.io/api/apiserverinternal/v1alpha1"
appsv1 "k8s.io/api/apps/v1"
appsv1beta1 "k8s.io/api/apps/v1beta1"
appsv1beta2 "k8s.io/api/apps/v1beta2"
authenticationv1 "k8s.io/api/authentication/v1"
authenticationv1beta1 "k8s.io/api/authentication/v1beta1"
// ...
)

var Scheme = runtime.NewScheme()

admissionregistrationv1.AddKnownTypes(Scheme)
admissionregistrationv1beta1.AddKnownTypes(Scheme)
internalv1alpha1.AddKnownTypes(Scheme)
appsv1.AddKnownTypes(Scheme)
appsv1beta1.AddKnownTypes(Scheme)
appsv1beta2.AddKnownTypes(Scheme)
authenticationv1.AddKnownTypes(Scheme)
authenticationv1beta1.AddKnownTypes(Scheme)
//...

而这在官方开发者看来仍然不够优雅。为了解决这个问题,官方开发者在apimachinery库中特地提供了runtime.SchemeBuilder类。我们先来看看这个类具体的使用方法,我们以k8s.io/api/core/v1/register.go中的用法为例:

k8s.io/api/core/v1/register.go
var (
SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
AddToScheme = SchemeBuilder.AddToScheme
)

其中:

  • NewSchemeBuilder()"吸收"一个注册函数addKnownTypes并创建出一个SchemeBuilder对象;
  • SchemeBuilderAddToScheme成员用于返回刚刚"吸收"的addKnownTypes,也就是返回的AddToScheme就是addKnownTypes函数。

看起来似乎SchemeBuilder类型只是将注册函数"左手倒右手",那它存在的意义又是什么呢? 其实NewSchemeBuilder()函数支持同时"吸收"多个注册函数:

func NewSchemeBuilder(funcs ...func(*Scheme) error) SchemeBuilder {
// ...
}

SchemeBuilderAddToScheme成员其实将"吸收"的多个注册函数在逻辑上封装成一个。 这样, 仅通过调用一次AddToSchme(scheme)就可以一次性地执行多个注册函数。

当然,如果在创建SchemeBuilder对象时只传入一个注册函数,就会造成"左手倒右手"的现象。

向全局Scheme注册原生kind

我们现在知道了官方开发者已经在k8s.io/api库中为我们事先准备了各个API分组的注册函数,并且在k8s.io/apimachinery库中也为我们提供了SchemeBuilder类型用于"优雅"地执行注册函数,我们现在来看看client-go中的全局Scheme对象是如何注册上所有原生kind的:

向全局Scheme对象注册所有原生kind
client-go/kubernetes/scheme/register.go

import (
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
admissionregistrationv1alpha1 "k8s.io/api/admissionregistration/v1alpha1"
admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1"
internalv1alpha1 "k8s.io/api/apiserverinternal/v1alpha1"
appsv1 "k8s.io/api/apps/v1"
appsv1beta1 "k8s.io/api/apps/v1beta1"
appsv1beta2 "k8s.io/api/apps/v1beta2"
authenticationv1 "k8s.io/api/authentication/v1"
authenticationv1alpha1 "k8s.io/api/authentication/v1alpha1"
authenticationv1beta1 "k8s.io/api/authentication/v1beta1"
authorizationv1 "k8s.io/api/authorization/v1"
authorizationv1beta1 "k8s.io/api/authorization/v1beta1"
autoscalingv1 "k8s.io/api/autoscaling/v1"
autoscalingv2 "k8s.io/api/autoscaling/v2"
autoscalingv2beta1 "k8s.io/api/autoscaling/v2beta1"
autoscalingv2beta2 "k8s.io/api/autoscaling/v2beta2"
batchv1 "k8s.io/api/batch/v1"
batchv1beta1 "k8s.io/api/batch/v1beta1"
certificatesv1 "k8s.io/api/certificates/v1"
certificatesv1beta1 "k8s.io/api/certificates/v1beta1"
coordinationv1 "k8s.io/api/coordination/v1"
coordinationv1beta1 "k8s.io/api/coordination/v1beta1"
corev1 "k8s.io/api/core/v1"
discoveryv1 "k8s.io/api/discovery/v1"
discoveryv1beta1 "k8s.io/api/discovery/v1beta1"
eventsv1 "k8s.io/api/events/v1"
eventsv1beta1 "k8s.io/api/events/v1beta1"
extensionsv1beta1 "k8s.io/api/extensions/v1beta1"
flowcontrolv1alpha1 "k8s.io/api/flowcontrol/v1alpha1"
flowcontrolv1beta1 "k8s.io/api/flowcontrol/v1beta1"
flowcontrolv1beta2 "k8s.io/api/flowcontrol/v1beta2"
flowcontrolv1beta3 "k8s.io/api/flowcontrol/v1beta3"
networkingv1 "k8s.io/api/networking/v1"
networkingv1alpha1 "k8s.io/api/networking/v1alpha1"
networkingv1beta1 "k8s.io/api/networking/v1beta1"
nodev1 "k8s.io/api/node/v1"
nodev1alpha1 "k8s.io/api/node/v1alpha1"
nodev1beta1 "k8s.io/api/node/v1beta1"
policyv1 "k8s.io/api/policy/v1"
policyv1beta1 "k8s.io/api/policy/v1beta1"
rbacv1 "k8s.io/api/rbac/v1"
rbacv1alpha1 "k8s.io/api/rbac/v1alpha1"
rbacv1beta1 "k8s.io/api/rbac/v1beta1"
resourcev1alpha1 "k8s.io/api/resource/v1alpha1"
schedulingv1 "k8s.io/api/scheduling/v1"
schedulingv1alpha1 "k8s.io/api/scheduling/v1alpha1"
schedulingv1beta1 "k8s.io/api/scheduling/v1beta1"
storagev1 "k8s.io/api/storage/v1"
storagev1alpha1 "k8s.io/api/storage/v1alpha1"
storagev1beta1 "k8s.io/api/storage/v1beta1"

runtime "k8s.io/apimachinery/pkg/runtime"
schema "k8s.io/apimachinery/pkg/runtime/schema"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
)

var Scheme = runtime.NewScheme()

var localSchemeBuilder = runtime.SchemeBuilder{
admissionregistrationv1.AddToScheme,
admissionregistrationv1alpha1.AddToScheme,
admissionregistrationv1beta1.AddToScheme,
internalv1alpha1.AddToScheme,
appsv1.AddToScheme,
appsv1beta1.AddToScheme,
appsv1beta2.AddToScheme,
authenticationv1.AddToScheme,
authenticationv1alpha1.AddToScheme,
authenticationv1beta1.AddToScheme,
authorizationv1.AddToScheme,
authorizationv1beta1.AddToScheme,
autoscalingv1.AddToScheme,
autoscalingv2.AddToScheme,
autoscalingv2beta1.AddToScheme,
autoscalingv2beta2.AddToScheme,
batchv1.AddToScheme,
batchv1beta1.AddToScheme,
certificatesv1.AddToScheme,
certificatesv1beta1.AddToScheme,
coordinationv1beta1.AddToScheme,
coordinationv1.AddToScheme,
corev1.AddToScheme,
discoveryv1.AddToScheme,
discoveryv1beta1.AddToScheme,
eventsv1.AddToScheme,
eventsv1beta1.AddToScheme,
extensionsv1beta1.AddToScheme,
flowcontrolv1alpha1.AddToScheme,
flowcontrolv1beta1.AddToScheme,
flowcontrolv1beta2.AddToScheme,
flowcontrolv1beta3.AddToScheme,
networkingv1.AddToScheme,
networkingv1alpha1.AddToScheme,
networkingv1beta1.AddToScheme,
nodev1.AddToScheme,
nodev1alpha1.AddToScheme,
nodev1beta1.AddToScheme,
policyv1.AddToScheme,
policyv1beta1.AddToScheme,
rbacv1.AddToScheme,
rbacv1beta1.AddToScheme,
rbacv1alpha1.AddToScheme,
resourcev1alpha1.AddToScheme,
schedulingv1alpha1.AddToScheme,
schedulingv1beta1.AddToScheme,
schedulingv1.AddToScheme,
storagev1beta1.AddToScheme,
storagev1.AddToScheme,
storagev1alpha1.AddToScheme,
}

var AddToScheme = localSchemeBuilder.AddToScheme

func init() {

utilruntime.Must(AddToScheme(Scheme))
}

在已经掌握了我们铺垫的预备知识的情况下,client-go中这段向全局Scheme对象中注册原生kind的逻辑就显得十分清晰了:

  1. k8s.io/api导入所有的API分组
  2. 调用runtime.SchemeBuilder"吸收"所有分组的预注册函数并生成一个SchemeBuilder对象:localSchemeBuilder
  3. 调用localSchemeBuilderAddToScheme成员以获取一个逻辑上包括所有预注册函数的函数:AddToScheme
  4. 将全局Scheme对象传入AddToScheme()函数:即执行所有预注册函数完成所有原生kind的注册

向全局Scheme注册特殊kind

到目前为止,我们仅介绍了client-go的全局Scheme注册k8s.io/api库中定义的原生kind的过程。 而k8s.io/api库中的kind仅包括单体种类以及集合种类。对于kind的第三种类(通用及特殊类型),它们被定义在apimachinery中。 接下来我们将介绍client-go的全局Scheme注册kind第三种类的过程。

AddToGroupVersion

k8s.io/api库中提供的预注册函数addKnownTypes()一样,apimachinery库中也提供了这些特殊kind的预注册函数,不过相比于k8s.io/api库中每个API分组中都存在一个注册函数,由于特殊kind本身就很少, apimachinery库中仅有一个注册函数叫做AddToGroupVersion()用于注册所有通用及特殊的kind,它被定义在了metav1包中:

apimachinery库中特殊kind的预注册函数
apimachinery/pkg/apis/meta/v1/register.go
// 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, optionsTypes...)
// Register Unversioned types under their own special group
scheme.AddUnversionedTypes(Unversioned,
&Status{},
&APIVersions{},
&APIGroupList{},
&APIGroup{},
&APIResourceList{},
)

// ...
}

client-go/kubernetes/scheme/register.go文件中,全局Scheme不仅注册了所有k8s.io/api中所有的kind,也注册了k8s.io/apimachinery中通用及特殊的kind

client-go/kubernetes/scheme/register.go
import (
// ...
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
runtime "k8s.io/apimachinery/pkg/runtime"
schema "k8s.io/apimachinery/pkg/runtime/schema"
)

var Scheme = runtime.NewScheme()

// ...

func init() {
v1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"})
// ...
}

client-go中的全局序列化器工厂

client-go中,也存在一个全局的序列化器工厂对象Codecsclient-go使用的正是全局Scheme而创建的它:

client-go/kubernetes/scheme/register.go
var Codecs = serializer.NewCodecFactory(Scheme)

这个全局序列化器工厂负责client-go中所有与kube-apiserver通信的编/解码工作。🎈

client-go中的全局URL参数"序列化器"

上述Codec是用于请求/返回的编解码。client-go中用于将Go对象转化为Kubernetes API URL参数(Query Parameter)的全局URL参数"序列化器"为ParameterCodec🎈:

client-go/kubernetes/scheme/register.go
ParameterCodec = runtime.NewParameterCodec(Scheme)

同样它使用的也是client-go中的全局Scheme对象。

小结

小结

其实我们使用client-go的资源客户端clientset时并不需要了解任何与序列化/反序列化有关的细节,这些细节被封装在clientset内。 我们之所以在本节中探究client-go中使用的序列化器是因为我们在编写自定义资源资源控制器时,封装完备的原生资源客户端clientset对我们来说已经没有用处了。 因此我们需要了解探究client-go背后与kube-apiserver通信的细节,而序列化/反序列化就是其中一部分。