你知道 apiserver 是如何映射请求 url 的吗?

发布时间 2023-07-13 16:55:40作者: 偶尔发呆

我们都知道 apiserver 是 kubernetes 里一个组件,可以简单地认为其是一个 web 应用,提供 http 接口(亦称 restful)服务,如同 CRUD 程序员所熟知的 tomcat,同样地 CRUD 程序员使用 MySQL 存储业务数据,而 apiserver 则使用 etcd 存储数据。

从使用角度看,web 服务器要做到:1. 监听端口,2. 映射 url 和处理方法,我们来看一看 apiserver 是如何做的?

调试启动 apiserver(别问我是怎么启动的,那是另外一个话题,欢迎关注留言)。

1 // cmd/kube-apiserver/apiserver.go:32
2 func main() {
3   command := app.NewAPIServerCommand()
4   code := cli.Run(command)
5   os.Exit(code)
6 }

在 secure_serving 文件的 Serve 方法中断点,结合注释,能清晰地看到启动了 http(s) server。

 

以 "kubectl get namespaces" 为例探究 url 的映射过程,该条命令对应的rest 请求是:

http get /api/v1/namespaces

接着看 url 是如何映射的,经过一番排查,发现 2 处关键调用点:

pkg/controlplane/instance.go:524 
InstallLegacyAPI()

vendor/k8s.io/apiserver/pkg/endpoints/installer.go:98
Install()

跟踪下来的调用栈如下:

 

url 和处理函数的映射关系保存在了 WebService 类中,类的定义如下:

 1 // WebService holds a collection of Route values that bind a Http Method + URL Path to a function.
 2 type WebService struct {
 3   rootPath       string
 4   pathExpr       *pathExpression // cached compilation of rootPath as RegExp
 5   routes         []Route
 6   produces       []string
 7   consumes       []string
 8   pathParameters []*Parameter
 9   filters        []FilterFunction
10   documentation  string
11   apiVersion     string
12 
13   typeNameHandleFunc TypeNameHandleFunction
14 
15   dynamicRoutes bool
16 
17   // protects 'routes' if dynamic routes are enabled
18   routesLock sync.RWMutex
19 }

这是 go-restful 包(https://github.com/emicklei/go-restful),apiserver 依赖它实现 url 和处理函数的映射,进一步查看 Route 的数据内容,包括了 url 和处理函数:

 命令行执行 kubectl get namespaces,apiserver 处理请求的调用栈如下,所以确定了 api/v1/namespaces 对应的处理函数(即pkg/registry/core/namespace/storage/storage.go 中 List 方法),继续深挖这一函数映射的时机点。

 继续 debug,发现具体的代码调用细节:

 

 1 func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storage, ws *restful.WebService) (*metav1.APIResource, *storageversion.ResourceInfo, error) {
 2   // 省略大段代码
 3   lister, isLister := storage.(rest.Lister)
 4   
 5   case "LIST": // List all resources of a kind.
 6     doc := "list objects of kind " + kind
 7     if isSubresource {
 8       doc = "list " + subresource + " of objects of kind " + kind
 9     }
10     handler := metrics.InstrumentRouteFunc(action.Verb, group, version, resource, subresource, requestScope, metrics.APIServerComponent, deprecated, removedRelease, 
11       restfulListResource(lister, watcher, reqScope, false, a.minRequestTimeout))
12     handler = utilwarning.AddWarningsHandler(handler, warnings)
13     route := ws.GET(action.Path).To(handler).
14       Doc(doc).
15       Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")).
16       Operation("list"+namespaced+kind+strings.Title(subresource)+operationSuffix).
17       Produces(append(storageMeta.ProducesMIMETypes(action.Verb), allMediaTypes...)...).
18       Returns(http.StatusOK, "OK", versionedList).
19       Writes(versionedList)
20     if err := AddObjectParams(ws, route, versionedListOptions); err != nil {
21       return nil, nil, err
22     }
23     switch {
24     case isLister && isWatcher:
25       doc := "list or watch objects of kind " + kind
26       if isSubresource {
27         doc = "list or watch " + subresource + " of objects of kind " + kind
28       }
29       route.Doc(doc)
30     case isWatcher:
31       doc := "watch objects of kind " + kind
32       if isSubresource {
33         doc = "watch " + subresource + "of objects of kind " + kind
34       }
35       route.Doc(doc)
36     }
37     addParams(route, action.Params)
38     routes = append(routes, route)
39 }

对于 namespace 而言,storage 是 pkg/registry/core/namespace/storage/storage.go 中的 REST 类,因为 REST 类实现了 rest.Lister 接口,所以可以强转。代码第 13 行中,action.path 和 handler 就是我们苦苦追寻的 url 和函数了。

 1 // pkg/registry/core/namespace/storage/storage.go:44
 2 // rest implements a RESTStorage for namespaces
 3 type REST struct {
 4   store  *genericregistry.Store
 5   status *genericregistry.Store
 6 }
 7 
 8 func (r *REST) NewList() runtime.Object {
 9   return r.store.NewList()
10 }
11 
12 func (r *REST) List(ctx context.Context, options *metainternalversion.ListOptions) (runtime.Object, error) {
13   return r.store.List(ctx, options)
14 }
15 
16 func (e *REST) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) {
17   return e.store.ConvertToTable(ctx, object, tableOptions)
18 }
19 
20 // staging/src/k8s.io/apiserver/pkg/registry/rest/rest.go:103 
21 // Lister is an object that can retrieve resources that match the provided field and label criteria.
22 type Lister interface {
23   // NewList returns an empty object that can be used with the List call.
24   // This object must be a pointer type for use with Codec.DecodeInto([]byte, runtime.Object)
25   NewList() runtime.Object
26   // List selects resources in the storage which match to the selector. 'options' can be nil.
27   List(ctx context.Context, options *metainternalversion.ListOptions) (runtime.Object, error)
28   // TableConvertor ensures all list implementers also implement table conversion
29   TableConvertor
30 }

接下来的代码就不跟了,最终是映射到了 namespace storage 的 List 方法中,前面已经验证过了。这一系列操作基本是 apiserver 大部分资源 crud 的套路。