k8s client-go快速入门教程及源代码阅读之发现客户端

文章目录

之前介绍了client-go的静态客户端,动态客户端,本文主要讲解一下发现客户端的源代码,发现客户端用于发现k8s集群现有的资源,包括但不限于内置资源,可以获取当时k8s集群中的所有资源,通过发现客户端我们可以确定当前器群存在哪些资源及其对应的版本。

快速入门

不知道大家有没有一个疑惑,在使用kubectl的时候我们不会指定资源的版本,但是kubectl怎么通过GVR/GVK定位资源呢? 其实这个问题可以由发现客户端回答,它的接口返回数据中有ServerPreferredResources, 即服务端首选资源。

kubectl会在发现客户端之上在封装一层缓存,因为每次直接从服务器获取比较消耗服务端资源。

package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"log"
	"path/filepath"

	"k8s.io/client-go/discovery"

	"k8s.io/client-go/tools/clientcmd"
	"k8s.io/client-go/util/homedir"
)

func printJson(obj interface{}) {
	data, err := json.MarshalIndent(obj, "", "  ")
	if err != nil {
		log.Fatal("序列化失败!", err)
	}
	fmt.Println(string(data))
}

func main() {
	var kubeconfig *string
	if home := homedir.HomeDir(); home != "" {
		kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) absolute path to the kubeconfig file")
	} else {
		kubeconfig = flag.String("kubeconfig", "", "absolute path to the kubeconfig file")
	}
	flag.Parse()

	config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)
	if err != nil {
		log.Fatal(err)
	}

	discoveryClient := discovery.NewDiscoveryClientForConfigOrDie(config)
	serverGroups, err := discoveryClient.ServerGroups()
	if err != nil {
		log.Fatal(err)
	}
	printJson(serverGroups)
	serverPreferredResources, err := discoveryClient.ServerPreferredResources()
	if err != nil {
		log.Fatal(err)
	}
	printJson(serverPreferredResources)
	serverPreferredNamespacedResources, err := discoveryClient.ServerPreferredNamespacedResources()
	if err != nil {
		log.Fatal(err)
	}
	printJson(serverPreferredNamespacedResources)
}

输出如下:

服务端首选资源组:{
  "kind": "APIGroupList",
  "apiVersion": "v1",
  "groups": [
    {
      "name": "apps",
      "versions": [
        {
          "groupVersion": "apps/v1",
          "version": "v1"
        }
      ],
      "preferredVersion": {
        "groupVersion": "apps/v1",
        "version": "v1"
      }
    },
    // 省略...
}
服务端首选资源:[
  {
    "groupVersion": "v1",
    "resources": [
      {
        "name": "componentstatuses",
        "singularName": "",
        "namespaced": false,
        "kind": "ComponentStatus",
        "verbs": [
          "get",
          "list"
        ],
        "shortNames": [
          "cs"
        ]
      },
      // 省略...
}
服务端首选命名空间级别资源:[
  {
    "groupVersion": "v1",
    "resources": [
      {
        "name": "persistentvolumeclaims",
        "singularName": "",
        "namespaced": true,
        "kind": "PersistentVolumeClaim",
        "verbs": [
          "create",
          "delete",
          "deletecollection",
          "get",
          "list",
          "patch",
          "update",
          "watch"
        ],
        "shortNames": [
          "pvc"
        ],
        "storageVersionHash": "QWTyNDq0dC4="
      },
      // 省略
}

发现客户端的使用是比较简单的,所以这段代码就不需要更多的解释了。

手动请求

其实也可以手动获取k8s集群内的所有资源版本信息

首先使用下面命令,代理k8s请求,这样就不需要认证了。

kubectl proxy
# 输出如下
Starting to serve on 127.0.0.1:8001

然后访问/api/v1路径, 即列出core组的v1版本所有资源

127.0.0.1:8001/api/v1

输出和上面是差不多的,所以这里就不赘述了。

客户端构造

代码如下:

discoveryClient := discovery.NewDiscoveryClientForConfigOrDie(config)

func NewDiscoveryClientForConfig(c *restclient.Config) (*DiscoveryClient, error) {
    // 浅拷贝
	config := *c
    // UnversionedRESTClientFor  即不指定版本
	client, err := restclient.UnversionedRESTClientFor(&config)
	return &DiscoveryClient{restClient: client, LegacyPrefix: "/api"}, err
}

func UnversionedRESTClientFor(config *Config) (*RESTClient, error) {
	// 前面和RESTClient几乎一致
    // 这里的gv是meta.k8s.io
	gv := metav1.SchemeGroupVersion
	if config.GroupVersion != nil {
		gv = *config.GroupVersion
	}
	clientContent := ClientContentConfig{
		AcceptContentTypes: config.AcceptContentTypes,
		ContentType:        config.ContentType,
		GroupVersion:       gv,
		Negotiator:         runtime.NewClientNegotiator(config.NegotiatedSerializer, gv),
	}
	restClient, err := NewRESTClient(baseURL, versionedAPIPath, clientContent, rateLimiter, httpClient)
	return restClient, err
}

构造的过程和RESTClient几乎一致,不同点主要是gv和Negotiator。

资源请求

这里以ServerGroups的资源请求为例,其他两个大致是差不多的。

代码如下:

func (d *DiscoveryClient) ServerGroups() (apiGroupList *metav1.APIGroupList, err error) {
	// 1. 
	v := &metav1.APIVersions{}
	err = d.restClient.Get().AbsPath(d.LegacyPrefix).Do(context.TODO()).Into(v)
	apiGroup := metav1.APIGroup{}
	if err == nil && len(v.Versions) != 0 {
		apiGroup = apiVersionsToAPIGroup(v)
	}

	// 2.
	apiGroupList = &metav1.APIGroupList{}
	err = d.restClient.Get().AbsPath("/apis").Do(context.TODO()).Into(apiGroupList)
	if err != nil && !errors.IsNotFound(err) && !errors.IsForbidden(err) {
		return nil, err
	}

	// 3.
	if len(v.Versions) != 0 {
		apiGroupList.Groups = append([]metav1.APIGroup{apiGroup}, apiGroupList.Groups...)
	}
	return apiGroupList, nil
}

代码解析如下:

  1. 请求/api路径的资源,即列出core组下的所有资源
  2. 请求/apis路径下的资源,即列出其他组的所有资源
  3. 将两者组合到一起

总结

由于k8s是一个可扩展的平台,所以不能够硬编码资源版本信息,因此k8s提供了发现机制用于客户端查询当前集群中的所有资源,值得注意的是,使用kubectl如果查询不到自己添加的CRD, 可能是缓存的原因。

参考链接