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
,这是因为deploy
是deployment
的缩写,不过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有两个比较重要的对象RESTMapper
和expander
, 前者用于将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
}
代码分解如下:
- 获取委托对象,首次调用的时候还没初始化,只有memCacheClient封装的discoveryClient
- 调用委托对象的ResourceFor方法
- 判断是否需要重新刷新, 如果没有刷新机制那么可能会导致无法映射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
}
代码分解如下:
- 判断是否已经构造过restmapper对象
- 从client中获取
APIGroupResources
,这里的client是memCacheClient - 构造
restmapper
对象 - 获取
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
}
代码分解如下:
- NewShortcutExpander的构建其实也可以传NewMemCacheClient封装的discoveryClient。
- 在将缩写补全后,直接调用传入的RESTMapper对象对应的
KindFor
- 和之前的ResourcesFor大同小异
- 获取资源组缩写的映射关系
- 通过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对象最好也用缓存对象封装一下,不然会多请求一遍资源组对象。