k8s client-go快速入门教程及源代码阅读之RESTMapper

文章目录

虽然k8s的发现客户端可以实时的获取k8s集群的所有资源,但是并不是直接面向用户的接口,因为解析这些资源组版本是一个乏味和无趣的过程,所以在此基础上client-go提供了RESTMapper来帮助用户映射和发现要找的资源。

快速入门

通过restmapper我们可以在仅知道资源名的情况下找到适合的GVK

package main

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

	"k8s.io/client-go/discovery"

	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/client-go/discovery/cached/memory"
	"k8s.io/client-go/restmapper"
	"k8s.io/client-go/tools/clientcmd"
	"k8s.io/client-go/util/homedir"
)

func printJson(obj interface{}, err error) {
	if err != nil {
		log.Fatal(err)
	}
	data, err := json.Marshal(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)
	mapper := restmapper.NewDeferredDiscoveryRESTMapper(memory.NewMemCacheClient(discoveryClient))
	printJson(mapper.KindFor(schema.GroupVersionResource{Group: "", Version: "", Resource: "deployment"}))
}

输出如下:

{"Group":"apps","Version":"v1","Kind":"Deployment"}

但是我们在使用kubectl的时候,其实不需要总是完整的输入资源名,比如使用deploy代替deployment,这是因为deploydeployment的缩写,不过RESTMapper并不能通过缩写直接找到对应的GVK, 在此之前我们需要先补全,或者说放大(Expand)

代码如下:

func main() {
	discoveryClient := discovery.NewDiscoveryClientForConfigOrDie(config)
	mapper := restmapper.NewDeferredDiscoveryRESTMapper(memory.NewMemCacheClient(discoveryClient))
    // 用于将缩写转成完整名的expander
	expander := restmapper.NewShortcutExpander(mapper, discoveryClient)
	printJson(expander.ResourcesFor(schema.GroupVersionResource{Group: "", Version: "", Resource: "deploy"}))
	printJson(mapper.KindFor(schema.GroupVersionResource{Group: "", Version: "", Resource: "deployment"}))
}

输出如下:

[{"Group":"apps","Version":"v1","Resource":"deployments"}]
{"Group":"apps","Version":"v1","Kind":"Deployment"}

当然了,即使不是缩写。你也可以先用expander补全一下,直接使用expander是更常见的操作

源代码

从上文我们知道,restmapper有两个比较重要的对象RESTMapperexpander, 前者用于将GVR(可以只写R)映射到GVK, 而后者用于将缩写补全。

所以这里源代码可以分为两个部分,对象的初始化,和方法的映射。

RESTMapper初始化

RESTMapper是建立在发现客户端的基础上实现的,所以需要传入发现客户端,而发现客户端会比较频繁的访问apiserver, 所以为了减少不必要的请求,restmapper提供了多种缓存机制,比如内存缓存和磁盘缓存。

kubectl默认使用磁盘缓存

restmapper.NewDeferredDiscoveryRESTMapper(memory.NewMemCacheClient(discoveryClient))

func NewMemCacheClient(delegate discovery.DiscoveryInterface) discovery.CachedDiscoveryInterface {
	return &memCacheClient{
		delegate:               delegate,
		groupToServerResources: map[string]*cacheEntry{},
	}
}

func NewDeferredDiscoveryRESTMapper(cl discovery.CachedDiscoveryInterface) *DeferredDiscoveryRESTMapper {
	return &DeferredDiscoveryRESTMapper{
		cl: cl,
	}
}

这里使用了k8s常用的委托模式,一层一层的封装,啥事也不用干,就是套娃,当然了,这种套娃还是有意义的。

KindsFor

expander还有许多有用的方法,比如RESTMapping, 但是内部逻辑其实大同小异,输出不一样而已,这里看看ResourcesFor的代码逻辑。

func (d *DeferredDiscoveryRESTMapper) KindFor(resource schema.GroupVersionResource) (gvk schema.GroupVersionKind, err error) {
    // 1.
	del, err := d.getDelegate()
    // 2.
	gvk, err = del.KindFor(resource)
    // 3.
	if err != nil && !d.cl.Fresh() {
		d.Reset()
		gvk, err = d.KindFor(resource)
	}
	return
}

代码分解如下:

  1. 获取委托对象,首次调用的时候还没初始化,只有memCacheClient封装的discoveryClient
  2. 调用委托对象的ResourceFor方法
  3. 判断是否需要重新刷新, 如果没有刷新机制那么可能会导致无法映射CRD资源

