| RFC-0215:结构化配置父级替换 | |
|---|---|
| 状态 | 已接受 |
| 区域 |
|
| 说明 | 允许父组件在运行时向其子组件提供配置。 |
| 问题 | |
| Gerrit 更改 | |
| 作者 | |
| 审核人 | |
| 提交日期(年-月-日) | 2023-03-21 |
| 审核日期(年-月-日) | 2023-04-26 |
摘要
允许父组件替换其子组件的结构化配置中的值。
设计初衷
根据 RFC-0127 中提出的方向,父组件应能够向其子组件提供配置值。例如,starnix_runner 将受益于能够直接向其启动的 starnix_kernel 传递自己的配置值,而无需创建配置目录和动态优惠。
父组件应能够在启动动态子组件时传递配置值。例如,用 Rust 实现的父级应该能够编写如下代码:
let overrides = vec![ConfigOverride {
key: Some("parent_provided".into()),
value: Some(ConfigValue::Single(ConfigSingleValue::String(
"foo".to_string(),
))),
..ConfigOverride::EMPTY
}];
connect_to_protocol::<RealmMarker>()
.unwrap()
.create_child(
&mut CollectionRef { name: "...".into() },
Child {
// name, url, startup, ...
config_overrides: Some(overrides),
..Child::EMPTY
},
CreateChildArgs::EMPTY,
)
.await
.unwrap()
.unwrap();
未来,父组件应该能够在 CML 中配置静态子组件,并使用生成的库来配置动态子组件,从而在确保覆盖正确类型的同时减少冗余。
利益相关方
Facilitator: jamesr@google.com
审核者:
- geb@google.com (CF)
- ypomortsev@google.com (CF)
- markdittmer@google.com(安全)
咨询对象:lindkvist@google.com、hjfreyer@google.com
社会化:此提案在提交为 RFC 之前,已在组件框架团队成员和潜在客户中传播。
要求
- 最初,父级可能会向动态子级(包括使用 RealmBuilder 实例化的子级)提供配置值。最终应该可以在 CML 中配置静态子项,包括使用父项配置中的值。
- 组件作者可以演变内部/仅限测试/仅限开发者使用的配置字段,而无需与父组件协调。
- 父组件和子组件可以独立更新。子组件可以通过与父组件协调软过渡来演变其配置架构。
设计
为了使此功能正常运行,我们需要定义:
可由父级修改的配置字段
根据要求 (2),家长应只能覆盖由子组件标记为可变的配置字段。
组件作者将向任何应允许接收替换项的配置字段添加 mutable_by 属性。该属性将接受字符串列表,以适应未来的替换机制。最初,唯一接受的字符串将是 "parent"。CML 示例:
{
// ...
config: {
fields: {
enable_new_feature: {
type: "bool",
mutable_by: [ "parent" ],
},
},
}
}
可变性将指定为可能来源的列表,以便轻松扩展此语法以支持新的替换来源,包括最初由 RFC-0127 确定范围的开发者替换数据库。
我们未来可以考虑的另一种被拒绝的替代方案是按可变性对配置字段进行分组。
字符串键与偏移量/序号
Component Manager 目前使用编译后的配置架构中字段的偏移量来解析打包的配置值,这是一种小优化,要求组件的清单、配置值和编译后的二进制文件在配置架构的精确布局上达成一致。如果值提供方和配置的组件是使用不同(但兼容)的配置架构构建的,则无法实现此目的。
父级替换项将通过检查子级架构中键与所提供的替换项的字符串是否相等来解析,从而允许更改子级架构中字段的顺序和数量,而无需子级组件指定明确的序号或父级组件更新其替换项。
此方法的一种被拒绝的替代方案是使用整数序数来表示分辨率,并要求子级组件明确选择一个序数。
打包的默认值
具有 mutable-by-parent 配置字段的组件仍需在其打包的配置中提供默认值/基本值。这样一来,添加新的父级可见字段就会实现软过渡。
在未来,我们可能会允许组件要求其父级提供一些配置值。
无超额预配
父级提供的配置值文件只能包含子组件配置架构中存在且可由父级更改的字段的值。防止过度配置可为家长提供可预测的行为,而无需他们手动验证孩子是否已按预期配置。
父级值的优先级
组件配置“可根据组件实例的运行环境调整其行为”,这意味着最权威的配置值应是那些编码了组件实例环境最多知识的配置值。
根据 RFC-0127,组件管理器将解析每个配置字段的值,并按以下顺序优先选择每个来源:
- (未来)来自开发者替换服务的值
- 来自组件父级的值
- 组件自有软件包中的值
处理工程 build 的组件开发者可以了解系统上运行的所有内容,因此这些替换项最具权威性。父级可以了解其为子级提供的上下文。 最后,由于组件自身软件包中的值只能编码对组件打包方式的理解,因此所有其他信息都必须从外部来源提供。
在 Realm.CreateChild 中提供值
我们将扩展 fuchsia.component.decl/Child 以允许在运行时指定配置替换项:
library fuchsia.component.decl;
@available(added=HEAD)
type ConfigOverride = table {
1: key ConfigKey;
2: value ConfigValue;
};
type Child = table {
// ...
@available(added=HEAD)
6: config_overrides vector<ConfigOverride>:MAX;
};
虽然将此字段添加到 fuchsia.component.CreateChildArgs 后,父级可以为动态子级设置配置,但这无法提供为 CML 中静态定义的组件指定父级替换项的途径。拟议的方法可确保 Component Manager 在未来仅需考虑父级提供的值这一单一来源。
在运行时提供值的父组件必须与配置字段的声明类型完全一致。不会执行任何类型推断、类型转换或整数提升。
实现
所提出的设计已制造出原型机。
fuchsia.component.decl FIDL 库需要能够访问当前位于 fuchsia.component.config 中的 Value 类型。不过,fuchsia.component.config 目前依赖于 fuchsia.component.decl,因此需要很长时间才能通过软过渡反转依赖关系。相反,我们将把所有 fuchsia.component.config 移至当前 API 级别的 fuchsia.component.decl 中,并废弃最终移除 fuchsia.component.config。在合并这两个库时,我们将为适当的类型添加 Config* 前缀,例如 Value 将变为 ConfigValue。
此提案新定义的 FIDL API Surface 将仅在 API 级别 HEAD 提供,直到我们积累了有关该功能的经验,并且树外用户能够使用结构化配置为止。
除了更改组件声明之外,还需要更新 RealmBuilder 客户端库,以允许将配置替换项作为 Child 声明的一部分进行传递。
性能
此 RFC 建议组件管理器使用字符串相等性来匹配替换项及其预期字段,这将比当前用于解析打包配置值的 O(1) 索引/偏移量比较慢。这会为组件启动增加极少的计算量,但不太可能产生任何可观测的影响,因为组件启动时间目前通常在数百毫秒左右。从背景来看,组件启动时间中的框架开销尚未成为任何面向用户的产品质量问题的重要输入。
工效学设计
在此功能的第一版中,父组件需要知道被替换的配置字段的确切类型,这不如我们最终可以实现的此功能那样符合人体工程学。例如,组件作者需要匹配数值配置字段的确切整数宽度和有符号性。未来,我们可以定义更宽松的类型解析规则,以支持配置静态子项,我们还可以从子项的配置架构生成代码,这样语言编译器就可以代表开发者检查字段名称和类型。
如果无法描述组件配置接口的版本序列,架构演变将是一个手动且社交的过程。我们未来可能会解决这个问题。
某些组件最终可能会包含多个具有相同可变性限制的字段,但为每个字段重复相同的属性会带来人体工程学方面的负担。我们目前没有任何符合此模式的应用场景,因此旨在解决此问题的语法是一种被拒绝的替代方案,我们将来可能会选择重新考虑。
向后和向前兼容性
组件的作者在修改其配置架构中可由父组件更改的部分时,将负责与父组件协调软过渡。本部分介绍了针对配置架构修改的安全演变流程。
没有 mutable_by 属性的配置字段不需要特别注意版本控制,因为这些字段的值只能从组件自己的软件包中提供。
未来,这些步骤可能会通过基于 FIDL 的版本控制以更符合人体工程学的方式进行。
添加了新的 mutable-by-parent 字段,并为现有字段添加了可变性
无需特殊考虑,因为所有配置字段仍需要基本/默认值才能位于组件自己的打包值文件中。一旦该字段出现在组件的配置架构中,父级就能够为该字段提供值。
移除了可由父级更改的配置键,移除了可变性修饰符
如需从组件的架构中安全移除可由父级更改的配置字段,需要先处理父组件,确保它们不再传递任何针对要移除的字段的替换项。
例如,如需从组件的配置接口中移除 parent_provided 配置字段,请执行以下操作:
- 组件作者传达了弃用和移除
parent_provided的意图 - 父组件停止为
parent_provided指定值 - 组件作者从其配置架构中移除了
parent_provided
重命名可由父级更改的字段
重命名字段相当于同时添加和移除字段,一般不应在单个步骤中执行。
更改 mutable-by-parent 字段的类型
更改字段的类型相当于同时添加和移除字段,一般不应在一个步骤中完成。
更改可由父级实体更改的字段的限制
增加字段的 max_len 或 max_size 约束条件始终是安全的。
只有当所有父级提供的值都在新范围内时,减少字段的 max_len 或 max_size 限制才是安全的。
安全注意事项
scrutiny 工具可以对已构建的系统映像中组件的最终配置做出断言。这是一项重要的安全保障机制,可防止在构建和组装过程中发生意外的错误配置。
我们将扩展 scrutiny,使其拒绝同时具有政策强制值和可由父级更改的配置字段。这样可确保安全关键型配置字段永远不会被父组件在检查的静态验证范围之外发生突变。
隐私注意事项
父级替换功能允许组件将运行时数据作为配置值传递。虽然某些组件可能会选择使用此功能来传递用户数据,但目前还没有任何功能可以自动在日志、指标或遥测数据中记录结构化配置值。实现此功能不应影响隐私权。
测试
config_encoder 库用于解析 ComponentManager、scrutiny 和相关工具中的配置。其单元测试将得到扩展,以确保错误配置的父级替换项(密钥未知、类型错误、缺少可变性)将被拒绝。
结构化配置集成测试将得到扩展,以确保父组件可以使用 Realm 协议和 RealmBuilder 提供配置。
scrutiny 测试将得到扩展,以确保它拒绝在政策文件中具有静态声明值的可由父级更改的字段。
文档
CML 参考文档将更新,以匹配更新后的配置架构语法。
我们将撰写有关结构化配置架构演变的指南。本文将介绍添加和移除字段的最佳实践。其中将包含有关管理软过渡的指南。
我们将编写新的参考文档,介绍结构化配置值来源及其优先级。
现有关于验证 build 映像安全属性的文档将扩展,其中会说明为什么不允许将父级可变字段用于安全相关配置。
未来工作
为替换生成的绑定
上述提案意味着,父组件的作者需要使用字符串键和“动态类型”值来提供替换项。这会导致一些样板代码,并可能导致父级提供错误的键/值,或者忘记更新在有新字段可用时会产生替换项的代码路径。由于配置值配置不正确而导致的错误仅在启动子组件时才会出现在运行时。
我们最终可以通过允许父配置组件的作者生成“父级替换”库来改进此处的开发者体验。父组件的作者可以使用这些属性来减少输入的字符,并让编译器检查他们是否正确替换了子组件的配置:
let overrides = FooConfigOverrides {
parent_provided: Some("foo".to_string()),
..FooConfigOverrides::EMPTY
};
connect_to_protocol::<RealmMarker>()
.unwrap()
.create_child(
&mut CollectionRef { name: "...".into() },
Child {
// name, url, startup, ...
config_overrides: overrides.to_values(),
..Child::EMPTY
},
CreateChildArgs::EMPTY,
)
.await
.unwrap()
.unwrap();
在 CML 中配置子级
具有静态子级的父组件最终应该能够在 CML 中声明子级时提供配置值。例如:
{
children: [
{
name: "...",
url: "...",
config: {
parent_provided: "foo",
},
},
],
}
我们可能还希望提供一种语法,让父级直接将自己的配置值转发给静态子级。
我们需要确定,当提供的配置值是静态已知时,scrutiny 是否允许可由父级更改的字段。
如果我们为 CML 构建此功能,传递字面值将需要设计工作来弥合 JSON5 的宽松数字类型与结构化配置的精确类型之间的差距。我们需要为所有可能的 JSON5 输入选择 fuchsia.component.decl.ConfigValue 表示形式,并定义一些规则,以便组件管理器能够将这些值用于可能具有更严格限制的配置字段。如果我们等待基于 FIDL 的父/子关系表示,就可以避免大部分此类设计工作,因为 fidlc 将能够根据子配置架构精确检查类型。如果允许父级向子级传递自己的配置值,则无需进行此设计工作。
必需的父级配置值
某些组件可能需要其父级提供特定的配置值,而无法依赖打包的默认值。
这需要允许打包的值文件省略值,并教导解析配置的各种库如何处理这些省略的值。
虽然目前不需要这样做,但将来可能会成为有用的扩展功能。
基于 FIDL 的架构和版本控制
组件框架团队已探索将 FIDL 用于组件清单。如果实现了这一点,我们或许能够使用 FIDL 可用性注释来协调配置架构的演变。
使用字符串键解析打包的配置
采用此 RFC 中提出的方法后,组件管理器将使用两个标识符来解析配置值:
- 打包的值将使用编译后的配置架构中的整数偏移量进行解析
- 父级提供的值将使用配置架构中的字符串键进行解析
正如下文中的替代方案所讨论的那样,我们有充分的理由更倾向于使用字符串键进行父级替换,而使用整数偏移量/序号的潜在动机在解析打包值时对我们没有太大好处。
我们应将打包的值移至编码字符串键,以减少 Component Manager 语义中的碎片。此更改对下游用户来说应该几乎不可见,但会简化配置分辨率的实现,并使未来的调试更加轻松。
模糊测试配置解析
未来,我们将为 Component Manager 的清单解析和组件解析添加模糊测试器。如果发生这种情况,我们将扩展模糊测试器,以包含来自父组件的配置值,并断言可变性修饰符受到尊重。
缺点、替代方案和未知因素
具有共享可变性限制的分组字段
如果某个组件有许多字段都具有相同的可变性限制,我们可能会考虑允许组件作者将配置字段与共享属性分组在一起。举个(非规范性)示例,我们可以定义多个配置部分:
{
// ...
config: {
// fields which can only be resolved from a component's package
},
parent_config: {
// fields which are mutable by parent
},
}
此方向可能对人体工程学有用,但我们尚未发现任何现有用例能支持此方向。如果我们发现详细程度在实践中会带来显著的性能开销,可以重新考虑这种方法。
请注意,此方法可能会使组件作者更难定义具有多个可变性说明符的配置字段,例如“可由父级和替换服务更改”。
用于覆盖 API 的整数序号
出于历史原因,打包的配置值通过其在打包值列表中的偏移量解析为其字段。与检查字符串是否相等相比,这种方法可提供更高效的编码并减少运行时开销。
为了让父级替换获得同样的优势,我们需要子组件的作者明确为其字段选择一个序号,类似于 FIDL 表。父组件作者需要根据这些序号指定其替换值,或者使用生成的库来获取人类可读的名称。
我们还需要设计机制来引导子组件作者避免重复使用整数序号,而精心选择的字符串键应该不太容易出现容易出错的重复使用情况。
最终,从整数解析键所带来的二进制大小和运行时开销方面的优势,在我们预期的结构化配置使用规模下可以忽略不计。使用字符串键可让我们延迟生成绑定,并使子组件作者免受管理其序号的复杂性影响,同时对系统性能的影响极小。