Skip to main content

RESTClient

本节的动机与我们在client-go与apimachinery一样, client-go中封装原完备资源客户端clientset仅针对原生的Kubernetes资源类型。 我们需要探究client-go背后与kube-apiserver通信的细节,这样我们才能"照猫画虎"——实现一个针对自定义资源的客户端。 client-go中的RESTClient就是背后细节的一部分。

当然,正如我们在GVK小节就已经提及过的一样,理论上,只要我们有与kube-apiserver进行TLS加密通信的证书和密钥,我们完全可以不借助client-go而使用其他任何语言与kube-apiserver交互。 不过,这些内容不在本书的讨论范围之内。本书的主旨是帮助读者理解kubebuilder等控制器框架背后的机制。正因为这些框架本身基于client-goapimachinery库构建, 探究这两个库背后的细节也成为我们必须要经历的过程。

Clientset与RESTClient

前言中,我们要求阅读本书需要掌握的预备知识包括会使用client-go中的ClientsetClientset顾名思义是Kubernetes所有内置资源类型的客户端的集合,正如它的定义一样:

k8s.io/client-go/kubernetes/clientset.go
type Clientset struct {
appsV1 *appsv1.AppsV1Client
appsV1beta1 *appsv1beta1.AppsV1beta1Client
appsV1beta2 *appsv1beta2.AppsV1beta2Client
// ...

我们可以看到Clientset包含了所有内置API分组(及版本)1的客户端。 请注意,根据其中每个客户端的命名,每个客户端其实的对应的是一个API组,而并不是具体到某一种资源类型

我们再稍微深入探究一下每个API组客户端的类型,例如appsv1.AppsV1Client

k8s.io/client-go/kubernetes/typed/apps/v1/apps_client.go
type AppsV1Client struct {
restClient rest.Interface
}

其中rest.Interface2是一个表达HTTP RESTFul动词的通用接口,正如它的定义一样:

k8s.io/client-go/rest/client.go
type Interface interface {
GetRateLimiter() flowcontrol.RateLimiter
Verb(verb string) *Request
Post() *Request
Put() *Request
Patch(pt types.PatchType) *Request
Get() *Request
Delete() *Request
APIVersion() schema.GroupVersion
}

client-go中,RESTClientrest.Interface接口的标准实现。 所以,API组客户端实际上是对client-go中更为基础的RESTful客户端组件RESTClient的一种封装。

在了解了这个事实之后,我们自然而然地猜测RESTClient类型本身初始化的"粒度"可能就是GV。那么事实是否也是这样呢? 根据RESTClient的初始化函数的签名:

client-go/rest/config.go
func RESTClientFor(config *Config) (*RESTClient, error) {
// ...
}

可以看出,rest.Config结构被用于配置及初始化RESTClient

接下来我们将稍微探究一下rest.Config类型。 rest.Config又嵌入了一个叫做ContentConfig的结构:

k8s.io/client-go/rest/config.go
type Config struct {
ContentConfig
// ...
}

而根据ContentConfig结构的定义:

k8s.io/client-go/rest/config.go
type ContentConfig struct {
GroupVersion *schema.GroupVersion
// ...
}

其中GroupVersion成员说明了ContentConfig结构的"粒度"正是GV。因此,RESTClient类型的粒度也可以被证实为GV。

下图总结了RESTClient的初始化函数RESTClientFor()的"配置链"的关系:

k8s.io/client-go/rest/config.gotype Config struct { // ...}k8s.io/client-go/rest/config.gotype ContentConfig struct { // ...}embededContentConfigGroupVersion *schema.GroupVersion©Xudong Wang🤖️🎈k8s.io/client-go/rest/client.gotype RESTClient struct { // ...}RESTClientFor()
tip

我们从clientset的角度切入,介绍了client-go中的API组客户端, 再从API组客户端引入client-go的"真正"的HTTP客户端基础组件RESTClient,意在说明RESTClient初始化"粒度"是GroupVersion

RESTClient基本用法🎈

下面我们将通过一个极为简单的例子来介绍如何使用RESTClient类型直接与Kubernetes API交互。

package main

import (

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
)

func main() {

config, _ := clientcmd.BuildConfigFromFlags("", "/root/.kube/config")

config.APIPath = "/api"
config.GroupVersion = &corev1.SchemeGroupVersion
config.NegotiatedSerializer = scheme.Codecs.WithoutConversion()

restClient, _ := rest.RESTClientFor(config)

result := &corev1.PodList{}

// list
restClient.
Get().
Namespace("default").
Resource("pods").
Name("pod").
VersionedParams(&metav1.ListOptions{}, scheme.ParameterCodec).
Do().
Into(result)

}

直接使用RESTClient类型与Kubernetes API交互主要分为两部分:

  1. 第一部分主要是通过rest.Config对象来初始化一个RESTClient实例。

    • clientcmd.BuildConfigFromFlags() 3用于创建并返回一个rest.Config对象,同时将.kube/config文件中记录的信息转化为Config对象中的Host字段以及用于与kube-apiserverTLS加密通信的有关字段。

    • 设置Kubernetes API URL Path的根路径, 通常为/apis,但是对于核心组这个API分组来说,由于历史原因,它对应的API根路径则是/api

    • 设置资源的API分组及版本信息——正如我们上节证实的那样:RESTClient的配置"粒度"是GV

    • 指定用于HTTP请求与返回体编/解码的序列化器:

      • 所用的序列化器工厂scheme.Codecs正是之前client-go中的全局序列化器工厂章节中所说的全局序列化工厂Codec
      • 另外,不同于kube-apiserver服务端,客户端在对资源编码前/解码后不需要再额外进行资源版本间的转换,所以在这里我们使用了CodecWithoutConversion()方法获取不进行资源版本转化的序列化器。

      可以说我们在序列化器与序列化工厂以及client-go与apimachinery小节的所有内容都是为了这一行代码所做的铺垫。

  1. 第二部分则是调用RESTClient相关方法用于实际向Kubernetes API发起请求。 可以看出,RESTClient发送请求的代码基于建造者模式(builder pattern),具体的资源类型,命名空间等是在向Kubernetes API发起请求时指定。 一个稍微特殊的方法是VersionedParams(),它用于将metav1.ListOptions对象"解码"为HTTP URL中查询参数(query parameter),它的第二个参数用于指定解码器,而这个解码器正是之前client-go中的全局url参数序列化器章节中提及的全局URL参数序列化器ParameterCodec。 这些填入的信息(包括根路径,GroupVersion,资源类型,命名空间等)最终将被用于组成HTTP请求的URL(以及URL中的查询参数)。

Request类型

我们有必要在这里稍微提及一个可能被忽略的细节。 在上述的例子中,我们说RESTClient基于建造者模式。其实更准确的说法是基于建造者模式的是client-go中的Request类型而非RESTClient

我们可以再回过头来具体看看RESTClient的接口rest.Interface,可以发现RESTClientPost()Get()Delete()等方法其实返回是Request类型(指针),而不是RESTClient类型自身。 真正执行向Kubernetes API发送请求动作的其实是Request类型。 上述例子中的Namespace()Resource()Name()VersionedParams()等方法其实是Request类型的方法,这些方法返回的也是Request类型本身。所以RESTClient的链式调用其实在背后基于的是Request类型的建造者模式。

需要注意的是Request类型包括一个完整的RESTClient结构:

client-go/rest/request.go
type Request struct {
c *RESTClient

// ...
}

这就意味着,Request结构将可以使用RESTClient中所携带的配置及组件与Kubernetes API通信:例如,协商序列化器,TLS加密通信的证书等等。 所以,现在看来,RESTClient更像是一个携带各种配置及工具的"壳",它调用Post()Get()Delete()等方法将自身转化为Request类型的一部分。

另外一个需要注意的细节是,Request类型的Do()方法则有所不同,它返回的是一个叫做Result的类型。 以下动作在Do()方法中完成:

  • 实际向Kubernetes API发送HTTP请求
  • 在拿到HTTP返回后,Request使用其RESTClient成员中的协商序列化器再将返回体反序列化,并进一步封装成Result类型并返回

换个角度来说,在Do()之前所有的链式调用其实都在为组装发往Kubernetes API的HTTP请求而准备。

下图以Get()方法为例总结了RESTClient的链式调用中各类型之间的关系:

资源客户端

其实除了API组客户端外,client-go中也为每个资源类型封装了一个客户端,例如为pods封装的客户端:

k8s.io/client-go/kubernetes/typed/core/v1/pod.go
type pods struct {
client rest.Interface
ns string
}

可以看出,pods客户端也是对基础组件RESTClient的封装,而且相比于API组客户端,多了一个有关命名空间的(namespace)的成员变量。 另外,通过它对应的接口:

type PodInterface interface {
Create(ctx context.Context, pod *v1.Pod, opts metav1.CreateOptions) (*v1.Pod, error)
Update(ctx context.Context, pod *v1.Pod, opts metav1.UpdateOptions) (*v1.Pod, error)
UpdateStatus(ctx context.Context, pod *v1.Pod, opts metav1.UpdateOptions) (*v1.Pod, error)
Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error
Get(ctx context.Context, name string, opts metav1.GetOptions) (*v1.Pod, error)
List(ctx context.Context, opts metav1.ListOptions) (*v1.PodList, error)
Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error)
Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v1.Pod, err error)
// ...
}

