介绍 Kubernetes 开发中用到的不同类型的 Client

1. 概述

在设计 Kubernetes 的控制器时,我们需要通过 client 对 Kubernetes 的原生资源或者自定义资源执行一些 CURD 操作。

使用 Golang 进行开发时,kubernetes 官方提供了不同类型的 Client 以应对不同场景,本文以下内容一一介绍这些 Client。

2. Client

1. REST Client

RESTClient 是 k8s.io/client-go 中用于和 kube-apiserver 进行 restful 交互的基础类,client-go 中其他类型的 Client 类都是基于该类型的二次封装。

RESTClient 中封装了 golang 的 http.Client 对象,但是设计它并非纯粹为了发送 HTTP 请求,同时还具备以下功能:

  • 序列化和反序列化
  • 封装 GVR、GVK、Schema 之间的映射关系,即自动填写访问指定资源的URL
  • 处理 HTTP 连接的超时、重试等机制
  • 封装 Watch、Stream 接口

以下是使用 REST Client 获取命名空间中所有 Pod 的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package main

import (
	"context"
	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"
	"k8s.io/klog/v2"
	"time"
)

func main() {

	kubeconfig := "./kubeconfig"
	config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
	if err != nil {
		klog.Fatalf("Error building kubernetes clientset: %s", err.Error())
	}

	gv := corev1.SchemeGroupVersion
	config.GroupVersion = &gv
	// 只有 k8s.io/api/core/v1 包中的 APIPath 指定为 /api,其余所有 Resource 的 APIPath 均为 /apis
	config.APIPath = "/api"
	config.NegotiatedSerializer = scheme.Codecs.WithoutConversion() // 指定序列化类型
	config.UserAgent = rest.DefaultKubernetesUserAgent()

	coreV1Cli, err := rest.RESTClientFor(config)
	if err != nil {
		klog.Fatalf("Error building rest client: %s", err.Error())
	}

	pods := &corev1.PodList{}
	opts := &metav1.ListOptions{}

	if err := coreV1Cli.Get().
		Resource("pods").
		Namespace("rccp").
		VersionedParams(opts, scheme.ParameterCodec).
		Timeout(time.Second * 60).
		Do(context.Background()).
		Into(pods); err != nil {
		klog.Fatalf("Error list pod: %s", err.Error())
	}

	for _, pod := range pods.Items {
		klog.Info(pod.Name)
	}

}

2. ClientSet

ClientSet 是进行控制器开发时最常用的 Client。无论是 k8s.io/client-go 或者自定义项目,ClientSet 的代码通常都是使用 k8s.io/code-generator 自动生成的,其本质上是对 rest.RESTClient 的二次封装。

从上一小节可以看到,创建 RESTClient 时我们需要手工指定 SchemeGroupVersion 、APIPath 、NegotiatedSerializer 等参数,发送请求时还需要通过 Resource 函数手工指定资源名称。

ClientSet 为我们屏蔽了这些细节,用户只需要针对指定 GVR 调用 GET/LIST/CREATE/DELETE/UPDATE 方法即可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

type Interface interface {
	Discovery() discovery.DiscoveryInterface
	SamplecontrollerV1alpha1() samplecontrollerv1alpha1.SamplecontrollerV1alpha1Interface
}

type SamplecontrollerV1alpha1Interface interface {
	RESTClient() rest.Interface
	FoosGetter
}

// FoosGetter has a method to return a FooInterface.
// A group's client should implement this interface.
type FoosGetter interface {
	Foos(namespace string) FooInterface
}

// FooInterface has methods to work with Foo resources.
type FooInterface interface {
	Create(ctx context.Context, foo *v1alpha1.Foo, opts v1.CreateOptions) (*v1alpha1.Foo, error)
	Update(ctx context.Context, foo *v1alpha1.Foo, opts v1.UpdateOptions) (*v1alpha1.Foo, error)
	UpdateStatus(ctx context.Context, foo *v1alpha1.Foo, opts v1.UpdateOptions) (*v1alpha1.Foo, error)
	Delete(ctx context.Context, name string, opts v1.DeleteOptions) error
	DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error
	Get(ctx context.Context, name string, opts v1.GetOptions) (*v1alpha1.Foo, error)
	List(ctx context.Context, opts v1.ListOptions) (*v1alpha1.FooList, error)
	Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error)
	Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.Foo, err error)
	FooExpansion
}

