☰
KubeVirt API 序列化兼容性测试指南:用 JSON/YAML fixture 守护 `kubevirt.io/v1` 的向后兼容
2026/10/6 7:31:12 网站建设 项目流程
  • 云原生

【免费下载链接】kubevirt

Kubernetes Virtualization API and runtime in order to define and manage virtual machines.

项目地址:https://gitcode.com/gh_mirrors/ku/kubevirt
点击查看免费下载

KubeVirt 在staging/src/kubevirt.io/api/apitesting下维护了一套 API 序列化兼容性测试(API serialization compatibility tests),通过固化每个发布版本的序列化对象快照,确保 API 演进过程中旧客户端不会被破坏。本文以 apitesting/testdata/README.md 为主体,结合 roundtrip 包 的源码实现,完整讲解这套测试的目录结构、运行/重新生成流程、失败输出解读,以及 API 修改时的红线清单,帮助开发者和评审者在日常迭代中正确维护兼容性数据。

这套测试解决什么问题

KubeVirt 对外暴露的 API 属于长期契约:一个在v1.0.0时代编写并持久化的 VirtualMachine 对象,必须在v1.9.0的 API server 上仍然能被解码、被正确往返序列化(round-trip)。任何字段删除、类型变更、字段改名,都会在集群升级后让旧数据"读不出来"或"读出不同的东西"。

为此,staging/src/kubevirt.io/api/apitesting/testdata/目录树存放了大量以 JSON 和 YAML 格式固化的序列化 API 对象。测试运行时,会用当前代码对这些文件执行三类校验:

  1. 可解码:历史版本的序列化数据必须能被当前版本的 Go 结构体无错解码;
  2. 字节级往返一致:解码后再编码,必须与原始序列化字节完全一致(或与随附的after_roundtrip期望文件一致);
  3. 语义等价:同一对象从 JSON 和 YAML 两种格式解码后,必须得到语义相同的对象。

这套机制直接保护了 KubeVirt 三大核心资源类型的兼容性契约。

测试数据目录结构与命名约定

当前覆盖的是 group-version 为kubevirt.io/v1的三个 API 类型(更多类型可在未来加入):

  • VirtualMachineInstance(运行中的虚拟机实例)
  • VirtualMachine(虚拟机定义)
  • KubeVirt(KubeVirt 部署自身的 CRD)

目录布局如下:

apitesting/testdata/ ├── HEAD/ # 当前主干版本(main branch) │ ├── kubevirt.io.v1.KubeVirt.json │ ├── kubevirt.io.v1.KubeVirt.yaml │ ├── kubevirt.io.v1.VirtualMachine.json │ ├── kubevirt.io.v1.VirtualMachine.yaml │ ├── kubevirt.io.v1.VirtualMachineInstance.json │ └── kubevirt.io.v1.VirtualMachineInstance.yaml ├── release-1.8/ # 历史发布版本 │ ├── kubevirt.io.v1.VirtualMachine.json / .yaml │ ├── kubevirt.io.v1.VirtualMachine.after_roundtrip.json / .yaml │ ├── kubevirt.io.v1.VirtualMachineInstance.json / .yaml │ ├── kubevirt.io.v1.VirtualMachineInstance.after_roundtrip.json / .yaml │ └── kubevirt.io.v1.KubeVirt.json / .yaml └── release-1.9/ # 另一个历史发布版本(结构同上)

文件命名遵循统一格式:<group>.<version>.<kind>.[json|yaml]。例如kubevirt.io.v1.VirtualMachineInstance.json表示 group=kubevirt.io、version=v1、kind=VirtualMachineInstance的 JSON 快照。该命名规则由源码中的makeName(gvk)函数生成(见 compatibility.go):group 为空时使用core作为前缀,否则拼接为group.version.kind。

这些 fixture 并不是手工维护的。测试运行时,会通过反射机制确定性填充一个类型的全部字段(详见下文"fixture 是如何生成的"一节),把填充结果序列化后与磁盘文件逐字节比对——这正是这些文件看起来字段取值都是xxxValue、时间戳都是固定年份的原因,例如 HEAD 目录下的 VirtualMachineInstance.json 中name、generateName、namespace等字段均为xxxValue形式的确定性占位值。

