RFC-0215:结构化配置父替换值

RFC-0215:结构化配置父级替换
状态已接受
领域
  • 组件框架
说明

允许父组件在运行时为其子组件提供配置。

问题
  • 96254
Gerrit 更改
  • 823149
作者
审核人
提交日期(年-月-日)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 中配置静态子项,并为动态子项使用生成的库以降低详细程度,同时确保替换项的类型正确。

利益相关方

教员:jamesr@google.com

审核者

  • geb@google.com (CF)
  • ypomortsev@google.com (CF)
  • Markdittmer@google.com(安全)

咨询人员:lindkvist@google.com、hjfreyer@google.com

社交:在以 RFC 形式提交之前,此提案会在组件框架团队成员和潜在客户之间传播。

要求

  1. 最初,父项可能会为动态子项提供配置值,包括使用 RealmBuilder 实例化的子项。最终应该可以在 CML 中配置静态子级,包括使用父级配置中的值。
  2. 组件作者可以改进内部/仅限测试/仅限开发者的配置字段,而无需与父组件协调。
  3. 父组件和子组件可能会单独更新。子项可以通过与父组件协调软转换来改进其配置架构。

设计

为使此功能正常运行,我们需要定义:

父项可变的配置字段

根据要求 (2),父组件只能替换被子组件标记为可变的配置字段。

组件作者会将 mutable_by 属性添加到应允许其接收替换项的所有配置字段中。该属性将接受一系列字符串,以适应未来的替换机制。最初,唯一接受的字符串是 "parent"。CML 示例:

{
    // ...
    config: {
        fields: {
            enable_new_feature: {
                type: "bool",
                mutable_by: [ "parent" ],
            },
        },
    }
}

可变性将指定为可能的来源列表,以便轻松地为新的替换来源扩展此语法,包括最初由 RFC-0127 设定范围的开发者替换数据库。

我们将来可以尝试的被拒绝的替代方案是按其可变性对配置字段进行分组

字符串键与偏移/序数

组件管理器目前使用已编译配置架构中的字段偏移量解析打包的配置值,这是一项小小的优化,要求组件的清单、配置值和已编译的二进制文件就配置架构的确切布局达成一致。如果值的提供程序和配置的组件是使用不同(但兼容)的配置架构构建的,则无法进行替换。

改为通过检查子项架构中的键的字符串相等性和所提供的替换项来解析父项替换项,允许子项架构中字段的顺序和数量发生变化,而无需子组件指定显式序数,或要求父组件更新其替换项。

此方法遭到拒绝的替代方案是使用整数序数进行解析,并要求子组件明确选择序数。

打包默认值

具有父项可变的配置字段的组件仍需要在打包配置中提供默认值/基本值。这会使添加新的父级可见字段变为软过渡。

将来,我们可能会允许组件要求其父级提供一些配置值。

不会过度预配

父组件提供的配置值文件只能包含子组件的配置架构中已存在且按父项可变的字段的值。防止过度预配配置可以为父级提供可预测的行为,而无需手动验证子级是否已按照预期进行配置。

来自父级的值的优先级

组件配置“根据组件的运行环境定制组件实例的行为”,这意味着最权威的配置值应该是编码最多的组件实例上下文的值。

根据 RFC-0127,组件管理器将解析每个配置字段的值,并按以下顺序优先选择每个来源:

  1. 来自开发者替换服务的值(未来)
  2. 值来自于组件的父级
  3. 组件自己的软件包中的值

开发工程 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 中静态定义的组件指定父级替换的路径。提议的方法可确保组件管理器将来只有一个父级提供的值的来源。

在运行时提供值的父组件必须与配置字段的声明类型完全匹配。不会执行类型推断、类型转换或整数提升。

实现

所提议的设计已进行原型设计

