源码剖析_K8S APIServer 源码剖析(二)| 探索 API

「技术直达」系列又回来啦!道客船长「技术直达」系列,关注国内外云原生领域的技术和前沿趋势,为开发者和企业提供最新的理论和实践干货。近期为大家带来 K8S API-Server 源码剖析,持续更新「理论+实践」的系列干货文章。

作者简介

f719f7884bef349062c62167f1a439ab.png

周尧

DaoCloud 后端工程师,热衷于研究云原生技术,CKA/CKAD 资格认证,Kubernetes 社区成员

上期文章剖析 K8S 监听机制 List-Watch 源码,针对 Controller 到 API-Server 端进行了一定程度的讲解。 List-watch 是 API-Server 的核心功能,本期将会介绍 API-Server 的基本实现,作为一个普通的 K8S 初学者,如何探索这些 API 。

1. API 基本概念

在开始探索之前,我们需要了解一些 API 的基本概念:Group,Version 和 Kind/Resources。相信看过 kubernetes 源码的同学都不少见过 GV,GVK 或者 GVR 这样命名的变量,其实就是说的 Group,Version 和 Kind / Resource。一个关于 Deployment 的路由如下图所示: af8cea90509bb57b6cdf13b0cacb9204.png Group: 是一组相关的 Kind 的集合。   Version:  每个 API Group 下面都能存在有多个 version 版本。 比如在一个 group 群组中最早有第一个 v1alpha1 版本,后来中间发展到了 v1beta1 版本,最终发展到 v1 的稳定版本。 如果在系统创建了一个 v1beta1 版本的对象,那么它能过被 Group 任一支持的版本( 比如v1 )检索到。 这是由于 API server 能够支持不同版本对象之间的无损耗转换,这一点会在后面的文章中讲到。 Kind / Resource: 代表以 JSON 格式通过 HTTP 发送或检索的资源实体。 它既可以使一个单独的resource资源(比如.../namespaces/default,也可以是一组resource 资源(比如.../jobs)。 这里说明一下 Reource 和 Kind 的区别: 其实基本上都是一个概念,只是  Kind 表示一个种类,在实际中它是首字母大写的; Resource 表示资源,在实际中它是全部小写的,并且有单数和复数之分。 拿 Deployment 举例:
  • Kind: Deployment
  • Resource: deployment(单数),deployments(复数)。
在源码中,你可以看到 Kind 转为 Resource 的代码,因为要考虑到英文的单复数问题,莫名喜感:
1switch string(singularName[len(singularName)-1]) {2case "s":3    return kind.GroupVersion().WithResource(singularName + "es"), singular4case "y":5    return kind.GroupVersion().WithResource(strings.TrimSuffix(singularName, "y") + "ies"), singular6}78return kind.GroupVersion().WithResource(singularName + "s"), singular
向左滑查看全部 Scope: 上面那个请求路由在 Version 和 Kind 之间还有一个 namespace,但是有些资源,比如 node,就不需要 namespace。 所以这里就涉及到一个非常重要的概念: scope,它决定了一个资源是是属于单一 namespace 还是整个集群的。 上面介绍的几个 API 的概念,其实在代码中有 一个他们的集成叫做 “ RestMapping ”,很多时候我们在确定一个资源时,就是在确定它的这个 “ RestMapping ”。 最后我们聊一聊这个路由的第一个字段 " /api ": 其实 kubernetes 有三种不同的 API 形式:     1. Core group API(在 /api/v1 路径下,由于某些历史原因而并没有在 /apis/core/v1 路径下)     2. Named groups API(在对应的 /apis/$NAME/$VERSION 路径下)     3. System-wide API(比如 /metrics,/healthz)。 图示如下: 1d3bdf19327d145b586a8c622db8075b.png

2. 自主探索 API

Kubernetes 使用的是 go-restful 那套机制写的 Restful 风格的 API 请求,关于 API 的路由,可以参考官方文档。 顺带一提,很多开始写各种 Kubernetes 的 Yaml 文件的时候,总会纠结于第一行 apiVersion 到底应该写什么,其实也可以在官方文档中找到参考,有些资源是在不同的 apiVersion 中都存在的。 比如在 kuberbetes1.15 中,关于 Deployment 你可以写 " apiVersion: extensions/v1beta1 "," apiVersion: apps/v1beta1 ", 或者" apiVersion: apps/v1 ",同样意味着你可以通过以下这些 API 路由访问到 Deployment: 1./apis/apps/v1beta1/namespaces/default/deployments 2./apis/apps/v1/namespaces/default/deployments 3./apis/extensions/v1beta1/namespaces/default/deployments

2.1. 使用 Curl

要想探索 API,我们可以用 Curl 命令来探索这个资源。 我们知道,Kubernetes 的各个组件都是向 API-Server 发送请求的,而 API-Server 的 HTTPS 端口默认就是 6443,然后我们可以通过创建 Service Account 的方式来获得 TOKEN。 Kubernetes 采用的 RBAC 的授权模式,所以这个 Service Account 还需要使用 Rolebinding(面向单一 namespace ) ( ClusterRolebinding 面向集群 ) 来绑定到某个 Role ( ClusterRole ) 上面,这个 Role 就是定义了对这个集群能进行的操作集合。 在上面的步骤完成后,我们需要找到跟这个 Service account 关联的 Secret,然后找到 Secret 中的 " token " 字段,然后对它进行 base64 解码,就可以访问 Role 里面规定的 API 资源了:
 1[root@master ~]# curl  https://10.6.192.3:6443/api --header "Authorization: Bearer $TOKEN" --cacert /tmp/ca.crt 2{ 3  "kind": "APIVersions", 4  "versions": [ 5    "v1" 6  ], 7  "serverAddressByClientCIDRs": [ 8    { 9      "clientCIDR": "0.0.0.0/0",10      "serverAddress": "10.6.192.3:6443"11    }12  ]

2.2. 使用 Kubectl

其实我们可以直接使用 Kubectl 来访问我们需要访问的路由,在 kubectl get 命令中加上 “ -raw ” 参数即可。
1[root@master ~]# kubectl get --raw /api2{"kind":"APIVersions","versions":["v1"],"serverAddressByClientCIDRs":[{"clientCIDR":"0.0.0.0/0","serverAddress":"10.6.192.3:6443"}]}
相比于刚才的 Curl 命令,这个 Kubectl 的命令省去了 IP 和端口,还有认证信息,你可能会有疑问,是否 Kubectl 使用的就是一个系统自带的 Service Account 呢,但其实它并不是 Service Account 而是一个 User Account,关于他们的区别这里不再赘述,简单来说,Service Account 是一种 API 资源,是集群内可控的;User Account 就是一个外部资源,集群并不能对其进行增删改查,但是同样的,User Account 也需要绑定一个 Role ( ClusterRole ) 来指明它的权限,这些信息都写在了 $HOME/.kube/config 里面,Kubectl 会读取里面的内容,省去各种加密的字段,我们可以看到这个 config 内容:
 1[root@demo-master-a-1 ~]# cat .kube/config 2apiVersion: v1 3clusters: 4- cluster: 5    certificate-authority-data:  6    server: https://apiserver.demo:6443 7  name: kubernetes 8contexts: 9- context:10    cluster: kubernetes11    user: kubernetes-admin12  name: kubernetes-admin@kubernetes13current-context: kubernetes-admin@kubernetes14kind: Config15preferences: {}16users:17- name: kubernetes-admin18  user:19    client-certificate-data: 20    client-key-data: 
所以从这个配置文件中,我们可以看到有 IP,端口,还有 CA 证书相关的信息,还有它所使用的 User Account 就是 " kubernetes-admin ",显而易见,这个 User Account 所绑定的角色权限一定是最高的,或者说是基本任何操作都是可以做的。 Kubectl 通过读取这个配置文件,就可以向 API-Server 发送请求了。 注意这里面有个字段是 “ Current-context ” 就是说目前正在使用的上下文,也就是说我们所谓的切换 Context,无非就是修改这个配置文件的这个字段罢了。

2.3. 使用 Kubect Proxy

kube ctl proxy 命令本质就可以使 API-Server 监听在本地的某个端口上,下面这个例子就是让 API-Server 监听 8080。
 1[root@demo-master-a-1 ~]# kubectl proxy --port=8080 & 2[1] 30460 3[root@demo-master-a-1 ~]# Starting to serve on 127.0.0.1:8080 4[root@demo-master-a-1 ~]# curl http://localhost:8080/api/ 5{ 6  "kind": "APIVersions", 7  "versions": [ 8    "v1" 9  ],10  "serverAddressByClientCIDRs": [11    {12      "clientCIDR": "0.0.0.0/0",13      "serverAddress": "10.6.192.7:6443"14    }15  ]16}

3.  Kubectl 探索 API

在我们掌握了自主探索 kubernetes 的技巧之后,我们大概会产生一些疑惑:     1. 一个资源 ( Deployment ) 的路由到底是怎样的? 它到底是属于哪个 Group,哪个 Version ? 多个 Version 的时候,应该返回哪一个 Version ?     2. 如果我 Post / Patch 一个 API 对象,这个这个对象的各个字段如何验证 ? 不仅我们会有这样的问题,kubectl 在发送请求的时候,也会考虑这些问题,为此,kubernetes 专门为这些问题做了一个 DiscoveryClient ( k8s.io/client-go/discovery/discovery_client.go ),目的就是解决上面的问题。 那么这个 DicoveryClient 是如何进行探索的呢,访问 " /api/v1 " 可以得到:
 1{ 2    "kind":"APIResourceList", 3    "groupVersion":"v1", 4    "resources":[ 5        { 6            "name":"pods", 7            "singularName":"", 8            "namespaced":true, 9            "kind":"Pod",10            "verbs":[11                "create",12                "delete",13                "deletecollection",14                "get",15                "list",16                "patch",17                "update",18                "watch"19            ],20            "shortNames":[21                "po"22            ],23            "categories":[24                "all"25            ],26            "storageVersionHash":"xPOwRZ+Yhw8="27        },28# 省略了其他的resources
由于内容较多,我们只看其中 Pod 资源的相关信息,可以看到,这里面列出来了 Pod 这种资源,所能进行的动作 ( Verb ),以及它的 scope ( namespaced:true )。同样的道理,其他的 API 资源都可以通过这个请求,拿到相应的规范;当然这个 " /api/v1 " 是只有一个 Version 的,我们访问一下 " /apis " 可以看到如下内容:
 1{ 2    "kind":"APIGroupList", 3    "apiVersion":"v1", 4    "groups":[ 5        { 6            "name":"autoscaling", 7            "versions":[ 8                { 9                    "groupVersion":"autoscaling/v1",10                    "version":"v1"11                },12                {13                    "groupVersion":"autoscaling/v2beta1",14                    "version":"v2beta1"15                },16                {17                    "groupVersion":"autoscaling/v2beta2",18                    "version":"v2beta2"19                }20            ],21            "preferredVersion":{22                "groupVersion":"autoscaling/v1",23                "version":"v1"24            }25        },2627# 省略了其他group
这样就可以看到在 “ /apis ” 下面的所有 Group 和它的所有 Versions,比如这里列出来的 “ autoscaling ” 这个 Group,它的 Versions 有三个:“ v1 ”,” v1beta1 “,” v1beta2 “;这三个版本在 API-Server 中都是共存的。注意下面还有一个 ” preferredVersion “,也就是我们在使用 “ kubectl get ” 命令去访问属于这个 Group 的资源 ( horizontalpodautoscalers ) 时,它所真实请求的路由会选择这个 “ preferredVersion ”:
1[root@demo-master-a-1 ~]# kubectl get horizontalpodautoscalers -v 72GET https://apiserver.demo:6443/apis/autoscaling/v1/namespaces/default/horizontalpodautoscalers?limit=500
同样的,如果我们接着访问 " /apis/autoscaling/v1/ ",我们就可以得到属于这个 GroupVersion 的各种资源的请求规范。 所以可以看出,通过访问 " /api " 和 “ /apis/ ”,确实可以满足我们之前所疑惑的问题,但是 kubectl 真的是在每次发送最后的请求之前,都要做这么多次请求来寻找资源的路由和规格吗? 下面就要介绍一下 Kubectl 所封装的一层叫做 “ CachedDiscoveryClient ” 来实现用本地缓存来直接获得 api 信息。

3.1. 本地缓存

关于 Kubectl 所做的本地缓存,也在 $HOME/.kube/cache 下。 本质都是一些 json 文件,内容如下:
1[root@demo-master-a-1 apiserver.demo_6443]# ls2admissionregistration.k8s.io  authentication.k8s.io  certificates.k8s.io    extensions         rbac.authorization.k8s.io  v13apiextensions.k8s.io          authorization.k8s.io   coordination.k8s.io    networking.k8s.io  scheduling.k8s.io4apiregistration.k8s.io        autoscaling            crd.projectcalico.org  node.k8s.io        servergroups.json5apps                          batch                  events.k8s.io          policy             storage.k8s.io
看上去文件很多,事实上这些文件都是按照 Group 来进行分层的,注意这里有一个 " servegroups.json ",它其实就是请求 " /apis " 的内容,显而易见,其他的以 Group 命令的文件夹里面的 " serverresources.json ",就是 “ /apis// ” 下面的内容。 这两种 json 文件作为本地缓存,就能让每次发出最后的请求之前,去寻找这个资源的 API 规范:     1. 找出该API对象的请求路由(Group,Version,Kind)     2. 验证是否能对API对象执行的操作 作为一个缓存,它必然会定期更新自己,不然这个缓存就没有意义,在代码中,我们可以看到 CachedDiscoveryClient 的更新时间的设置:
1// k8s.io/cli-runtime/pkg/genericclioptions/config_flags.go2discoveryCacheDir := computeDiscoverCacheDir(filepath.Join(homedir.HomeDir(), ".kube", "cache", "discovery"), config.Host)3    return diskcached.NewCachedDiscoveryClientForConfig(config, discoveryCacheDir, httpCacheDir, time.Duration(10*time.Minute))
所以这里就是 10 分钟为一个期限,其逻辑是:如果这个资源已经超时 10 分钟了,那么就发送请求去拿最新的数据,然后将其结果写入这个本地缓存中。 很明显,这个 CachedDiscoveryClient 的作用就是减少 API-Server 的压力,因为这些数据,一般都不会变化的,所以放在本地缓存中是可行的。

3.2. 验证 API 字段

通过上面对 “ /api ” 和 “ /apis ” 的请求,还有对本地缓存的读取,我们实现了 API 路由的确定和验证。但是在执行 “ kuectl apply ” 等操作的时候,我们会将一个API资源通过 yaml / json 的方式传给 API-Server,在这种情况下,kubectl 会先进行一次对这个 API 资源的字段的验证,这个验证又是怎么做到的呢? 其实要搞清楚很简单,我们试着创建一个错误的 Pod,将第一个字段 “ apiVersion ” 改为 “ ApiVersion ” (刚开始写 Pod 的 Yaml 文件时经常犯的错误):
 1[root@master pod-yaml]# cat testPod.yaml 2ApiVersion: v1 3kind: Pod 4 5metadata: 6  name: my-busybox 7  labels: 8    app: my-testapp 9    env: my-env1011spec:12  containers:13  - name: my-busybox14    image: busybox15    command: ["sh", "-c", "sleep 3600"]16[root@master pod-yaml]# kubectl apply -f testPod.yaml -v 717I1125 11:13:07.087738    3263 loader.go:359] Config loaded from file:  /root/.kube/config18I1125 11:13:07.090048    3263 round_trippers.go:416] GET https://10.6.192.3:6443/openapi/v2?timeout=32s19I1125 11:13:07.090068    3263 round_trippers.go:423] Request Headers:20I1125 11:13:07.090077    3263 round_trippers.go:426]     Accept: application/com.github.proto-openapi.spec.v2@v1.0+protobuf21I1125 11:13:07.090248    3263 round_trippers.go:426]     User-Agent: kubectl/v1.15.3 (linux/amd64) kubernetes/2d3c76f22I1125 11:13:07.114321    3263 round_trippers.go:441] Response Status: 200 OK in 24 milliseconds23F1125 11:13:07.203606    3263 helpers.go:114] error: error validating "testPod.yaml": error validating data: apiVersion not set; if you choose to ignore these errors, turn validation off with --validate=false
可以很明显得看到,其实验证字段只需要发送一次请求:/openapi/v2,这个请求很明显就是去查看 OpenAPI 的,顺带一提,在 API-Server 的配置中开启 --enable-swagger-ui=true 后还可以通过 /swagger-ui 访问 Swagger UI。 下期预告 在本次「探索API」的文章中,我们了解到 k8s API 的大致内容和规范。这些 API 都是通过 API-Server 来进行注册的,下期内容会分享 API-Server 整个启动流程。 往期回顾

K8S API-Server 源码剖析(一)| 监听机制 List-Watch 剖析

0617a41c5854154173429a3c58a6555f.png
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值