这条在讲一个学 Go 三周的人给 k9s 修了一个真 bug 的完整过程——不是那种“我也能贡献开源”的鸡汤文,是那种会把 HTTP method 怎么映射成 RBAC verb 写清楚的技术复盘。出处是 Le Beltagy 8 月 1 日发在 dev.to 上的博客。
我读到第三段才确认值得推荐:作者自己在测试里踩了个数据竞争,被 go test -race 抓出来,然后他写了每个 case 单独 mock 的修法。这个细节在同类“小白贡献开源”故事里基本不会出现,把“学语言”这事拉回了真实的工程地面。
这篇没法跑,是 PR 复盘不是方法工具,只能读。但读完能带走的东西不少。
k9s 在检查用户能不能 port-forward 的时候,只查了 pods/portforward 上有没有 create 这个 verb。Kubernetes 1.31 改了端口转发的实现,新路径走 WebSocket,要求的是 get,不是 create。结果就是:一个只配了 get 权限的用户,用 kubectl 能正常 port-forward,打开 k9s 发现那个选项是灰的。这是 issue #4144。
作者第一次修复把 create 换成 get。被 maintainer 拦下来了:旧集群还在用 SPDY 路径,那个还是走 create。你直接替换,等于让老集群的用户反过来被挡住。这个点很关键,说明作者写第一版的时候根本没意识到端口转发有两种底层实现——但他几天前才刚开始学 Go,这么要求他有点过了。
最终合入的版本是把 create 和 get 各自独立检查,任一个过就算有权限。改了两个文件,加了 240 行删了 3 行,测试补了 6 个 case,覆盖无权限、仅 create、仅 get、两个都有。48 小时从 draft 到合并,两轮 review。
作者在复盘里写了一个他后来才搞懂的点:SPDY 端口转发开头是一个 HTTP POST,Kubernetes 授权把它映射成 create;WebSocket 端口转发开头是 GET Upgrade,映射成 get。所以 Kubernetes 1.31 把 port-forward 换成 WebSocket 之后,权限检查的 verb 就从 create 变成了 get。这个映射关系是整件事的核心。很多人修这种问题只看到表面报错,不去看协议动作,作者至少走到那一步了。
测试里那事也值得一提。作者一开始给多个 t.Parallel() 测试用例共享一个 mock RestClient,结果数据竞争。普通人不会想到在 port-forward 的 mock 里并行跑测试会有事,只有 race detector 会较真。他修的方式也很直接:每个 case 自己的 mock RestClient。这比嘴硬说“我的用例不会同时跑”实诚得多。
这是我喜欢这篇推荐的原因:作者没把数据竞争那段删掉装没事,而是留着。一个学了三周 Go 的人能写出会触发 race 的测试,还知道用 go test -race 抓住,最后给出和 reviewer 讨论过的修复,这就是修真 bug 的收获。教程项目不会逼你处理这种问题。
我原来对“别做教程项目,去修真 bug”这种说法挺抵触的,觉得是爽文口号。但这篇留了两个教程给不了的东西:真实项目的兼容性约束,和真实 reviewer 盯着你。第一次修错,不是因为不会 Go,是因为不知道 K8s 有两种 port-forward 语义;第二次修对,是因为被 reviewer 点出 old path 不能破坏。这种知识不是看文档能自动长出来的,是你提交一次错误修复后被挡住才记住的。
所以文章里那个建议是有分量的:读测试文件先于源文件。对一个没文档的 Go 项目,测试文件就是现成的使用示例。我夹子里存过好几个用 Go 写的小工具,读源码读不进去的时候,确实从测试开始读会快很多。这个建议和作者的经历是匹配的,不是从别处抄的。
不点链接也能带走的一句:给 Kubernetes 工具加 port-forward 权限检查,别只知道 create 这个词。K8s 1.31 切到 WebSocket 后,get 也能过;协议变了,verb 就变。把你代码里所有单 verb 的 SelfSubjectAccessReview 搜一搜。
最后提醒一句,如果还没在本地跑 go test -race 的,顺手加上。
go test -race ./...