ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

OpenTofu Provider Wire Format 深度解析:DynamicValue 的 MessagePack 与 JSON 序列化规则

OpenTofu Provider Wire Format 深度解析:DynamicValue 的 MessagePack 与 JSON 序列化规则 OpenTofu Provider Wire Format 深度解析DynamicValue 的 MessagePack 与 JSON 序列化规则【免费下载链接】opentofuOpenTofu lets you declaratively manage your cloud infrastructure.项目地址: https://gitcode.com/gh_mirrors/op/opentofu导读本文以 docs/plugin-protocol/object-wire-format.md 为核心系统讲解 OpenTofu Provider 协议协议版本 5 及 6中DynamicValue消息的线上编码格式OpenTofu 如何把自身的类型系统来自resource、data、provider块求值结果翻译为 MessagePack 与 JSON 两种动态序列化格式。无论你是 Provider 的服务器端server实现者需要解码 OpenTofu 发来的配置、状态值还是需要在自己的响应消息中正确构造DynamicValue读完后你都将掌握两种格式下 Block、Attribute、NestedBlock 的逐类型映射规则理解 unknown value 的扩展extension编码与 refined unknown 语义以及服务端实现中应当遵循的编码与回退策略。DynamicValue为什么需要两种动态序列化格式OpenTofu 与 Provider 之间的 gRPC 协议中DynamicValue是一个不透明opaque的消息类型其结构在运行时才由 Provider 返回的 Schema 决定因此在协议层无法用固定的 protobuf 字段描述。它提供了两个字节字段分别承载两种序列化方案定义见 tfplugin5.0.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; }msgpack字段 1首选编码。MessagePack 是紧凑的二进制表示体积小、解析快是 OpenTofu 最常使用的格式。json字段 2回退编码。服务器端实现应在msgpack字段未填充时回退到 JSON从而同时支持两种格式。服务端在产生DynamicValue时即各种 RPC 响应消息中应始终使用 MessagePack 编码因为 OpenTofu 并非在所有请求类型、所有版本上都一致支持 JSON 响应。换言之解码时要两者都支持编码时只发 MessagePack。DynamicValue在协议中被广泛用于承载各类数据。例如 tfplugin5.0.proto 中PrepareProviderConfig的请求/响应、ValidateResourceTypeConfig、PlanResourceChange、ApplyResourceChange、ReadDataSource等几乎所有核心 RPC 都以它为载荷。在 OpenTofu 的客户端实现 internal/plugin/grpc_provider.go 中可以看到Configure、ReadResource、PlanResourceChange、ApplyResourceChange、ImportResourceState等方法的收发两端都通过msgpack.Marshal/decodeDynamicValue完成转换处处印证MessagePack 为主这一约定。编码的驱动信息Schema无论 MessagePack 还是 JSON 序列化都由 Provider 此前在GetSchema的Schema消息中返回的信息驱动。OpenTofu 会根据 Schema 中给定的类型约束type constraint为每个值选择最贴近的 MessagePack / JSON 类型进行编码。因此服务端实现只需用标准的 MessagePack 或 JSON 库解码序列化后的值即可假定它符合下文描述的规则。Schema消息中与编码直接相关的结构tfplugin5.0.protomessage Schema { message Block { int64 version 1; repeated Attribute attributes 2; repeated NestedBlock block_types 3; } message Attribute { string name 1; bytes type 2; // 即文档中所述 type 字段OpenTofu 类型约束的紧凑 JSON 序列化 string description 3; bool required 4; bool optional 5; bool computed 6; bool sensitive 7; } message NestedBlock { enum NestingMode { INVALID 0; SINGLE 1; LIST 2; SET 3; MAP 4; GROUP 5; } string type_name 1; Block block 2; NestingMode nesting 3; int64 min_items 4; int64 max_items 5; } int64 version 1; Block block 2; }其中Attribute.typebytes 类型实际存放的是 OpenTofu 类型约束的紧凑 JSON 序列化要么是单个字符串原始类型如string要么是两元素数组类型种类 类型参数如[list,string]。这正是下文两张映射表第一列的输入。MessagePack 序列化规则总则与两条全局特殊规则下表给出各类型约束到 MessagePack 表示的映射。两条特殊规则在任何类型上都优先于表中规则null 值一律表示为 MessagePack 的 nil 值。unknown 值仅在 apply 阶段才能确定值的占位符表示为 MessagePack 扩展extension值具体编码见下文unknown 值的两种表示。type模式MessagePack 表示stringMessagePack 字符串内容为字符串值按规范化 UTF-8 序列化的 Unicode 字符。numberMessagePack 整数、浮点数或字符串。若以字符串表示则内容为该数字的十进制表示其尾数mantissa可能超过 64 位浮点数的表示范围。boolMessagePack 布尔值。[list,T]MessagePack 数组元素个数与列表值相同每个元素由对嵌套类型T应用同样的映射规则得到。[set,T]与[list,T]表示完全相同但元素顺序未定义——OpenTofu 的 set 本身无序。[map,T]MessagePack map每个元素一对键值元素键作为 map 键恒为 MessagePack 字符串元素值由对嵌套类型T应用同样规则构造。[object,ATTRS]MessagePack mapATTRS对象中每个属性一对键值属性名作为 map 键恒为 MessagePack 字符串属性值由对每个属性自身类型应用同样规则构造。[tuple,TYPES]MessagePack 数组TYPES数组描述每个元素元素值由对TYPES中对应元素应用同样规则构造。dynamicMessagePack 数组恰好两个元素第一个元素是二进制值内含与表中相同格式的 JSON 序列化类型约束第二个元素是按第一个元素给出的类型对值应用同样规则的结果。该特殊类型约束表示类型只在运行时才确定的值。需要注意MessagePack 为每个类型定义了多种合法序列化格式OpenTofu 可能在版本间选择不同格式但上表给出的类型是契约性的contractual。反之服务端在产生MessagePack 编码值时可自由选用某一类型的任意合法格式但推荐选择能在不损失数值范围的前提下最紧凑的格式。Schema.Block 的映射规则为了把块内容表示为 MessagePackOpenTofu 构造一个 MessagePack map其中每个属性一对键值、Schema.Block消息描述的每种嵌套块类型一对键值表示属性的键值对其值遵循Schema.Attribute的映射规则表示嵌套块类型的键值对其值遵循Schema.NestedBlock的映射规则。Schema.NestedBlock 的映射规则嵌套块集合的序列化取决于Schema.NestedBlock消息的nesting字段即Schema.NestingBlock.NestingMode枚举值。所有nesting值下单个块都由Schema.Block映射规则基于block字段编码得到下文所称的块值block value。随后nesting决定如何把各块值聚合成代表该嵌套块类型的单一属性值。除MAP外各模式不允许块带标签labelMAP模式要求块恰好有一个标签即下表中的块标签。nesting值MessagePack 表示SINGLE该类型唯一块的块值若无该类型块则为 nil。LIST所有块值的 MessagePack 数组保持配置中块的定义顺序。SET所有块值的 MessagePack 数组顺序不定。MAPMessagePack map每块一对键值键为块标签值为块值。GROUP与SINGLE相同区别在于若无该类型块OpenTofu 会合成一个块值——把声明过的属性全部视为 null、各声明块类型数量视为 0。LIST与SET模式还有一个数量保证OpenTofu 保证 MessagePack 数组的元素个数介于 Schema 给定的min_items与max_items之间——除非任一块值内含嵌套的 unknown 值。此时 OpenTofu 认为值可能不完整从而推迟块数量的校验。典型场景配置中的dynamic块其for_each参数是 unknown 值最终块数只能到 apply 阶段才可预测。unknown 值的两种表示与 refined unknownunknown 值有两种可能表示均使用 MessagePack 扩展extension值旧版编码无精化扩展码为 0扩展值载荷完全被忽略。适用于未精化的 unknown 值。新版编码精化refined扩展码为 12扩展对象载荷是一个以整数键编码的 MessagePack map每种键表示一种精化对最终值可能范围的约束键含义载荷格式适用类型1nullness是否确定为空布尔值true 表示确定为空false 表示确定非空键缺失表示可能空也可能非空。实际编码中没必要把 unknown 编码为确定为空——每类型只有一个 null 值直接用已知 null 即可。任意2字符串前缀字符串最终值已知以此开头。仅 string 类型的 unknown3数字下界两元素 msgpack 数组第一个元素是数字的有效编码同上表第二个元素是布尔值——true 表示闭区间含该界false 表示开区间不含该界。仅 number 类型的 unknown4数字上界同键3的格式。仅 number 类型的 unknown5集合长度下界整数表示闭区间下界。仅 list、set、map 三种集合类型的 unknown6集合长度上界整数表示闭区间上界。仅 list、set、map 三种集合类型的 unknown关于精化的使用语义务必注意以下契约精化是可选的目的是在 unknown 输入下仍能算出已知结果或把只能在 apply 阶段暴露的错误提前到 plan 阶段发现。把精化忽略、把 unknown 完全当作全然未知处理永远是安全的但考虑精化可能得出更精确的答案。Provider 在PlanResourceChange返回的 planned new state 中产生了精化值则必须在ApplyResourceChange返回的最终 state 中兑现这些精化。反序列化代码应忽略未知的精化键——未来协议版本可能定义更多精化种类。两条编码一致性规则编码无任何精化的 unknown 值时总是使用旧版格式扩展码 0而不是用扩展码 12 带一个空精化 map任何精化 unknown 值必须至少包含一条精化 map 条目。这保证与精化概念出现之前的旧实现向后兼容。服务端实现应把任何MessagePack 扩展码都视为 unknown 值但除非扩展码为 12表示载荷为精化否则应完全忽略扩展值载荷。未来协议版本可能为其他扩展码定义特定格式但它们永远表示 unknown 值。JSON 序列化规则JSON 序列化是DynamicValue的次级表示MessagePack 因能通过扩展表示 unknown 值而更受青睐。需要特别说明本节描述的 JSON 编码也用于UpgradeResourceState请求中RawValue消息的json字段——但此时数据按创建它的那个 Provider 版本的 Schema 序列化未必与当前 Provider 版本的 Schema 一致旧状态升级场景见 tfplugin5.0.proto 中raw_state与upgraded_state的注释。全局特殊规则与 MessagePack 不同JSON 只有一条全局规则null 值一律表示为 JSON 的null。unknown 值在 JSON 中无法用扩展表示这也是 MessagePack 成为首选编码的原因之一。Schema.Attribute 的映射规则type模式JSON 表示stringJSON 字符串内容为字符串值的 Unicode 字符。numberJSON 数字。OpenTofu 数字是任意精度浮点尾数可能超过 64 位浮点表示范围。boolJSONtrue或false。[list,T]JSON 数组元素个数与列表值相同每个元素由对嵌套类型T应用同样规则得到。[set,T]与[list,T]相同但元素顺序未定义。[map,T]JSON 对象每个元素一个属性元素键作为属性名字符串元素值作为属性值。[object,ATTRS]JSON 对象ATTRS中每个属性一个属性属性名为属性名字符串属性值由对每个属性自身类型应用同样规则构造。[tuple,TYPES]JSON 数组TYPES中每个元素对应一个数组元素。dynamicJSON 对象含两个属性type以带内in-band方式给出该值的精确运行时类型即本表所述某种type模式value为按指定运行时类型应用同样规则的结果。Schema.Block 与 Schema.NestedBlock 的映射规则块内容表示为 JSON 对象每个属性一个属性、每种嵌套块类型一个属性。属性值遵循Schema.Attribute映射规则嵌套块属性值遵循Schema.NestedBlock映射规则。嵌套块的nesting聚合方式与 MessagePack 完全同构nesting值JSON 表示SINGLE该类型唯一块的块值无该类型块则为null。LIST所有块值的 JSON 数组保持定义顺序。SET所有块值的 JSON 数组顺序不定。MAPJSON 对象每块一个属性属性名为块标签值为块值。GROUP同SINGLE但无该类型块时合成一个块值属性视为 null、块类型数量视为 0。LIST与SET模式下JSON 数组元素个数保证介于min_items与max_items之间这一点上 JSON 规则未像 MessagePack 那样声明 unknown 时的例外但语义与 MessagePack 规则一致。源码印证OpenTofu 客户端如何解码 DynamicValueOpenTofu 自身的 gRPC 客户端即运行在核心进程内、与 Provider 通信的一端在 internal/plugin/grpc_provider.go 中给出了一个完整的参考实现验证了文档描述的解码约定// Decode a DynamicValue from either the JSON or MsgPack encoding. func decodeDynamicValue(v *proto.DynamicValue, ty cty.Type) (cty.Value, error) { // always return a valid value var err error res : cty.NullVal(ty) if v nil { return res, nil } switch { case len(v.Msgpack) 0: res, err msgpack.Unmarshal(v.Msgpack, ty) case len(v.Json) 0: res, err ctyjson.Unmarshal(v.Json, ty) } return res, err }从源码结构可以确认以下实现事实优先级明确只要msgpack字段非空就优先用cty/msgpack解码json字段仅在 msgpack 缺失时作为回退——与文档OpenTofu 最常使用 MessagePack的表述一致。类型驱动解码必须携带ty cty.Type由 Schema 推导的类型印证两种序列化都由 Schema 中返回的类型约束驱动这一设计。编码方向单一同一文件中所有产生DynamicValue的调用点Configure、ReadResource、PlanResourceChange、ApplyResourceChange、ImportResourceState、ReadDataSource、CallFunction等都只填充Msgpack字段例如Config: proto.DynamicValue{Msgpack: mp}落实了服务端应答只用 MessagePack的规范。双向测试覆盖在 internal/plugin/grpc_provider_test.go 中测试同时构造Msgpack如[]byte(\x81\xa4attr\xa3bar)与Json如[]byte({attr:bar})两种输入的DynamicValue证明客户端对两种入站格式都能正确解码也间接说明协议兼容双方格式是必须满足的契约。协议 6 的对应实现位于 internal/plugin6消息定义见 tfplugin6.0.proto其DynamicValue与类型映射语义与协议 5 保持一致。服务端实现者的操作清单综合文档规则与 OpenTofu 客户端实现Provider 服务器端应遵循以下实践解码入站DynamicValue用标准 MessagePack / JSON 库解析优先msgpack字段缺失时回退json解析出的结构应符合本文两张映射表。OpenTofu 保证发送值贴合对应 Schema 类型无需额外猜测。识别 unknownMessagePack 场景下任何扩展码都视为 unknown 值扩展码 12 时按整数键 map 解析精化其余扩展码忽略载荷。务必忽略未知精化键以兼容未来协议扩展。编码出站DynamicValue一律填充msgpack字段不要在响应中依赖 JSON。单个值可从该类型的任意合法 MessagePack 格式中选最紧凑者。无精化 unknown 用扩展码 0若产生精化值扩展码 12 且至少一个精化条目并在ApplyResourceChange的最终状态中兑现 plan 阶段给出的精化。状态升级场景UpgradeResourceState请求中的raw_stateRawValue.json按旧 Provider 版本的 Schema 编码需用相应旧 Schema 解析后再以当前 Schema 重编码为upgraded_state。总结DynamicValue是 OpenTofu Provider 协议中承载一切动态结构数据的通用载体其msgpack与json双字段分别对应两种等价的序列化方案映射规则完全由 Provider 返回的SchemaBlock/Attribute/NestedBlock含NestingMode与min_items/max_items驱动。MessagePack 通过扩展机制为 OpenTofu 类型系统补齐了 unknown 值的表达能力——从扩展码 0 的无精化占位符到扩展码 12 携带 nullness、字符串前缀、数字上下界、集合长度上下界等精化信息是 plan 阶段提前发现错误、unknown 输入下求得精确结果的关键机制JSON 则作为通用回退格式额外服务于旧状态升级UpgradeResourceState。对 Provider 开发者而言解码双向兼容、编码只用 MessagePack、忽略未知精化、兑现 plan 精化这四条原则足以保证与当前及未来 OpenTofu 版本的无缝互操作。【免费下载链接】opentofuOpenTofu lets you declaratively manage your cloud infrastructure.项目地址: https://gitcode.com/gh_mirrors/op/opentofu创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表