「技术直达」系列又回来啦!道客船长「技术直达」系列,关注国内外云原生领域的技术和前沿趋势,为开发者和企业提供最新的理论和实践干货。近期为大家带来 K8S API-Server 源码剖析,持续更新「理论+实践」的系列干货文章。
作者简介
周尧
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](https://img-blog.csdnimg.cn/img_convert/af8cea90509bb57b6cdf13b0cacb9204.png)
- Kind: Deployment
- Resource: deployment(单数),deployments(复数)。
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](https://img-blog.csdnimg.cn/img_convert/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/deployments2.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](https://img-blog.csdnimg.cn/img_convert/0617a41c5854154173429a3c58a6555f.png)