上述接口是 k8s.io/sample-controller 项目自定义类型的 ClientSet 接口,其中 SamplecontrollerV1alpha1Interface 包含了 samplecontroller/v1alpha1 中所有类型的 CURD 接口。

SamplecontrollerV1alpha1Interface 的实现类包含了一个 rest.RESTClient 对象,并且通过 NewForConfig 和 setConfigDefaults 接口为 rest.RESTClient 执行了必要的初始化。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

// SamplecontrollerV1alpha1Client is used to interact with features provided by the samplecontroller.k8s.io group.
type SamplecontrollerV1alpha1Client struct {
	restClient rest.Interface
}

// NewForConfig creates a new SamplecontrollerV1alpha1Client for the given config.
func NewForConfig(c *rest.Config) (*SamplecontrollerV1alpha1Client, error) {
	config := *c
	if err := setConfigDefaults(&config); err != nil {
		return nil, err
	}
	client, err := rest.RESTClientFor(&config)
	if err != nil {
		return nil, err
	}
	return &SamplecontrollerV1alpha1Client{client}, nil
}

func setConfigDefaults(config *rest.Config) error {
	gv := v1alpha1.SchemeGroupVersion
	config.GroupVersion = &gv
	config.APIPath = "/apis"
	config.NegotiatedSerializer = scheme.Codecs.WithoutConversion()

	if config.UserAgent == "" {
		config.UserAgent = rest.DefaultKubernetesUserAgent()
	}

	return nil
}

3. Dynamic Client

Dynamic Client 是一种类型无关的 Client,定义在 k8s.io/client-go/dynamic 包中,只处理 Unstructured 类型的对象。

Unstructured 类型是 json.Unmarshal 的包装类,使用 map[string]interface{} 类型保存 Object,使用 []]interface{} 保存数组:

1
2
3
4
5
6
type Unstructured struct {
	// Object is a JSON compatible map with string, float, int, bool, []interface{}, or
	// map[string]interface{}
	// children.
	Object map[string]interface{}
}

k8s.io/apimachinery/pkg/apis/meta/v1/unstructured 定义了大量 Unstructured 的辅助函数,用户可以通过以下接口快速获取 json.Unmarshal 中的指定属性:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11

func NestedFieldCopy(obj map[string]interface{}, fields ...string) (interface{}, bool, error)
func NestedFieldNoCopy(obj map[string]interface{}, fields ...string) (interface{}, bool, error)

func NestedString(obj map[string]interface{}, fields ...string) (string, bool, error) 

func NestedMap(obj map[string]interface{}, fields ...string) (map[string]interface{}, bool, error) 
func nestedMapNoCopy(obj map[string]interface{}, fields ...string) (map[string]interface{}, bool, error)

...

示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package main

import (
	"context"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/client-go/dynamic"
	"k8s.io/client-go/tools/clientcmd"
	"k8s.io/klog/v2"
)

func main() {

	kubeconfig := "./kubeconfig"
	master := ""

	config, err := clientcmd.BuildConfigFromFlags(master, kubeconfig)
	if err != nil {
		klog.Fatalf("Error building kubernetes clientset: %s", err.Error())
	}

	dyClient := dynamic.NewForConfigOrDie(config)

	podGVR := schema.GroupVersionResource{
		Group:    "",
		Version:  "v1",
		Resource: "pods",
	}

	pods, err := dyClient.Resource(podGVR).Namespace("rccp").List(context.Background(), metav1.ListOptions{})
	if err != nil {
		klog.Fatalf("Error get pods: %s", err.Error())
	}
	for _, pod := range pods.Items {
		name := pod.GetName()
		podIp, _, _ := unstructured.NestedString(pod.Object, "status", "podIP")
		klog.Infof("Pod %-60s : %s", name, podIp)
	}

}

client-go 同样提供了 Unstructured 类型的 Informer 和 Lister ,使用例子参考 sample/dynamic

4. 其他 Client

除了上述 kubernetes 项目提供的原生客户端,sigs.k8s.io/controller-runtime 包中也提供了不同类型的 client。

sigs.k8s.io/controller-runtime 是 Kubebuilder 和 Operator SDK 的基础依赖,该项目提供了大量开发 controller 时的便利接口。

参考:sigs.k8s.io/controller-runtime

参考