fuchsia.component.decl FIDL 库需要能够访问目前位于 fuchsia.component.configValue 类型。不过,fuchsia.component.config 目前依赖于 fuchsia.component.decl,因此需要很长时间才能颠倒与软过渡的依赖关系关系。相反,我们会将所有 fuchsia.component.config 移至当前 API 级别的 fuchsia.component.decl,废弃并最终移除 fuchsia.component.config。在合并两个库时,我们将在适当的类型中添加 Config* 前缀,例如 Value 变为 ConfigValue

为此提案新定义的 FIDL API 接口将只能在 API 级别 HEAD 使用,直到我们积累经验,并且树外用户能够使用结构化配置为止。

除了更改组件声明之外,还需要更新 RealmBuilder 客户端库,以允许将配置替换作为 Child 声明的一部分进行传递。

性能

此 RFC 提议组件管理器使用字符串相等性来匹配替换项与其预期字段,与当前用于解析打包配置值的 O(1) 索引/偏移比较操作相比,该操作的运行速度较慢。这会为组件启动增加极少量的计算量,但不太可能产生任何可观察的影响,因为目前组件的启动时间通常约为几百毫秒。就上下文而言,组件启动时间中的框架开销还不是导致用户面临的任何产品质量问题的重要因素。

工效学设计

在此功能的第一次迭代中,父组件需要知道被替换的配置字段的确切类型,该类型不像我们最终实现此功能时那样符合工效学要求。例如,组件作者需要与数字配置字段的整数宽度和符号完全匹配。将来,我们可以定义宽松的类型解析规则来支持配置静态子级,还可以从子级的配置架构生成代码,这样语言编译器就可以代表开发者检查字段名称和类型。

如果不能描述组件配置接口的版本顺序,架构演变将是一个手动的社交过程。这种情况或许可以在将来得到解决

某些组件最终可能会具有多个具有相同可变性约束的字段,并通过为每个字段重复相同的属性来征收人体工程学税费。我们目前还没有任何适合此模式的用例,因此旨在解决此问题的语法是一种遭到拒绝的替代方案,我们以后可能会选择重新考虑。

向后和向前兼容性

组件的作者在修改其配置架构的父组件可变部分时,将负责与其父组件协调软过渡。本部分介绍了修改配置架构的安全演变过程。

没有 mutable_by 属性的配置字段不需要特别注意版本控制,因为这些字段的值只能通过组件自己的软件包提供。

将来,通过基于 FIDL 的版本控制,这些步骤可能会更符合人体工程学的要求。

添加新的可变父项字段,为现有字段增加可变性

无需考虑任何特殊注意事项,因为所有配置字段仍然需要在组件自己的打包值文件中提供基本/默认值。当该字段出现在组件的配置架构中后,父级将能够为该字段提供一个值。

移除父项可变的配置键,同时移除可变性修饰符

若要从组件架构中安全地移除按父项可变的配置字段,首先需要使用父组件,以确保它们不再为要移除的字段传递任何替换项。

例如,如需从组件的配置接口中移除 parent_provided 配置字段,请使用以下代码:

  1. 组件作者会传达要废弃和移除 parent_provided 的 intent
  2. 父组件停止为 parent_provided 指定值
  3. 组件作者从其配置架构中移除了“parent_provided

重命名基于父项的可变字段

重命名字段相当于同时执行添加和移除操作,一般不应单步执行。

更改父项可变字段的类型

更改字段类型等同于同时执行添加和移除操作,而且通常不应执行一步。

更改父项可变字段的约束条件

增加字段的 max_lenmax_size 约束条件始终是安全的。

仅当父级提供的所有值都在新范围内时,减小字段的 max_lenmax_size 约束条件才是安全的。

安全注意事项

scrutiny 工具可以针对已构建的系统映像中组件的最终配置断言。这是一种重要的保护机制,可防止在构建和组装流程中发生意外配置错误。

我们将扩展 scrutiny,使其拒绝同时具有实施政策的值且可变的父项的配置字段。这将确保安全关键配置字段绝不会被审查静态验证范围之外的父组件更改。

隐私注意事项

父级替换允许组件将运行时数据作为配置值传递。虽然某些组件可能会选择使用此功能传递用户数据,但目前还没有任何功能可以在日志、指标或遥测中自动记录结构化配置值。实现此功能应该不会对隐私保护造成影响。

