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-go
和apimachinery
库构建,
探究这两个库背后的细节也成为我们必须要经历的过程。
Clientset与RESTClient
在前言中,我们要求阅读本书需要掌握的预备知识包括会使用client-go
中的Clientset
。Clientset
顾名思义是Kubernetes所有内置资源类型的客户端的集合,正如它的定义一样:
type Clientset struct {
appsV1 *appsv1.AppsV1Client
appsV1beta1 *appsv1beta1.AppsV1beta1Client
appsV1beta2 *appsv1beta2.AppsV1beta2Client
// ...
我们可以看到Clientset
包含了所有内置API分组(及版本)1的客户端。
请注意,根据其中每个客户端的命名,每个客户端其实的对应的是一个API组,而并不是具体到某一种资源类型。
我们再稍微深入探究一下每个API组客户端的类型,例如appsv1.AppsV1Client
:
type AppsV1Client struct {
restClient rest.Interface
}
其中rest.Interface
2是一个表达HTTP RESTFul动词的通用接口,正如它的定义一样:
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
中,RESTClient
是rest.Interface
接口的标准实现。
所以,API组客户端实际上是对client-go
中更为基础的RESTful客户端组件RESTClient
的一种封装。
在了解了这个事实之后,我们自然而然地猜测RESTClient
类型本身初始化的"粒度"可能就是GV。那么事实是否也是这样呢?
根据RESTClient
的初始化函数的签名:
func RESTClientFor(config *Config) (*RESTClient, error) {
// ...
}
可以看出,rest.Config
结构被用于配置及初始化RESTClient
。
接下来我们将稍微探究一下rest.Config
类型。
rest.Config
又嵌入了一个叫做ContentConfig
的结构:
type Config struct {
ContentConfig
// ...
}
而根据ContentConfig
结构的定义:
type ContentConfig struct {
GroupVersion *schema.GroupVersion
// ...
}
其中GroupVersion
成员说明了ContentConfig
结构的"粒度"正是GV。因此,RESTClient
类型的粒度也可以被证实为GV。
下图总结了RESTClient
的初始化函数RESTClientFor()
的"配置链"的关系:
我们从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交互主要分为两部分:
第一部分主要是通过
rest.Config
对象来初始化一个RESTClient
实例。clientcmd.BuildConfigFromFlags()
3用于创建并返回一个rest.Config
对象,同时将.kube/config
文件中记录的信息转化为Config
对象中的Host
字段以及用于与kube-apiserver
TLS加密通信的有关字段。设置Kubernetes API URL Path的根路径, 通常为
/apis
,但是对于核心组这个API分组来说,由于历史原因,它对应的API根路径则是/api
设置资源的API分组及版本信息——正如我们上节证实的那样:
RESTClient
的配置"粒度"是GV指定用于HTTP请求与返回体编/解码的序列化器:
- 所用的序列化器工厂
scheme.Codecs
正是之前client-go中的全局序列化器工厂章节中所说的全局序列化工厂Codec
。 - 另外,不同于
kube-apiserver
服务端,客户端在对资源编码前/解码后不需要再额外进行资源版本间的转换,所以在这里我们使用了Codec
的WithoutConversion()
方法获取不进行资源版本转化的序列化器。
注可以说我们在序列化器与序列化工厂以及client-go与apimachinery小节的所有内容都是为了这一行代码所做的铺垫。
- 所用的序列化器工厂
- 第二部分则是调用
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
,可以发现RESTClient
的Post()
,Get()
,Delete()
等方法其实返回是Request
类型(指针),而不是RESTClient
类型自身。
真正执行向Kubernetes API发送请求动作的其实是Request
类型。
上述例子中的Namespace()
,Resource()
,Name()
,VersionedParams()
等方法其实是Request
类型的方法,这些方法返回的也是Request
类型本身。所以RESTClient
的链式调用其实在背后基于的是Request
类型的建造者模式。
需要注意的是Request
类型包括一个完整的RESTClient
结构:
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
封装的客户端:
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()
方法:
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
}
除了增删改查以外,其中资源客户端封装的Watch()
和UpdateStatus()
方法也十分重要,我们在后续章节还会介绍。
在这里,你只需要了解client-go
为每个资源类型也单独封装了一个客户端。
client-go中各客户端类型之间的关系
到现在为止,我们还没有介绍API组客户端与资源客户端的关系,我们只要稍微探究API组客户端的方法就会知道它们之间的关系了——为了阅读上的连贯性,在Clientset与RESTClient小节中我们只是介绍了API组客户端类型的结构体,并没有提及它的方法。
我们还是以appsv1.AppsV1Client
组客户端为例:
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{})
客户端类型的转化关系如下图所示:

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