DEVELOPERS GUIDE:开发者的日常工作流

每个 release 的数据固化流程

每当 KubeVirt 发布新版本时,需要把当前版本文件复制到对应的 release 目录,作为历史兼容基线。以v1.2.0为例(注意:示例使用release-1.2目录,实际仓库当前包含release-1.8、release-1.9等目录):

export VERSION=release-1.2 git checkout ${VERSION} cp -fr staging/src/kubevirt.io/api/apitesting/testdata/{HEAD,${VERSION}} git checkout -b add-${VERSION}-api-testdata master git add . git commit -m "Add ${VERSION} API testdata"

要点:HEAD目录始终存放由当前提交生成的序列化对象,历史版本目录则在每次发版时从HEAD快照而来。这样新旧版本之间的差异就沉淀为静态文件,供后续回归比对。

只跑当前版本(HEAD)的测试

go test kubevirt.io/api/apitesting -run //HEAD

其中//HEAD是 Go 子测试路径过滤语法,对应Run(t)中为每个 GVK 生成的HEAD子测试(见 compatibility.go)。该用例会验证:

  • 磁盘上的 JSON/YAML 与内存对象序列化结果逐字节相等;
  • 磁盘文件能被无错解码;
  • 解码结果与填充出的期望对象语义相等(使用apiequality.Semantic.DeepEqual比较)。

同一 group/version/kind 的所有格式都必须能解码为相同对象,且往返序列化后字节完全一致。

API 变更后重新生成 fixture

新增字段、废弃字段或新增 API 类型都会改变序列化结果,此时 fixture 文件需要更新。重新生成的方式是带上环境变量重跑测试:

UPDATE_COMPATIBILITY_FIXTURE_DATA=true go test kubevirt.io/api/apitesting -run //HEAD

实现上,runCurrentVersionTest在比对失败时会检查UPDATE_COMPATIBILITY_FIXTURE_DATA环境变量;若为true,则把当前期望的 JSON/YAML 写回磁盘并提示"verify, commit, and rerun tests"(见 compatibility.go)。因此正确的流程是:

  1. 修改 API 结构体(如staging/src/kubevirt.io/api/core/v1/types.go);
  2. 用UPDATE_COMPATIBILITY_FIXTURE_DATA=true重新生成 HEAD fixtures;
  3. 人工审查 diff,确认是预期变更;
  4. 提交 fixture,再不带环境变量跑一遍,确保测试通过。

在 Bazel 构建体系中,fixture 通过data = glob(["testdata/**"])随测试包一起打包,且测试开启race = "on"(见 apitesting/BUILD.bazel)。

跑某个历史版本的测试

go test kubevirt.io/api/apitesting -run //release-1.1

例如当前仓库中可以这样验证v1.9.0的快照:

go test kubevirt.io/api/apitesting -run //release-1.9

只测某个特定 group/version/kind

如果只想关注某个具体类型(示例中为apps/v1的Deployment),可以按子测试路径过滤:

go test kubevirt.io/api/apitesting -run /apps.v1.Deployment/

对应到 KubeVirt,例如:

go test kubevirt.io/api/apitesting -run /kubevirt.io.v1.VirtualMachineInstance/

子测试的层级结构是TestCompatibility/<group>.<version>.<kind>/<release 目录名>,排序是确定性的(按 group、version、kind 字典序),这保证了失败输出的可复现性(见 compatibility.go)。

失败输出解读:一次真实的兼容性回归

当历史数据在当前代码下解码/往返后与期望不一致,测试会打印详细的 diff。下面是文档给出的VirtualMachineInstance在release-0.50上的失败样例(节选):

