k8s Api server 源码 Route 对不上:Codex 用 TaoToken 通道行不行

k8s Api server 源码 Route 对不上:Codex 用 TaoToken 通道行不行 在 k8s v1.10 里为了搞清 Resource Quota 的创建流程沿cmd/kube-apiserver/app的 main 一路追到 go-restful 初始化最容易卡住的地方不是模型能不能读源码而是 Route 注册顺序与 webservice 挂载对不上APIInstaller.Install已经生成了wsregisterResourceHandlers也把 POST/namespaces/{namespace}/resourcequotas的 handler 拼进了routes可你回头在调试器里看ws的 route 列表却发现 POST 不在里面。这时用 Codex 带着现象去读源码行不行可以前提是先把 Codex 的请求通道配通打开 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 创建 API Key把~/.codex/config.toml里的base_url填成https://taotoken.net/api再把 go-restful 的 container/webservice/route 片段贴给 Codex。TaoToken 只负责供 Key 和统一入口不替 Codex 判断源码。下面按原文的追源码路径走一遍把 Route 对不上的问题拆开。1. kube-apiserver v1.10 追 mainRoute 对不上先看 go-restful Container1.1 背景Resource Quota 和 go-restful 的三层对象这次排障的起点很具体工作中要引入 k8s Resource Quota于是从 API Server 源码看 resource quota 的创建流程。源码版本是 v1.10Go 版本 1.10.5etcd 3.0IDE 用 IDEA 或 GoLand 都可以。1.10 及以上的 k8s 对 Go 版本有要求本地编译 kube-apiserver 常用命令是进入./go/src/k8s.io/kubernetes后执行make WHATcmd/kube-apiserver all先启动 etcd再本地启动 api-server 进 debug。如果遇到编译期 nil 不兼容把 nil 改成对应类型的 0 值继续调。k8s 的 RESTful 架构靠 go-restful 这类第三方模块落地。它有三个必须分清的对象Route 对应一个请求和一个处理函数Webservice 由多个 Route 组成Container 由多个 Webservice 组成。原文给的官方 demo 里ws.Route(ws.GET(/hello).To(hello))是往ws里塞 Routerestful.Add(ws)或container2.Add(ws2)才是把 Webservice 挂到 Container。Route 对不上时第一步不是怀疑模型而是确认你看的是 Route 的构造、Webservice 的挂载还是 Container 的最终路由表。1.2 从 main 到 NewAPIServerHandler 的调用链kube-apiserver 的入口在cmd/kube-apiserver/app的 main。它先app.NewAPIServerCommand()创建 cobra command然后把command.Execute()作为总入口。ExecuteC里大部分是参数标准化、help/version 处理真正进入Run后链路如下cmd/kube-apiserver/app main - app.NewAPIServerCommand() - command.Execute() - Run() - CreateServerChain() - CreateKubeAPIServer() - completedConfig.New() - c.GenericConfig.New() - NewAPIServerHandler()CreateServerChain里会先创建 node dialer、CreateKubeAPIServerConfig再创建apiExtensionsServer然后才进入CreateKubeAPIServer。到CreateKubeAPIServer时kubeAPIServerConfig.Complete(versionedInformers).New(delegateAPIServer)会创建 master 对应的 server 结构并在completedConfig.New里调用c.GenericConfig.New(kube-apiserver, delegationTarget)。真正创建 go-restful Container 的地方在NewAPIServerHandler。这里gorestfulContainer : restful.NewContainer()设置ServeMux、CurlyRouter、RecoverHandler、ServiceErrorHandler然后返回APIServerHandler里面带FullHandlerChain、GoRestfulContainer、NonGoRestfulMux、Director。如果你在源码里搜restful.NewContainer()只看到这里就说明 kube-apiserver 并没有用 go-restful 的默认全局 container而是自己维护了一个 container。1.3 为什么这条路最容易把“未注册”看成“通道不行”Director决定一个 HTTP 请求是进goRestfulContainer还是nonGoRestfulMux。如果资源路由确实没进 container 的 route 表请求就会落到 NonGoRestfulMux 的 404或者被 discovery 的 webservice 抢走匹配。很多人在调试时看到 POST/api/v1/namespaces/default/resourcequotas返回 404第一反应是“Codex 走通道读源码是不是读错了”。其实通道只负责把请求转发给模型源码里的ws.Route(route)有没有执行、container.Add(ws)有没有加对 containerCodex 不会替你改变。所以用 Codex 排查前先把问题描述成源码事实kube-apiserver v1.10resource quota 的 POST route 在ws中看不到或者container注册的 webservice 数量不对。然后贴NewAPIServerHandler、InstallLegacyAPIGroup、InstallREST三段片段。这样 Codex 才能沿着container - webservice - route的层次帮你对齐而不是凭空猜一个“通道不兼容”。2. InstallLegacyAPIGroup 到 APIInstaller.InstallWebService 和 discovery 谁先挂2.1 installAPIResources 遍历 GroupVersioncompletedConfig.New里会判断apiv1.SchemeGroupVersion是否启用然后构造LegacyRESTStorageProvider调用m.InstallLegacyAPI。InstallLegacyAPI里先通过legacyRESTStorageProvider.NewLegacyRESTStorage(restOptionsGetter)得到legacyRESTStorage和apiGroupInfo。apiGroupInfo里最重要的字段是VersionedResourcesStorageMap它是一个两层 map第一层 key 是版本号core 组当前是v1第二层 key 是资源名value 是该资源对应的 storage。resource quota 会在这里以resourcequotas为 key 对应到resourceQuotaStorage。接着调用m.GenericAPIServer.InstallLegacyAPIGroup。这个方法先检查apiPrefix是否在允许的 legacy 前缀里再调用s.installAPIResources(apiPrefix, apiGroupInfo)。installAPIResources会遍历apiGroupInfo.GroupMeta.GroupVersions如果某个版本没有任何 storage就跳过并打 warning。对启用的版本它会用s.getAPIGroupVersion把apiGroupInfo重新封装成apiGroupVersion然后执行apiGroupVersion.InstallREST(s.Handler.GoRestfulContainer)。这里 Route 对不上的第一层原因就出现了InstallREST接收的是s.Handler.GoRestfulContainer也就是前面NewAPIServerHandler创建的那个 container。如果你打断点时看的是restful.DefaultContainer那当然对不上。2.2 InstallREST 里 ws 的创建、discovery 挂载和 container.AddAPIGroupVersion.InstallREST的关键动作是拼 prefixprefix : path.Join(g.Root, g.GroupVersion.Group, g.GroupVersion.Version)。core 组 group 为空version 是 v1所以最终前缀通常是/api/v1。然后构造APIInstaller{ group:g, prefix:prefix, ... }调用installer.Install()。Install()返回apiResources, ws, registrationErrors。此时ws已经由APIInstaller.newWebService()创建并且ws.Path(prefix)会把/api/v1挂到 webservice 上。然后InstallREST做两件容易看错顺序的事先versionDiscoveryHandler.AddToWebService(ws)把版本发现用的 route 加到同一个ws再container.Add(ws)把这个 webservice 挂进 container。注意这里并不是“discovery 先覆盖了资源路由”而是同一个 webservice 里既有 discovery route也有资源 route。资源 route 是在更早的installer.Install()内部由registerResourceHandlers加进去的。如果你在container.Add(ws)处才看ws应该已经能看到资源 route 和 discovery route。如果看不到 POST/namespaces/{namespace}/resourcequotas问题不在container.Add而在registerResourceHandlers的某一步没有把 route 追加进ws。2.3 用 Codex 对照 container/webservice 片段时的验收点把下面这类片段贴给 Codex 时让它只回答“调用顺序”和“对象归属”不要让它扩写成 k8s 整体架构apiResources, ws, registrationErrors : installer.Install() versionDiscoveryHandler.AddToWebService(ws) container.Add(ws)你要 Codex 检查的验收点有四个。第一ws是不是APIInstaller.newWebService()创建的那个而不是另外 new 出来的。第二ws.Path(prefix)是否已经设置prefix 是否等于你请求路径的前缀/api/v1。第三registerResourceHandlers里ws.Route(route)的循环是否在Install()返回前执行。第四container.Add(ws)加的是不是s.Handler.GoRestfulContainer。这四点对齐后再看 POST route 缺失才有意义。3. registerResourceHandlers 的 POST 分支resourcequotas 的 Route 在哪一步进 ws.Route3.1 storage 接口断言决定 actions 里有没有 POSTAPIInstaller.Install里会先ws : a.newWebService()然后取出a.group.Storage的所有 key排序后逐个调用a.registerResourceHandlers(path, a.group.Storage[path], ws)。resource quota 的 path 就是resourcequotasstorage 就是前面NewLegacyRESTStorage生成的resourceQuotaStorage。进入registerResourceHandlers后第一组关键代码是接口断言creater, isCreater : storage.(rest.Creater)、namedCreater, isNamedCreater : storage.(rest.NamedCreater)、lister, isLister : storage.(rest.Lister)、getter, isGetter : storage.(rest.Getter)等等。resource quota 创建时不能带 name所以它走的是restfulCreateResource对应rest.Creater。如果isCreater是 false后面的 actions 数组就不会加入 POST自然也不会有ws.POST(action.Path)。这是 Route 对不上时最该先看的断言。3.2 namespace scope 下的 path 模板k8s 资源分 root scope 和 namespace scope。resource quota 是有 namespace 区分的资源所以 switch 会进入meta.RESTScopeNameNamespace。这里会构造namespaceParam : ws.PathParameter(scope.ArgumentName(), scope.ParamDescription()).DataType(string) namespacedPath : scope.ParamName() /{ scope.ArgumentName() }/ resource resourcePath : namespacedPath itemPath : namespacedPath /{name}其中scope.ParamName()通常是namespacesscope.ArgumentName()是namespace。所以 collection 路径会变成namespaces/{namespace}/resourcequotasitem 路径会变成namespaces/{namespace}/resourcequotas/{name}。再配合ws.Path(prefix)的/api/v1最终请求路径就是/api/v1/namespaces/{namespace}/resourcequotas。如果 Codex 只说“POST 路由应该在 resourcequotas”但没有把 prefix 和 namespace 模板拼起来就很容易误判。你贴片段时要包含namespacedPath、resourcePath、itemPath这三行以及ws.Path(prefix)的设置点。3.3 ws.POST 只是构造ws.Route(route) 才算注册actions 数组会通过appendIf加入各种操作。POST 这一项大致是actions appendIf(actions, action{POST, resourcePath, resourceParams, namer, false}, isCreater)随后代码遍历 actions按 verb 分支构造 route。POST 分支里handler restfulCreateResource(creater, reqScope, a.group.Typer, admit) handler metrics.InstrumentRouteFunc(action.Verb, resource, subresource, requestScope, handler) route : ws.POST(action.Path).To(handler). Doc(doc). Operation(create namespaced kind operationSuffix). Returns(http.StatusCreated, Created, producedObject) routes append(routes, route)注意ws.POST(action.Path).To(handler)返回的是一个*restful.RouteBuilder或 route 对象此时它只是被追加到局部变量routes数组里还没有进ws的 route 表。所有 action 处理完后代码才会统一循环for _, route : range routes { route.Metadata(ROUTE_META_GVK, metav1.GroupVersionKind{...}) route.Metadata(ROUTE_META_ACTION, strings.ToLower(action.Verb)) ws.Route(route) }所以你在 POST case 里单步时看wsPOST route 不在是正常的要走到最后ws.Route(route)循环结束才是真正注册。这个时机差是 k8s 源码里最经典的“Route 对不上”错觉。3.4 CurlyRouter 与 prefix 拼接的核对方法NewAPIServerHandler里设置的是gorestfulContainer.Router(restful.CurlyRouter{})。CurlyRouter 对/namespaces/{namespace}/resourcequotas这类模板路径有匹配规则。你要核对的是APIInstaller.newWebService()里ws.Path(prefix)设置的 prefix 是否和你实际请求的/api/v1一致registerResourceHandlers生成的action.Path是否是不带 prefix 的相对路径两者拼起来是否正好是请求路径。一个实用的本地调试动作在registerResourceHandlers的 POST 分支打断点看action.Path在for _, route : range routes的ws.Route(route)处打断点看ws里 route 数量在InstallREST的container.Add(ws)处看 container 的RegisteredWebServices()。这三个点连起来Route 到底在哪一层丢失就清楚了。Codex 能做的是根据你贴的这三段代码解释变量含义不能替你打断点。4. Codex 接入 TaoToken 通道~/.codex/config.toml 怎么填才能读源码4.1 创建 Key 和模型 ID 的取法前面说的排障请求要发给 Codex得先让 Codex 能正常调用模型。打开 TaoToken 注册账号在控制台创建 API Key复制后先别写进代码用占位符YOUR_API_KEY代替。模型 ID 不要凭记忆写gpt-5或随便加日期后缀去模型广场看当时上架列表选一个适合代码阅读和长上下文分析的模型 ID再填到 Codex 配置里。接口 Base URL 是https://taotoken.net/api末尾不要加/v1。官网落地页https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end只用于注册、创建 Key、看模型广场、看用量不要把它填进 Codex 的base_url。这两个地址各管各的混用会导致 404 或 HTML 解析错误。4.2 config.toml 与环境变量Codex 的配置文件在~/.codex/config.toml。一个可复制的写法如下model YOUR_MODEL_ID model_provider taotoken [model_providers.taotoken] name TaoToken base_url https://taotoken.net/api env_key TAOTOKEN_API_KEY wire_api chatmodel写你在模型广场选定的模型 ID。model_provider指向下面的model_providers.taotoken。base_url就是https://taotoken.net/api不要加 UTM也不要加/v1。env_key指定 Codex 从哪个环境变量读 Key。然后在 shell 里导出export TAOTOKEN_API_KEYYOUR_API_KEYWindows PowerShell 可以写$env:TAOTOKEN_API_KEYYOUR_API_KEY保存后启动codex先发一条简单消息让它回答“你是否能读到当前会话”。这一步只验证通道和模型 ID不验证源码结论。通道通了以后再把 k8s 源码片段贴进去。4.3 给 Codex 的提问模板和边界贴源码时不要只丢一句“Route 对不上怎么办”。更有效的模板是现象kube-apiserver v1.10resourcequota 的 POST /api/v1/namespaces/{namespace}/resourcequotas 在调试时看不到 route或请求返回 404。 我贴三段 1. APIInstaller.Install 里创建 ws、遍历 storage、调用 registerResourceHandlers 的代码 2. registerResourceHandlers 中 namespace scope 分支和 POST action 的代码 3. InstallREST 里 versionDiscoveryHandler.AddToWebService(ws) 和 container.Add(ws) 的顺序。 请只根据这些代码说明POST route 从 ws.POST(...) 到 ws.Route(route) 的追加时机以及 container.Add(ws) 与 discovery 挂载的先后关系。 不要猜版本外实现不要给修复补丁只列核对点。同时把边界说清楚Codex 只生成解释、对照代码和 SQL 类文本时也一样真正编译 kube-apiserver、启动 etcd、打断点查看变量都由你在本地执行。TaoToken 通道只负责把这次请求转发出去不会替 Codex 判断源码也不应该把生产库或生产机器交给模型直接操作。5. 从 registerResourceHandlers 到 etcdresourcequota 的 storage 何时初始化5.1 NewLegacyRESTStorage 到 resourcequotastore.NewRESTRoute 对上了以后问题 2 和问题 3 会接着出现handler 怎么创建 resource quota对象怎么落到 etcd回到NewLegacyRESTStorage找到 resource quota 的部分它会调用resourcequotastore.NewREST(restOptionsGetter)返回resourceQuotaStorage和resourceQuotaStatusStorage。NewREST里创建的是genericregistry.Store关键字段包括NewFunc、NewListFunc、DefaultQualifiedResource、CreateStrategy、UpdateStrategy、DeleteStrategy、ReturnDeletedObject。其中NewFunc返回空的api.ResourceQuota{}NewListFunc返回api.ResourceQuotaList{}。然后构造generic.StoreOptions{RESTOptions: optsGetter}调用store.CompleteWithOptions(options)。这一步完成后Store才拥有真正的e.Storage。5.2 CompleteWithOptions 里 e.Storage 的真实来源CompleteWithOptions会做很多检查DefaultQualifiedResource是否为空NewFunc、NewListFunc是否设置KeyRootFunc和KeyFunc是否成对CreateStrategy或UpdateStrategy是否设置。对 namespace scope 的 resource quota它会设置KeyRootFunc为NamespaceKeyRootFunc设置KeyFunc为NamespaceKeyFunc这样资源在 etcd 里的 key 会带 namespace。最关键的一段是if e.Storage nil { e.Storage, e.DestroyFunc opts.Decorator( opts.StorageConfig, e.NewFunc(), prefix, keyFunc, e.NewListFunc, attrFunc, triggerFunc, ) }opts来自SimpleRestOptionsFactory.GetRESTOptions。如果启用了 watch cacheret.Decorator会被设为genericregistry.StorageWithCacher(cacheSize)否则是generic.UndecoratedStorage。k8s 默认通常启用缓存所以e.Storage实际是一个 Cacher 包装层。5.3 Cacher.Create 到 etcd问题 2 和问题 3 的落点StorageWithCacher里先s, d : generic.NewRawStorage(storageConfig)创建原始存储后端再用storage.NewCacherFromConfig包装cacherConfig : storage.CacherConfig{ Storage: s, Versioner: etcdstorage.APIObjectVersioner{}, Type: objectType, ResourcePrefix: resourcePrefix, KeyFunc: keyFunc, NewListFunc: newListFunc, GetAttrsFunc: getAttrsFunc, TriggerPublisherFunc: triggerFunc, Codec: storageConfig.Codec, } cacher : storage.NewCacherFromConfig(cacherConfig)Cacher实现了storage.Interface它的Create方法最终会调用c.storage.Create(ctx, key, obj, out, ttl)其中c.storage就是前面创建出来的原始存储。再往上追POST 请求进入restfulCreateResource后会走到handlers.CreateResource再到createHandler。createHandler里先r.New()创建空对象用于反序列化然后解码请求 body做 admission最后r.Create(ctx, name, obj, ...)。这里的r就是resourceQuotaStorage对应的StoreStore.Create里再调用e.Storage.Create。所以问题 2 的 handler 创建在registerResourceHandlers问题 3 的 etcd 落盘在Cacher.Create到 raw storage 这一段。要让 Codex 帮你核对这条链贴Store.New、Store.Create、CompleteWithOptions的e.Storage初始化、Cacher.Create四个点即可。至于 etcd 里实际 key 长什么样用你本地的etcdctl查把输出贴回对话不要让模型直接连接生产 etcd。6. 验证、排障与下一步让 Codex 的结论回到本地断点6.1 验证 Codex 通道和 Route 注册配置完~/.codex/config.toml后先启动codex发一条普通消息确认没有 401也没有模型不存在的报错。然后打开 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_contentk8s_route_verify 看一眼这次调用有没有正常记上。通道没问题后回到 k8s 源码里打断点在APIInstaller.Install里观察paths排序后resourcequotas的位置在registerResourceHandlers的 POST 分支看isCreater是否 true在最后的ws.Route(route)循环看 route 数量在InstallREST的container.Add(ws)看 webservice 数量。如果你在 POST case 里看到ws没有 route不要慌那只是构造阶段。如果你在ws.Route(route)循环后仍然没有 POST才需要检查actions是否包含 POST、action.Path是否被覆盖、ws.Path(prefix)是否设置正确。验证的顺序应该是先本地断点再让 Codex 解释变量关系而不是直接把报错丢给模型让它猜。6.2 Route 对不上的排查顺序第一确认你看的 container 是s.Handler.GoRestfulContainer不是 go-restful 默认 container。第二确认installer.Install()返回的ws和container.Add(ws)是同一个 webservice。第三确认registerResourceHandlers里isCreater为 truePOST 被appendIf加入 actions。第四确认ws.POST(action.Path)构造出的 route 被 append 到routes并且最后执行了ws.Route(route)。第五确认CurlyRouter匹配的最终路径是/api/v1/namespaces/{namespace}/resourcequotas而不是少了/api/v1或多了/v1。Codex 配置侧也要排一次报 401 就检查TAOTOKEN_API_KEY是否导出成功env_key是否写错报模型不存在就回模型广场核对模型 ID如果请求路径异常再检查base_url是否误写成官网落地页或者给https://taotoken.net/api后面加了/v1。这些错误和源码里的 Route 对不上是两回事不要混在一起查。6.3 下一步把这次调用记到控制台配通之后可以先在 TaoToken 模型对话 里用同一把 Key 发一条测试消息确认模型 ID 和 Base URL 没填错。若你后面要长期用 Codex 读 k8s、Kubernetes controller、etcd storage 这类大段源码可以打开 Coding Plan 看套餐是否够用。Key 在 控制台 API Keys 创建官网入口仍然是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 。把这次APIInstaller.Install、registerResourceHandlers、container.Add(ws)三个断点的结果整理成一段描述再让 Codex 对照下一轮就不用从“Route 对不上”重新猜起了。