我们可以看出资源客户端为某一资源类型的增删改查都封装了相应的方法。并且在实现上,封装的各个方法中其实都使用了RESTClient这个基础组件,例如pods资源客户端的Get()方法:

k8s.io/client-go/kubernetes/typed/core/v1/pod.go
func (c *pods) Get(ctx context.Context, name string, options metav1.GetOptions) (result *v1.Pod, err error) {
result = &v1.Pod{}
err = c.client.Get().
Namespace(c.ns).
Resource("pods").
Name(name).
VersionedParams(&options, scheme.ParameterCodec).
Do(ctx).
Into(result)
return
}
tip

除了增删改查以外,其中资源客户端封装的Watch()UpdateStatus()方法也十分重要,我们在后续章节还会介绍。 在这里,你只需要了解client-go为每个资源类型也单独封装了一个客户端。

client-go中各客户端类型之间的关系

到现在为止,我们还没有介绍API组客户端资源客户端的关系,我们只要稍微探究API组客户端的方法就会知道它们之间的关系了——为了阅读上的连贯性,在Clientset与RESTClient小节中我们只是介绍了API组客户端类型的结构体,并没有提及它的方法。 我们还是以appsv1.AppsV1Client组客户端为例:

k8s.io/client-go/kubernetes/typed/apps/v1/apps_client.go