--- FAIL: TestCompatibility/kubevirt.io.v1.VirtualMachineInstance (0.01s) --- FAIL: TestCompatibility/kubevirt.io.v1.VirtualMachineInstance/release-0.50 (0.01s) compatibility.go:416: json differs compatibility.go:417: ( """ ... // 215 identical lines "readonly": true }, - "floppy": { - "readonly": true, - "tray": "trayValue" - }, "cdrom": { "bus": "busValue", ... // 678 identical lines "tscFrequency": -12 }, - "virtualMachineRevisionName": "virtualMachineRevisionNameValue" + "virtualMachineRevisionName": "virtualMachineRevisionNameValue", + "runtimeUser": 0 } } """ ) compatibility.go:422: yaml differs ...

逐行解读(-表示旧快照有而新编码没有,+表示新编码新增):

  1. API 字段spec.domain.devices.disks.floppy被移除(对应 KubeVirt 早期的 floppy 磁盘支持移除议题与相关 PR);
  2. API 字段status.runtimeUser被新增(对应为 VMI 暴露运行用户信息的相关 PR)。

这两类变更都会让历史版本的序列化快照与当前代码的序列化结果产生差异——前者是字段消失,后者是字段新增。测试正是靠这些 diff 把"API 演进对旧数据的影响"显式暴露出来。

after_roundtrip 文件:新字段引入的专项处理

有一种特殊情况需要单独机制:给既有 API 类型新增非指针字段。这类字段即使未赋值也会序列化出零值,导致用当前代码往返历史数据时多出字段。文档中的示例:

--- FAIL: TestCompatibility/kubevirt.io.v1.VirtualMachine/release-1.0 (0.09s) compatibility.go:411: json differs compatibility.go:412: ( """ ... // 1113 identical lines "status": {} } - ] + ], + "dummyField": null }, "status": { ... // 111 identical lines """ ) compatibility.go:417: yaml differs ...

在dummyField加入之前,旧版本的序列化表示中根本没有该字段;加入后,往返结果中出现了dummyField: null。这种差异会导致字节级往返比对失败(输出中包含预期之外的字段)。

解决方式是:在历史版本目录中,紧挨着序列化数据文件放置一个after_roundtrip期望文件,即<group>.<version>.<kind>_after_roundtrip.[json|yaml](注意命名:源码中实际拼接格式为<group>.<version>.<kind>.after_roundtrip.json,如 release-1.9 目录 所示)。该文件记录了"用当前代码把历史数据往返一次后"的期望输出,把新增字段带来的差异显式固化下来。runPreviousVersionTest的实现逻辑是:若存在after_roundtrip文件则以它为准比对,否则回退到原始快照字节(见 compatibility.go)。

这些after_roundtrip文件同样可以用环境变量生成:

UPDATE_COMPATIBILITY_FIXTURE_DATA=true go test kubevirt.io/api/apitesting -run //release-1.8

当runPreviousVersionTest发现 JSON/YAML 往返结果与期望不符时,若环境变量为true就会写入对应的after_roundtrip文件(见 compatibility.go)。

源码视角:fixture 与测试框架是如何工作的

测试入口与 Scheme 注册

roundtrip_test.go 是唯一入口:把kubevirtv1.SchemeBuilder注册进runtime.Scheme,然后调用roundtrip.NewCompatibilityTestOptions(scheme).Complete(t).Run(t)。Complete()负责填充默认值(testdata目录、HEAD子目录、release-*目录列表、待测 kinds、JSON/YAML serializer),Run()负责遍历 GVK 并生成HEAD与各历史版本的子测试,最后还有一个unused_fixtures子测试,检查 HEAD 目录里是否有未被任何用例引用的多余 fixture 文件并强制清理(见 compatibility.go)。

fixture 的确定性填充

construct.go 中的CompatibilityTestObject通过反射递归填充对象的每个字段,保证每次调用结果完全一致:

  • 字符串字段填入<json字段名>Value(如nameValue);
  • 布尔字段置为true,以确保omitempty字段也会被序列化出来;
  • 整数字段填入从 protobuf tag 提取的编号(无 tag 时退化为字段名长度的负数);
  • 切片填成单元素切片,map填成xxxKey: xxxValue的单键条目;
  • 指针填充为底层类型的零值实例;
  • 特殊类型由defaultFillFuncs()定制,例如metav1.Time用2000+i年份生成固定时间戳(对应 fixture 里2008-01-01T01:01:01Z这类日期),RawExtension填入固定归一化 JSON,IntOrString填入字符串形式。