测试

config_encoder 库用于解析组件管理器、scrutiny 和相关工具中的配置。其单元测试将进行扩展,以确保错误预配的父级替换项(未知键、类型错误、缺少可变性)会被拒绝。

结构化配置集成测试将进行扩展,以确保父组件可以使用 Realm 协议和 RealmBuilder 提供配置。

scrutiny 测试将扩展,以确保其拒绝在政策文件中具有静态声明值的可变的父项字段。

文档

CML 参考文档将更新,以与更新后的配置架构语法保持一致。

我们将编写有关结构化配置架构演变的指南。该部分将介绍添加和移除字段的最佳做法。其中包括管理软转换的指导。

我们将编写关于结构化配置值来源及其优先级的新参考文档。

我们将对有关验证已构建映像安全属性的现有文档进行扩展,并解释为什么父可变字段不允许用于安全相关配置。

后续工作

为替换生成的绑定

上述方案意味着,父组件的作者需要使用字符串键和“动态类型”值来提供替换值。这会导致产生一些样板,并使父项可能会提供错误的键/值,或者忘记更新代码路径,从而在有可用的新字段时生成替换项。由于配置值配置不正确而导致的错误只会在运行时启动子组件时出现。

我们最终可以通过允许父级配置的组件的作者生成“父级替换”库来改善开发者体验。父组件的作者可以使用这些方法来减少输入的字符,并让编译器检查它们是否正确替换了子项的配置:

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 中提议的方法,组件管理器将有两个用于解析配置值的标识符:

  1. 打包的值将使用已编译配置架构中的整数偏移进行解析
  2. 将使用配置架构中的字符串键解析父级提供的值

下面的替代方案中所述,使用字符串键进行父级替换是有充分理由的,并且在解析打包的值时,使用整数偏移/序数的潜在动机对我们没有太大助益。

我们应将打包的值移至对字符串键进行编码,以减少组件管理器语义中的碎片。这项变更在很大程度上应该对下游用户不可见,但可以简化配置解析的实现,并使未来的调试变得更轻松。

模糊测试配置解析

将来,我们将为组件管理器的清单解析和组件解析添加一个模糊测试工具。发生这种情况时,我们将扩展模糊测试工具以包含来自父组件的配置值,并断言会遵循可变性修饰符。

缺点、替代方案和未知情况

对具有共同可变性约束的字段进行分组

如果某个组件有许多字段,所有字段都具有相同的可变性约束,我们可能会考虑允许组件作者将配置字段和共享属性组合在一起。作为(非规范性)示例,我们可以定义多个配置部分:

{
    // ...
    config: {
        // fields which can only be resolved from a component's package
    },
    parent_config: {
        // fields which are mutable by parent
    },
}

此方向对人体工程学而言可能很有用,但不受我们已确定的任何现有用例的激励。如果发现详细报告在实践中是一项重大负担,我们可以再次考虑此方法。

请注意,这种方法可能会使组件作者更难以定义具有多个可变性说明符的配置字段,例如“可由父项和通过替换服务可变”。

替换 API 的整数序数

由于历史原因,通过使用打包值列表中的偏移量,系统会将打包的配置值解析为相应的字段。与检查字符串相等性相比,这种方法的编码效率更高,运行时开销更小。

为了实现与父组件替换项相同的优势,我们需要子组件的作者为其字段明确选择序数,类似于 FIDL 表。父组件作者需要根据这些序数指定被替换的值,或者使用生成的库生成便于用户辨认的名称。

我们还需要设计相应的机制,引导子组件作者避免重复使用整数序数,而精心选择的字符串键应该可以降低容易出错的风险。

归根结底,就我们预期的结构化配置使用规模而言,解析整数键带来的二进制文件大小和运行时开销优势是微乎其微的。使用字符串键让我们可以推迟生成的绑定,并将子组件作者免于管理其序数的复杂性,同时将系统性能的成本降至最低。