Terraform 插件协议中 DynamicValue 的 MessagePack 与 JSON 序列化规则是什么
【免费下载链接】terraformTerraform enables you to safely and predictably create, change, and improve infrastructure. It is a source-available tool that codifies APIs into declarative configuration files that can be shared amongst team members, treated as code, edited, reviewed, and versioned.项目地址: https://gitcode.com/GitHub_Trending/te/terraform
如果你在直接针对 Terraform 插件协议开发 SDK 或插件实现(而不是调用已经封装好协议的 SDK),就会反复遇到DynamicValue这个消息:Terraform Core 在请求里用它传入resource、data、provider块求值后的数据,插件在响应里也要用它返回新状态和计划值。它的结构由提供方 schema 在运行时决定,所以协议把它编码成一串不透明的字节,并允许 MessagePack 和 JSON 两种序列化格式。
这篇文章基于仓库内 docs/plugin-protocol/object-wire-format.md 与 tfplugin5.proto、tfplugin6.proto 的定义,说明这两种格式各自的选择时机、逐类型的映射规则和实现时必须满足的硬约束,供你在解码请求、编码响应时逐条对照。
适用前提
- 这套线协议自 Terraform v0.12.0 起构建在 gRPC 之上,
DynamicValue从协议 major version 5 开始存在,tfplugin5.proto(当前为协议 5.10)和 tfplugin6.proto(当前为协议 6.11)是两个 major 版本的权威定义。 - docs/plugin-protocol/README.md 明确指出:只有随 Terraform 发布 tag 发布的
.proto文件才是正式协议版本;main等开发分支上的定义可能尚未定稿。插件开发者应从最近的发布 tag 取 proto 文件。 - 同一 README 也说明:大多数 provider 不直接写在这个协议上,而是优先使用实现了该协议的 SDK;这份文档面向的是Terraform SDK 的开发者,而不是普通插件实现者。
DynamicValue 消息本体:两个编码字段
两个 major 版本的定义完全一致(以 tfplugin5.proto 为例):
// DynamicValue is an opaque encoding of terraform data, with the field name // indicating the encoding scheme used. message DynamicValue { bytes msgpack = 1; bytes json = 2; }注释点明了字段语义:字段名指示所采用的编码方案。由于值的结构在运行时才由 schema 决定,wire 上只能给一个字节容器,由具体字段承载序列化结果。
解码时如何选编码
Terraform 最常用 MessagePack,因为它提供更紧凑的二进制表示。但文档给出的规则是:当msgpack字段没有填充时,服务端实现必须回退到 JSON。也就是说,解码侧要同时支持两种格式,不能只实现 MessagePack。
编码时如何选编码
服务端必须能在各种响应消息中生产DynamicValue。文档对此的要求很明确:编码时始终使用 MessagePack,因为 Terraform 并没有在所有请求类型和所有版本上都一致地支持 JSON 响应。只写 JSON 响应的实现会在部分请求类型上失效。
序列化由 Schema 消息驱动
两种序列化都由 provider 之前返回的Schema消息驱动:Terraform 按 schema 中每个值的类型约束(type constraint)编码,选用与 Terraform 语言类型最接近的 MessagePack 或 JSON 类型。Schema.Attribute的type字段是一个 Terraform 类型约束的紧凑 JSON 序列化——要么是单个字符串(原始类型),要么是两个元素的数组(类型种类加类型参数)。
由此可以推出一条实现上最实用的结论:服务端可以用标准 MessagePack 或 JSON 库直接解码,并假定结果符合下文描述的序列化规则。
MessagePack 序列化规则
类型约定
文档引用的是 MessagePack 类型系统规范。MessagePack 对每个类型定义了多种可能的序列化格式,Terraform 可以选择其中任意一种,具体选择甚至可能在不同 Terraform 版本间变化,但类型本身是契约性的。生产侧同理:实现可以自由选择该类型下任何合法格式,文档建议选用能无精度损失地表示该值的最紧凑格式。
Block 与 Attribute 的映射
一个 block 的内容被编码为 MessagePack map,每个属性一个键值对,每种嵌套块一个键值对。属性的编码取决于type字段,映射规则如下(表格内容按 object-wire-format.md 原文整理):
type模式 | MessagePack 表示 |
|---|---|
"string" | MessagePack string,内容为字符串值的 Unicode 字符、以规范化 UTF-8 序列化 |
"number" | MessagePack integer、float,或表示该数字的 string。以字符串表示时,字符串内含十进制表示,有效数字(mantissa)可能超出 64 位浮点的表达能力 |
"bool" | 对应的 MessagePack boolean |
["list",T] | 与列表等长的 MessagePack array,元素按同一规则以嵌套类型T编码 |
["set",T] | 表示与["list",T]相同,但元素顺序未定义(Terraform 的 set 无序) |
["map",T] | MessagePack map,元素键序列化为 map key(始终是 string),值按T递归编码 |
["object",ATTRS] | MessagePack map,ATTRS中每个属性一个键值对,属性名作为 key |
["tuple",TYPES] | MessagePack array,TYPES的每个元素对应一个数组元素 |
"dynamic" | 恰好两个元素的 MessagePack array:第一个是 MessagePack binary,内含该值运行时类型约束的 JSON 序列化(格式同本表);第二个是按该类型规则编码的值本身 |
在这张表之外还有两条优先于所有类型映射的特殊规则:
- null 值编码为 MessagePack nil;
- unknown 值(apply 阶段才能确定的占位值)编码为 MessagePack extension 值,细节见下节。
unknown 值的处理:extension 与 refinements
unknown 值有两种表示,都是 MessagePack extension:
- 旧式编码:extension code 为0,extension 的 payload 完全被忽略;
- 新版 Terraform 可产生带 "refinements" 的 unknown 值:extension code 为12,payload 是一个 MessagePack map,用整数键区分不同种类的 refinement:
1:nullness。布尔值,true 表示最终值必然为 null,false 表示必然非 null;键缺失表示可能为 null 也可能非 null。2:字符串前缀。仅对 string 类型的 unknown 有效,表示最终值已知以该字符串开头。3/4:数字值的下界 / 上界。值是两元素 array,第一个元素是表中合法的数字编码,第二个是布尔值(true 表示闭区间边界)。仅对 number 类型有效。5/6:集合长度(list、set、map)的下界 / 上界。值是整数,表示包含该值的闭边界。
文档对实现方给出的规则:
- refinements 是可选信息,忽略它们、把 unknown 当完全未知处理总是安全的;但一个 provider 如果在其计划新状态(来自
PlanResourceChange)中产出了 refined 值,就必须在最终状态(来自ApplyResourceChange)中遵守这些 refinements。 - 反序列化代码应忽略不认识的 refinement 键,因为未来协议版本可能定义新的 refinement。
- 编码无 refinement 的 unknown 值时,必须使用 extension code 0,而不能用 extension code 12 加一个空的 refinement map;refined unknown 值必须至少带一条 refinement。这条规则保证与 refinement 概念出现之前的旧实现向后兼容。
- 解码侧应把任何extension code 都当作 unknown 值处理,并且除非 extension code 是 12,否则完全忽略 payload。未来版本的其它 extension code 也只会表示 unknown。
嵌套块按 nesting 模式聚合
每种嵌套块类型的各个 block 先按Schema.Block规则得到"块值",再由Schema.NestedBlock的nesting字段(Schema.NestingBlock.NestingMode枚举)决定如何聚合成一个属性值。除MAP外,块不允许带标签;MAP要求恰好一个标签(block label):
nesting值 | MessagePack 表示 |
|---|---|
SINGLE | 该唯一块值的块值;不存在该类型块时为 nil |
LIST | 所有块值的 MessagePack array,保持配置中块的定义顺序 |
SET | 所有块值的 MessagePack array,顺序不保证 |
MAP | MessagePack map,键为 block label,值为块值 |
GROUP | 同SINGLE,但当该类型块不存在时,Terraform 会合成一个块值:所有声明的属性视为 null,各声明块类型的块数为零 |
对LIST和SET模式,Terraform 保证数组元素数落在 schema 的min_items与max_items之间——除非块值中包含嵌套的 unknown 值;此时 Terraform 认为值可能不完整,会把元素数校验推迟到 apply 阶段(例如配置中有for_each为 unknown 的dynamic块时,最终块数在 apply 前不可预测)。
JSON 序列化规则
文档把 JSON 定位为DynamicValue的次要表示,MessagePack 之所以被优先,是因为它能通过 extension 表示 unknown 值。文档中 JSON 规则给出的特殊覆盖规则只有一条:null 值始终表示为 JSONnull。
属性映射规则与 MessagePack 表结构相同,区别在于各类型的落点:
type模式 | JSON 表示 |
|---|---|
"string" | JSON string |
"number" | JSON number。Terraform 数字是任意精度浮点,有效数字可能超出 64 位浮点的表达能力 |
"bool" | true或false |
["list",T] | 与列表等长的 JSON array,元素按T递归编码 |
["set",T] | 与 list 表示相同,元素顺序未定义 |
["map",T] | JSON object,元素键作为属性名,值按T递归编码 |
["object",ATTRS] | JSON object,ATTRS中每个属性一个属性 |
["tuple",TYPES] | JSON array,TYPES每个元素对应一个数组元素 |
"dynamic" | 含两个属性的 JSON object:"type"属性按表内类型模式给出值的精确运行时类型,"value"属性按该类型规则编码的值 |
嵌套块的nesting聚合规则与 MessagePack 一一对应,唯一文字差异是:不存在的SINGLE块表示为 JSONnull(对应 MessagePack 的 nil)。JSON 一节对LIST/SET模式的元素数保证没有附加 unknown 例外,直接保证落在min_items与max_items之间。
另外,文档明确 JSON 编码还用于UpgradeResourceState请求中RawValue消息的json字段;但那种情况下数据是按创建它的 provider 版本的 schema 序列化的,未必匹配当前 provider 版本的 schema。做状态升级时要把这一点算进解析逻辑。
两种编码的关键差异
对照上面两张表,有三处差异在实现时最容易踩:
"dynamic":MessagePack 侧是两个元素的 array(binary 类型约束 + 值),JSON 侧是带"type"/"value"两个属性的 object,形状不同,不能复用同一套解析逻辑。"number":MessagePack 侧允许再用 string 表示超大有效数字,JSON 侧只有 JSON number 一种落点。- unknown 值:只有 MessagePack 定义了表示方式(extension code 0 或 12),这也是"响应必须用 MessagePack"的根源。
实现完成后的对照检查点
文档给出的契约与硬约束可以作为实现自测的清单:
- 解码时按字段名取编码,
msgpack为空时能正确回退 JSON,两种格式都能解出值。 - 解出的值用标准 MessagePack/JSON 库即可处理,类型符合上文映射表(类型是契约性的,具体字节格式可能随 Terraform 版本变化,不要把某个具体字节形态写死成断言)。
- 生产值时只写
msgpack字段。 - 编码 unknown 值时遵守 code 0 / code 12 的分界规则:无 refinement 用 code 0;有 refinement 的 code 12 至少带一条 refinement,且不认识的 refinement 键忽略掉。
PlanResourceChange中产出的 refined 值,在ApplyResourceChange的最终状态里被遵守。LIST/SET元素数校验在 MessagePack 路径上要考虑嵌套 unknown 的豁免情形。
下一步:在 SDK 中落地这些定义
docs/plugin-protocol/README.md 给出了 SDK 开发者的操作路径:把需要支持的.proto文件复制到你的仓库,用protoc(带 gRPC 扩展)为目标语言生成 RPC stubs。README 中的 Python 示例命令(文档示例,按其原文保留):
protoc --python_out=. --grpc_python_out=. tfplugin5.1.proto注意本仓库docs/plugin-protocol/目录中现存的两个文件是 tfplugin5.proto 与 tfplugin6.proto,使用时请取你目标版本对应的文件(且以发布 tag 中的版本为准)。README 还建议把协议 major 版本号纳入包名(例如tfplugin5)以便日后并发支持多个 major 版本;升级到新的 minor 版本时原地替换旧 stubs 重新生成,而支持新 major 版本则应新建包并同时支持新旧两个 major,让用户不必同时升级 Terraform Core 与所有 provider。
【免费下载链接】terraformTerraform enables you to safely and predictably create, change, and improve infrastructure. It is a source-available tool that codifies APIs into declarative configuration files that can be shared amongst team members, treated as code, edited, reviewed, and versioned.项目地址: https://gitcode.com/GitHub_Trending/te/terraform
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考