Cilium Agent 控制面测试(Control-plane Tests)深度解析:以 K8s 对象为输入、Mock Datapath 为输出的集成测试框架
【免费下载链接】ciliumeBPF-based Networking, Security, and Observability项目地址: https://gitcode.com/GitHub_Trending/ci/cilium
Cilium 仓库中的 test/controlplane/README.md 定义了一套专门用于验证 Cilium Agent 控制面行为的集成测试体系:以 Kubernetes 资源作为输入,以 Mock 的 datapath 状态作为输出,在完整端到端(e2e)测试与纯单元测试之间架起一座桥梁。阅读本文后,你将掌握这套控制面测试框架的运行方式、golden 文件更新机制、新测试用例的编写范式,以及 Kubernetes 版本升级时的维护流程,并能在源码层面理解其底层实现原理。
一、什么是 Cilium 控制面测试
控制面测试(control-plane tests)是集成测试的一种,其核心目标是验证 Agent 在收到 Kubernetes 资源作为输入时,能否执行正确的 datapath 动作。它们比完整的端到端测试低一个层级:端到端测试需要真实集群与真实 datapath,而控制面测试刻意将用例表述为"K8s 对象进、Mock datapath 状态出"(k8s objects in, mock datapath state out),从而不对控制面实现方式做任何假设。
这种设计理念带来了两个核心价值:
- 回归测试(Regression testing):控制面实现中任何能在 K8s 环境里复现的 bug,都可以通过捕获描述集群状态的 K8s 资源,将其转化为一条测试用例,把"现场"固化进仓库。
- 重构安全(Refactoring):即使控制面实现发生大规模改动,测试用例本身也不会失效——只需要适配
suite/agent.go这一处(对应仓库文件 test/controlplane/suite/agent.go)即可,用例的表述与具体实现解耦,让重构有充分的测试保障。
在目录结构上,整个框架位于 test/controlplane,由入口文件、测试套件(suite 包)与具体测试用例三部分组成:
- 入口与套件基础设施:controlplane_test.go、suite 目录
- 代表性测试用例:node/nodehandler.go(手工构造 K8s 对象)、node/ciliumnodes/ciliumnodes.go(golden 测试)
- 版本与生成脚本:k8s_versions.txt、k8s_versions.sh、Makefile
二、运行控制面测试
控制面测试使用标准的 Go 测试命令运行:
# 运行全部控制面测试 $ go test ./test/controlplane2.1 Golden 测试更新:-update 标志
如果测试用例是 golden 测试(即以预生成的输出文件作为比对基准),可以使用-update标志重新生成 golden 输出文件。例如更新名为GracefulTermination的用例:
$ go test ./test/controlplane -test.v -test.run TestControlPlane/GracefulTermination -update这一命令会运行TestControlPlane下名为GracefulTermination的子测试,并将当前输出覆盖写入对应的 golden 文件。更新机制对应 suite/flags.go 中定义的标志:
var ( FlagUpdate = flag.Bool("update", false, "Update golden test files") FlagDebug = flag.Bool("debug", false, "Enable debug logging") )2.2 调试日志:-debug 标志
需要观察测试过程中的详细日志时,加上-debug:
$ go test ./test/controlplane -test.v -debug-debug会将日志级别提升到slog.LevelDebug(见 suite/flags.go 的ParseFlags()实现),便于定位 Agent 在测试过程中的内部行为。
提示:测试内建的
Eventually校验带有指数退避重试机制(详见下文),在 CI 环境下默认不设超时以降低 flaky 概率;本地开发时可用WithValidationTimeout设置自定义超时。
三、编写测试用例
3.1 单测试二进制的设计动机
由于控制面测试几乎拉入了整个 Agent(包括 datapath、k8s 客户端、hive 等全部组件),如果按 Go 惯例为每个包生成独立测试二进制,会导致链接时间漫长且产生大量体积庞大的二进制文件。因此该框架非惯用地将全部测试编译进单个测试二进制:
- 入口定义在 controlplane_test.go,其核心只有寥寥数行——通过
_空导入的方式引入各测试包,使其init()得以执行并注册用例,随后调用suite.RunSuite(t):
import ( _ "github.com/cilium/cilium/test/controlplane/node" _ "github.com/cilium/cilium/test/controlplane/node/ciliumnodes" "github.com/cilium/cilium/test/controlplane/suite" ) func TestControlPlane(t *testing.T) { suite.RunSuite(t) }- 测试用例通过各自的
init()调用suite.AddTestCase注册进测试套件。注册机制位于 suite/suite.go:
var allTestCases []testCase func AddTestCase(name string, fun func(t *testing.T)) { allTestCases = append(allTestCases, testCase{name, fun}) } func RunSuite(t *testing.T) { ParseFlags() // ... for _, tc := range allTestCases { t.Run(tc.name, tc.test) os.Chdir(cwd) // 每个用例结束后切回测试根目录 } }每个用例都以t.Run子测试的形式运行,最终呈现为TestControlPlane/<CaseName>的层级结构,这也解释了上文-test.run TestControlPlane/GracefulTermination的过滤写法。
3.2 ControlPlaneTest:测试的核心构造器
测试用例的主体逻辑是调用suite.NewControlPlaneTest构造出suite.ControlPlaneTest实例,随后即可按需启动 Agent、增删 K8s 对象并校验 Mock datapath 状态。其定义与主要方法位于 suite/testcase.go,结构化链式调用(每个方法返回*ControlPlaneTest)是典型写法:
| 方法 | 作用 |
|---|---|
NewControlPlaneTest(t, nodeName, k8sVersion) | 构造测试实例,初始化 4 组 Fake clientset(Kubernetes / Slim / Cilium / APIExtensions) |
SetupEnvironment() | 设置节点名、用 Fake client 进行 K8s 能力探测、创建临时测试目录 |
StartAgent(modConfig, extraCells...) | 以指定配置(含可选的配置修改函数与附加 cell)启动 Cilium Agent hive |
StopAgent() | 停止 Agent 并清理句柄 |
UpdateObjects(objs...) | 添加或更新 K8s 对象(已存在则 Patch,否则 Add) |
UpdateObjectsFromFile(filename) | 从 YAML 文件读取对象列表后批量更新 |
DeleteObjects(objs...) | 从各 tracker 中删除对象 |
EnsureWatchers(resources...) | 阻塞等待指定资源的 informer watcher 建立完毕 |
Eventually(check)/Execute(task) | 轮询校验条件 / 立即执行任务并断言无错 |
Get(gvr, ns, name) | 按 GVR 从 trackers 中查询对象 |
WithValidationTimeout(d) | 设置校验超时 |
AgentDB() | 暴露测试 Agent 的 statedb 句柄与 NodeAddress 表 |
3.3 手工构造 K8s 对象的用例:NodeHandler
node/nodehandler.go 是 README 推荐的手工构造对象范例。它先构造一个最小的corev1.Node(带 PodCIDR10.0.1.0/24、InternalIP 与 HostName 地址),再注册用例:
func init() { suite.AddTestCase("NodeHandler", func(t *testing.T) { k8sVersions := controlplane.K8sVersions() // 只需测试最新一个 k8s 版本 test := suite.NewControlPlaneTest(t, "minimal", k8sVersions[len(k8sVersions)-1]) test. UpdateObjects(minimalNode). SetupEnvironment(). StartAgent(func(*option.DaemonConfig) {}). Eventually(func() error { return validateNodes(test.FakeNodeHandler) }). StopAgent(). ClearEnvironment() }) }这里可以清楚看到完整生命周期:先注入 K8s 对象 → 搭建环境 → 启动 Agent → 轮询校验 Fake Node Handler 中的节点状态(名字、PodCIDR 是否正确)→ 停止 Agent → 清理临时目录。
3.4 基于生成输入与 golden 文件的用例:CiliumNodes
另一类用例(对应 README 中"generated k8s objects and golden test files"的范式)由 node/ciliumnodes/ciliumnodes.go 承载。它的输入文件不是手工构造,而是通过脚本在真实 kind 集群上抓取生成(见下文"版本更新"一节),golden 文件按版本存放在v1.24/、v1.25/、v1.26/目录下,每个目录包含init.yaml与state1.yaml~state4.yaml,对应不同的节点标签状态(增标签、删标签、改标签值),测试断言 CiliumNode 对象携带的标签集合与期望一致。golden 文件比对与更新分别由-update标志和make update-golden驱动。
四、底层原理:Mock Datapath 与 Agent 装配
4.1 Hive 容器化装配与 Fake Datapath
suite/agent.go 是 README 所说"重构时只需适配"的关键文件。它通过 Cilium 的 hive 依赖注入框架组装出一个完整但全部被 mock 的 Agent:
- 以
cmd.ControlPlane作为控制面主体 cell; - 用
fakeDatapath.Cell(来自pkg/datapath/fake)替换真实 eBPF datapath; - 提供 Fake CNI 配置管理器(
fakecni.FakeCNIConfigManager)、Fake GC Runner(ctmap.NewFakeGCRunner())、空的路由 reconciler、禁用状态的 kvstore(kvstore.Cell(kvstore.DisabledBackendName))等; - 通过
cell.Provide(...)注入测试用的k8sClient.Clientset。
测试的默认 Agent 配置在populateCiliumAgentOptions中设置,例如:
option.Config.IdentityAllocationMode = option.IdentityAllocationModeCRD option.Config.DryMode = true // 干跑模式,不真正下发 datapath option.Config.IPAM = ipamOption.IPAMKubernetes option.Config.EnableIPv6 = false option.Config.EnableL7Proxy = false option.Config.Debug = true其中DryMode = true正是"Mock datapath 状态"得以成立的前提——Agent 在干跑模式下计算出的 datapath 期望状态会被记录到 Fake 结构中,供测试断言。每个用例还可以通过StartAgent的modConfig func(*option.DaemonConfig)参数进一步改写配置。
4.2 三路 Decoder 与对象同步
控制面测试同时维护了三套对象表示:完整corev1、Cilium 精简版slim_corev1以及 Cilium CRD(cilium_v2)。suite/marshalling.go 为三者各建一个 decoder,以便把解码后的对象交给对应的 ObjectTracker。UpdateObjects的实现(见 suite/testcase.go)先将对象转为 unstructured 再 JSON 序列化,随后尝试用三个 decoder 依次解码并写入各自 tracker——这样测试只需写一份对象,而 core 与 slim 两套表示都能收到。
4.3 字段选择器过滤与 Watch 竞态防护
Fake clientset 的Watch与真实 API Server 不等价:它不尊重 ResourceVersion 的起始位置,导致 informer 在 List 与 Watch 之间可能漏掉事件。为此augmentTracker(同样位于 suite/testcase.go)为 4 个 Fake clientset 前置了自定义 reactor,实现了:
- List/Watch 的字段选择器过滤:
filterList与filteringWatcher按spec.nodeName、metadata.name等字段过滤结果;对于 fake client 无法天然匹配的字段(如 Pod 的spec.nodeName),代码中做了专门映射,且要求字段必须与真实 API Server 的可选择字段一致,避免"fake 通过、真实集群失败"的假阳性。 - Watcher 记录:每次建立 watch 都会登记资源名,配合
EnsureWatchers让测试等待 watcher 真正建立后再推进,显著降低竞态导致的 flake。
4.4 Golden 输出格式化工具
suite/table_writer.go 提供TableBuilder,用于把测试期间捕获的 datapath 状态(如 BPF 映射内容、端点表)格式化为对齐的 ASCII 表格,作为 golden 文件的稳定输出格式,保证-update再生成时输出确定性。
五、升级 Kubernetes 测试版本
控制面测试当前覆盖三个 K8s 版本,记录在 k8s_versions.txt(每行包含完整镜像 tag 与 sha256 摘要):
v1.24.7@sha256:577c630ce8e509131eab1aea12c022190978dd2f745aac5eb1fe65c0807eb315 v1.25.3@sha256:f52781bc0d7a19fb6c405c2af83abfeb311f130707a0e219175677e366cc45d1 v1.26.0@sha256:691e24bd2417609db7e589e1a479b902d2e209892a10ce375fab60a8407c73525.1 常规升级流程
升级被测试的 K8s 版本时,通常只需两步:
# 1. 更新 k8s_versions.txt 中的版本行 # 2. 重新生成所有自动生成的输入文件 make update-k8s-versions generate-input-files如果只是提升了某个既有版本的小补丁号(patch revision),通常还需要运行:
make update-golden来重新生成 golden 文件,避免因节点状态细节变化导致的比对失败。相关 make 目标定义在 test/controlplane/Makefile:
test: go test . update-golden: go test . -test.v -update pre-pull-kind-images: bash ./k8s_versions.sh --pre-pull-images update-k8s-versions: $(shell bash ./k8s_versions.sh --update-kind-config) generate-input-files: pre-pull-kind-images update-k8s-versions find . -name generate.sh -exec {} \;5.2 新增 K8s 版本
若要新增一个 K8s 版本,除了修改k8s_versions.txt,还需要:
移除最旧的 kind-config 文件,并手动为所有包含 kind-config 的目录新增新版本的 kind-config 文件。可用下面的命令列出全部 kind-config 文件:
find . -type f -regextype posix-extended -regex ".*/kind-config-.*.yaml"例如 node/ciliumnodes/manifests/kind-config-1.26.yaml 的内容是标准 kind 集群定义(control-plane + worker,禁用默认 CNI,并给 worker 打上
cilium.io/ci-node=k8s1节点标签):kind: Cluster apiVersion: kind.x-k8s.io/v1alpha4 nodes: - role: control-plane image: kindest/node:v1.26.0@sha256:691e24bd2417609db7e589e1a479b902d2e209892a10ce375fab60a8407c7352 - role: worker image: kindest/node:v1.26.0@sha256:691e24bd2417609db7e589e1a479b902d2e209892a10ce375fab60a8407c7352 kubeadmConfigPatches: - | kind: JoinConfiguration nodeRegistration: kubeletExtraArgs: node-labels: "cilium.io/ci-node=k8s1" networking: disableDefaultCNI: true同步更新 suite/testcase.go:如果新 K8s 版本引入了新的 API 资源,需要把对应的资源信息补充到该文件,否则 Fake clientset 无法正确处理新资源。
生成脚本 k8s_versions.sh 会依据
k8s_versions.txt批量改写所有 kind-config 中的镜像 tag,并在镜像变化时清理对应版本目录,随后由各目录下的generate.sh重新抓取输入文件。
5.3 输入文件是如何"长出来"的
以 node/ciliumnodes/generate.sh 为例,它展示了"从真实集群捕获 K8s 对象作为测试输入"的完整过程:
- 用 kind 按
kind-config-<version>.yaml创建临时集群; cilium install --wait安装 Cilium;- 用
kubectl get nodes,ciliumnodes -o yaml抓取初始状态写入init.yaml; - 依次对 worker 节点加标签(
test-label→another-test-label)、删标签、覆盖标签值,并在每步之间kubectl wait --for=condition=ready --all nodes,然后把节点状态分别 dump 为state1.yaml~state4.yaml; - 最后删除集群并清理 kubeconfig。
这些抓取出的 YAML 就构成了 golden 测试的"输入侧",而 Agent 的 Fake datapath 状态则构成"输出侧",输入输出共同完成对控制面行为的完整校验。
六、版本解析与测试版本选择
测试代码通过 k8s_versions.go 读取并解析版本文件:
//go:embed k8s_versions.txt var k8sVersionsData []byte func K8sVersions() (k8sVersions []string) { for w := range bytes.SplitSeq(k8sVersionsData, []byte{'\n'}) { if len(w) != 0 { version := regexp.MustCompile(`\d\.\d{2}`).Find(w) k8sVersions = append(k8sVersions, string(version)) } } return k8sVersions }k8s_versions.txt通过//go:embed内嵌进二进制,K8sVersions()用正则\d\.\d{2}提取出1.24、1.25、1.26这样的主次版本号。测试用例可以自行决定在哪些版本上运行——例如 NodeHandler 用例只取K8sVersions()的最后一个(最新)版本,而 CiliumNodes 这类用例则对每个版本分别执行。这些版本号同时用于 Fake Discovery 的FakedServerVersion与各 clientset 的Resources(API 资源清单),从而模拟出特定版本集群的能力探测结果。
七、总结:从"跑起来"到"写得好"
从实践角度看,这套控制面测试框架的价值在于它的可复用测试语言:UpdateObjects/StartAgent/Eventually/StopAgent构成了一条清晰的"注入-启动-断言-清理"流水线,配合EnsureWatchers消除 watch 竞态、retry指数退避机制降低 flake、-update一键刷新 golden,让控制面行为可以被精确、稳定、低成本地固化。无论你是在为某个 Cilium 控制面 bug 编写回归用例,还是准备对控制面实现做大规模重构,都可以从 test/controlplane 中寻找现成范式:手工构造对象参考 node/nodehandler.go,生成式 golden 测试参考 node/ciliumnodes,框架扩展则从 suite 目录入手。
【免费下载链接】ciliumeBPF-based Networking, Security, and Observability项目地址: https://gitcode.com/GitHub_Trending/ci/cilium
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考