深度解析:以 K8s 对象为输入、Mock Datapath 为输出的集成测试框架)
Cilium Agent 控制面测试Control-plane Tests深度解析以 K8s 对象为输入、Mock Datapath 为输出的集成测试框架【免费下载链接】ciliumeBPF-based Networking, Security, and Observability项目地址: https://gitcode.com/GitHub_Trending/ci/ciliumCilium 仓库中的 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.gogolden 测试版本与生成脚本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.govar 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 clientsetKubernetes / Slim / Cilium / APIExtensionsSetupEnvironment()设置节点名、用 Fake client 进行 K8s 能力探测、创建临时测试目录StartAgent(modConfig, extraCells...)以指定配置含可选的配置修改函数与附加 cell启动 Cilium Agent hiveStopAgent()停止 Agent 并清理句柄UpdateObjects(objs...)添加或更新 K8s 对象已存在则 Patch否则 AddUpdateObjectsFromFile(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 对象的用例NodeHandlernode/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 Datapathsuite/agent.go 是 README 所说重构时只需适配的关键文件。它通过 Cilium 的 hive 依赖注入框架组装出一个完整但全部被 mock 的 Agent以cmd.ControlPlane作为控制面主体 cell用fakeDatapath.Cell来自pkg/datapath/fake替换真实 eBPF datapath提供 Fake CNI 配置管理器fakecni.FakeCNIConfigManager、Fake GC Runnerctmap.NewFakeGCRunner()、空的路由 reconciler、禁用状态的 kvstorekvstore.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 CRDcilium_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.7sha256:577c630ce8e509131eab1aea12c022190978dd2f745aac5eb1fe65c0807eb315 v1.25.3sha256:f52781bc0d7a19fb6c405c2af83abfeb311f130707a0e219175677e366cc45d1 v1.26.0sha256:691e24bd2417609db7e589e1a479b902d2e209892a10ce375fab60a8407c73525.1 常规升级流程升级被测试的 K8s 版本时通常只需两步# 1. 更新 k8s_versions.txt 中的版本行 # 2. 重新生成所有自动生成的输入文件 make update-k8s-versions generate-input-files如果只是提升了某个既有版本的小补丁号patch revision通常还需要运行make update-golden来重新生成 golden 文件避免因节点状态细节变化导致的比对失败。相关 make 目标定义在 test/controlplane/Makefiletest: 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-nodek8s1节点标签kind: Cluster apiVersion: kind.x-k8s.io/v1alpha4 nodes: - role: control-plane image: kindest/node:v1.26.0sha256:691e24bd2417609db7e589e1a479b902d2e209892a10ce375fab60a8407c7352 - role: worker image: kindest/node:v1.26.0sha256:691e24bd2417609db7e589e1a479b902d2e209892a10ce375fab60a8407c7352 kubeadmConfigPatches: - | kind: JoinConfiguration nodeRegistration: kubeletExtraArgs: node-labels: cilium.io/ci-nodek8s1 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 --forconditionready --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 的ResourcesAPI 资源清单从而模拟出特定版本集群的能力探测结果。七、总结从跑起来到写得好从实践角度看这套控制面测试框架的价值在于它的可复用测试语言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),仅供参考