k8s的代码设计还是很有借鉴意义的,各个对象做的事情比较单一,比如DeferredDiscoveryRESTMapper用于判断是否要重新获取资源列表,memCacheClient用于缓存资源列表,而discoveryClient只管获取。

func (d *DeferredDiscoveryRESTMapper) getDelegate() (meta.RESTMapper, error) {
	d.initMu.Lock()
	defer d.initMu.Unlock()
	// 1.
	if d.delegate != nil {
		return d.delegate, nil
	}
	// 2.
	groupResources, err := GetAPIGroupResources(d.cl)
	// 3.
	d.delegate = NewDiscoveryRESTMapper(groupResources)
	return d.delegate, err
}


func GetAPIGroupResources(cl discovery.DiscoveryInterface) ([]*APIGroupResources, error) {
    // 4.
	gs, rs, err := cl.ServerGroupsAndResources()
	rsm := map[string]*metav1.APIResourceList{}
	for _, r := range rs {
		rsm[r.GroupVersion] = r
	}

	var result []*APIGroupResources
	for _, group := range gs {
		groupResources := &APIGroupResources{
			Group:              *group,
			VersionedResources: make(map[string][]metav1.APIResource),
		}
		for _, version := range group.Versions {
			resources, ok := rsm[version.GroupVersion]
			if !ok {
				continue
			}
			groupResources.VersionedResources[version.Version] = resources.APIResources
		}
		result = append(result, groupResources)
	}
	return result, nil
}

代码分解如下:

  1. 判断是否已经构造过restmapper对象
  2. 从client中获取APIGroupResources,这里的client是memCacheClient
  3. 构造restmapper对象
  4. 获取APIGroupResources的逻辑就是, 从k8s集群中获取各种资源组信息,然后组合起来

匹配和构造优先级restmapper对象的逻辑比较繁琐这里就略过了。

ResourcesFor

其实expander也有KindsFor,为了不重复的代码就看ResourcesFor了,其实两者代码差不多。

// 1. 
func NewShortcutExpander(delegate meta.RESTMapper, client discovery.DiscoveryInterface) meta.RESTMapper { 
	return shortcutExpander{RESTMapper: delegate, discoveryClient: client}
}

func (e shortcutExpander) KindFor(resource schema.GroupVersionResource) (schema.GroupVersionKind, error) {
    // 2.
	return e.RESTMapper.KindFor(e.expandResourceShortcut(resource))
}

// 3.
func (d *DeferredDiscoveryRESTMapper) KindFor(resource schema.GroupVersionResource) (gvk schema.GroupVersionKind, err error) {
	del, err := d.getDelegate()
	if err != nil {
		return schema.GroupVersionKind{}, err
	}
	gvk, err = del.KindFor(resource)
	if err != nil && !d.cl.Fresh() {
		d.Reset()
		gvk, err = d.KindFor(resource)
	}
	return
}

func (e shortcutExpander) expandResourceShortcut(resource schema.GroupVersionResource) schema.GroupVersionResource {
    // 4. 
	if allResources, shortcutResources, err := e.getShortcutMappings(); err == nil {
		// 省略构造过程
	}

	return resource
}

func (e shortcutExpander) getShortcutMappings() ([]*metav1.APIResourceList, []resourceShortcuts, error) {
	res := []resourceShortcuts{}
    // 5.
	_, apiResList, err := e.discoveryClient.ServerGroupsAndResources()
    
	for _, apiResources := range apiResList {
		// 省略构造过程..
	}

	return apiResList, res, nil
}

代码分解如下:

  1. NewShortcutExpander的构建其实也可以传NewMemCacheClient封装的discoveryClient。
  2. 在将缩写补全后,直接调用传入的RESTMapper对象对应的KindFor
  3. 和之前的ResourcesFor大同小异
  4. 获取资源组缩写的映射关系
  5. 通过client获取资源组列表,然后构造一个可以映射对象

k8s的各资源缩写在资源列表的ShortNames字段,虽然发现客户端那篇文章已经贴过响应内容了,但还是在贴一下吧

  {
    "groupVersion": "v1",
    "resources": [
      {
        "name": "componentstatuses",
        "singularName": "",
        "namespaced": false,
        "kind": "ComponentStatus",
        "verbs": [
          "get",
          "list"
        ],
        "shortNames": [
          "cs"
        ]
      },
      // 省略...
}

可以看到componentstatuses的缩写是cs

总结

有了restmapper我们就可以快速的找到GVK和GVR。

一般情况下我们使用expander就足够了,因为它是在RESTMapper对象基础上创建的,不过要注意的是,因为还要传一个client对象,这个client对象最好也用缓存对象封装一下,不然会多请求一遍资源组对象。

参考链接