type AppsV1Client struct {
restClient rest.Interface
}

func (c *AppsV1Client) DaemonSets(namespace string) DaemonSetInterface {
return newDaemonSets(c, namespace)
}

func (c *AppsV1Client) Deployments(namespace string) DeploymentInterface {
return newDeployments(c, namespace)
}

func (c *AppsV1Client) ReplicaSets(namespace string) ReplicaSetInterface {
return newReplicaSets(c, namespace)
}
// ...

它所封装的方法以组内的各资源类型为方法名,以命名空间为参数,并返回各资源客户端。

Clientset,API组客户端,资源客户端,RESTClient的关系可以被总结成下图所示:

其实,clientset的链式调用本质上就是封装的不同客户端之间的转化,最终由RESTClient类型完成与Kubernetes API的交互。 例如对于如下clientset的链式调用:

clientset.AppsV1().Deployments("default").Get(context.TODO(),  metav1.GetOptions{})

客户端类型的转化关系如下图所示:

小结

tip

虽然在本节中我们介绍了client-go中的多个客户端类型,但是本节的重点依然是RESTClient的使用方法。 原因是不管是Clientset还是API组客户端,亦或是资源客户端,它们都是为Kubernetes原生资源封装的客户端。 对于自定义资源,我们不得不利用更为底层的RESTClient来编写客户端。


  1. 为了叙述上的方便,在接下来我们会把API组及其版本简称为API组,GV或者GroupVersion

  2. restclient-go库中的rest包。

  3. 例子中给的是运行在Kubernetes集群外的客户端例子,对于已知要运行在集群中的客户端,需要使用rest.InClusterConfig()方法来创建rest.Config对象。