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

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

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

问题
Gerrit 更改
作者
审核人
提交日期(年-月-日)2023-03-21
审核日期(年-月-日)2023-04-26

摘要

允许父级组件替换其子级的结构化配置中的值。

设计初衷

与 RFC-0127 中提出的方向一致,父级组件应能够向其子级提供配置值。例如,starnix_runner 能够直接将自己的配置值传递给其启动的 starnix_kernel,而无需创建配置目录和动态优惠,这对 starnix_runner 来说非常有益。

父级组件应能够在启动动态子级时传递配置值。例如,使用 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

Reviewers:

  • 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,Component Manager 将为每个配置字段解析值,并按以下顺序优先使用每个来源:

  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.config 中的 Value 类型。不过,fuchsia.component.config 目前依赖于 fuchsia.component.decl,因此使用软过渡来反转依赖关系需要很长时间。我们将在当前 API 级别将所有 fuchsia.component.config 移至 fuchsia.component.decl,并废弃并最终移除 fuchsia.component.config。合并这两个库时,我们将向相应类型添加 Config* 前缀,例如 Value 会变为 ConfigValue

在我们积累了该功能的使用经验,并且树外用户能够使用结构化配置之前,为此提案新定义的 FIDL API 接口仅在 API 级别 HEAD 提供。

除了对组件声明所做的更改之外,还需要更新 RealmBuilder 客户端库,以允许在 Child 声明中传递配置替换项。

性能

此 RFC 提议,Component Manager 将使用字符串相等性将替换项与其预期字段进行匹配,这比目前用于解析打包配置值的 O(1) 索引/偏移量比较操作要慢。这会为组件启动增加少量计算,但不太可能产生任何明显影响,因为目前组件启动时间通常为几百毫秒。需要说明的是,组件启动时间中的框架开销尚未成为任何面向用户的产品质量问题的重要因素。

工效学设计

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

如果无法描述组件配置接口的版本序列,架构演变将是一个手动和社交过程。我们或许将来可以解决此问题。

某些组件最终可能会有多个字段共享相同的可变性约束条件,而为每个字段重复相同的属性会造成人体工学负担。目前,我们没有任何符合此模式的用例,因此旨在解决此问题的语法是被拒绝的替代方案,我们日后可能会选择重新考虑。

向后和向前兼容性

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

对于不含 mutable_by 属性的配置字段,无需特别注意版本控制,因为这些字段的值只能从组件自己的软件包中提供。

未来,基于 FIDL 的版本控制可能会以更符合人体工学的方式协调这些步骤。

添加了新的可由父级修改的字段,为现有字段添加了可变性

无需考虑任何特殊事项,因为所有配置字段仍需要在组件自己的打包值文件中包含基准/默认值。该字段在组件的配置架构中存在后,父级便可为该字段提供值。

移除了可由父级修改的配置键,移除了可变性修饰符

如需从组件的架构中安全地移除可由父级更改的配置字段,您需要先处理父级组件,确保它们不再为要移除的字段传递任何替换项。

例如,如需从组件的配置接口中移除 parent_provided 配置字段,请执行以下操作:

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

重命名可由父级更改的字段

重命名字段相当于同时添加和移除字段,通常不应在单个步骤中执行。

更改可由父项修改的字段的类型

更改字段的类型相当于同时添加和移除,通常不应在单个步骤中执行。

更改可由父级更改的字段的约束条件

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

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

安全注意事项

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

我们将扩展 scrutiny,使其拒绝同时具有政策强制执行值且可由父级更改的配置字段。这将确保安全至关重要的配置字段绝不会被审核的静态验证范围之外的父级组件更改。

隐私注意事项

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

测试

config_encoder 库用于解析 ComponentManager、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 中提出的方法后,Component Manager 将有两个用于解析配置值的标识符:

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

正如下文中的替代方案中所述,有充分的理由优先使用字符串键来替换父级,并且在解析打包的值时,使用整数偏移量/序数的潜在动机对我们没有太大帮助。

我们应将打包的值移至编码字符串键,以减少组件管理器语义中的碎片化。下游用户应该不会注意到此更改,但这将简化配置解析的实现,并让日后进行调试变得更容易。

模糊配置解析

未来,我们将为组件管理器的清单解析和组件解析添加模糊测试工具。在这种情况下,我们将扩展模糊测试工具,以包含父级组件中的配置值,并断言系统会遵循可变性修饰符。

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

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

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

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

这条方向对人体工学有用,但不会受到我们发现的任何现有用例的推动。如果我们发现冗长性在实践中会造成严重影响,可以重新考虑这种方法。

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

替换 API 的整数序数

出于历史原因,打包的配置值会通过其在打包值列表中的偏移量解析为其字段。与检查字符串是否相等相比,这种方法可提供更高效的编码,并减少运行时开销。

为了让父级替换项也能获得相同的好处,我们需要子组件作者为其字段明确选择一个序数,类似于 FIDL 表。父级组件作者需要根据这些序数指定其替换的值,或者使用生成的库来获取人类可读的名称。

我们还需要设计机制来引导子组件作者避免重复使用整数序数,而精心选择的字符串键应该不太容易出现易于出错的重复使用问题。

最终,在我们预计的结构化配置使用规模下,从整数解析键所带来的二进制大小和运行时开销优势可以忽略不计。使用字符串键可让我们延迟生成绑定,并使子组件作者免于管理其序号的复杂性,同时对系统性能的影响降到最低。