这套实现明确标注参考自 k8s 的apimachinery/pkg/api/apitesting,未来可能直接引入上游(见 construct.go 注释)。

被测 kinds 的选择

Complete()会枚举 Scheme 中全部已知类型并做筛选:跳过 internal version、跳过*List类型、跳过CreateOptions/UpdateOptions等核心类型(这些在 k8s 中已覆盖,见ignoreCoreKinds),并显式跳过VirtualMachineInstanceMigration、VirtualMachineInstancePreset、VirtualMachineInstanceReplicaSet三个类型(见 compatibility.go)。因此最终落盘的 fixture 就是 README 中列出的三类核心资源。

REVIEWERS GUIDE:评审者的兼容性红线

任何对 API Go 结构体的修改,都会同步改变对应的 JSON/YAML fixture(变更后需重新生成)。评审者应借助上述测试,判断"当前版本的改动是否会破坏升级后的旧客户端"。

修改 API 时绝对不允许的行为清单(这些正是测试所保护的向后兼容契约):

  1. 删除已有字段——旧数据里序列化出来的字段将无法被新结构体表达;
  2. 新增必填字段——旧数据缺少该字段将导致解码失败;
  3. 改变既有字段的类型——旧数据中的值无法正确反序列化;
  4. 重命名字段——序列化键名变化会让旧数据丢失该字段;
  5. 改变字段行为——例如把必填字段改成可选、或反之。

新增非指针字段属于"被允许但需要配套处理"的场景:它会让历史数据往返后多出零值字段,此时应通过after_roundtrip期望文件显式接纳这一变化(详见上一节)。评审时看到这类 diff,应确认"新增字段有合理的默认语义、且已生成 after_roundtrip 文件"。

Open Issues:测试覆盖的盲区

README 中记录了一个已知的开放问题:考虑"升级恰好卡在创建 CRD 之后、管理员被迫中止升级"的场景——这种情况是否是被支持的合法场景?它该如何被测试?这反映了当前测试主要覆盖"序列化/反序列化契约",而对升级流程中断等运维层面的场景尚无覆盖,属于未来可以完善的方向。

总结:日常维护 Checklist

场景命令
只验证当前版本序列化契约go test kubevirt.io/api/apitesting -run //HEAD
验证某个历史版本仍可兼容go test kubevirt.io/api/apitesting -run //release-1.9
只测某类资源go test kubevirt.io/api/apitesting -run /kubevirt.io.v1.VirtualMachine/
API 变更后更新 HEAD fixturesUPDATE_COMPATIBILITY_FIXTURE_DATA=true go test kubevirt.io/api/apitesting -run //HEAD
历史版本往返结果变化后更新期望UPDATE_COMPATIBILITY_FIXTURE_DATA=true go test kubevirt.io/api/apitesting -run //release-1.8
发版时固化历史快照cp -fr staging/src/kubevirt.io/api/apitesting/testdata/{HEAD,release-X.Y}并提交

一句话原则:凡是改变了staging/src/kubevirt.io/api/core/v1中 API 结构体的 PR,都必须同步更新staging/src/kubevirt.io/api/apitesting/testdata下的 fixture,并让兼容性测试保持绿色——这是 KubeVirt 保证 API 向后兼容、保护存量集群平滑升级的最后一道自动防线。

  • 云原生

【免费下载链接】kubevirt

Kubernetes Virtualization API and runtime in order to define and manage virtual machines.

项目地址:https://gitcode.com/gh_mirrors/ku/kubevirt
点击查看免费下载
上一篇:rPPG-Toolbox 实战指南:从零跑通无接触心率估计的完整方案
下一篇:docling 实战指南:3 条命令把扫描版 PDF 变成 AI 可用的数据

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询