RFC-0146:CML 中的结构化配置架构

RFC-0146:CML 中的结构化配置架构
状态已接受
区域
  • 组件框架
说明

关于 CML 中结构化配置架构的设计、实现策略和其他决策

问题
Gerrit 更改
作者
审核人
提交日期(年-月-日)2021-10-18
审核日期(年-月-日)2021-12-22

摘要

本文档以 RFC-0127 为基础,记录了 CML 中有关结构化配置架构的设计、实现策略和其他决策。此提案将提供配置架构的 JSON 语法、一组用于在组件清单中编码配置架构的 FIDL 类型,以及将配置架构从 JSON 转换为 FIDL 的过程。

请注意,“配置架构”是 RFC-0127 中定义的“配置定义文件”的具体实现。

设计初衷

开发者需要一种为其组件声明配置架构的方法。还必须构建用于解析该配置架构、对其进行编译并将其传递给组件管理器的过程。此提案旨在解决这些问题。

配置架构是面向用户的主要结构化配置界面。根据 RFC-0098 设置的标准,此功能需要 RFC 提案。

建议的清单语法和 FIDL 结构将包含在 Fuchsia SDK 中。为控制此功能的使用,清单文件中的配置架构将受 cmc 中的功能标志控制。

利益相关方

协调员:pascallouis@google.com

审核者:adamperry@google.com、jsankey@google.com、geb@google.com、pascallouis@google.com、cpu@google.com

咨询者:aaronwood@google.con、yifeit@google.com、surajmalhotra@google.com、shayba@google.com

共享:此设计已与组件框架团队分享,并成为组件框架设计讨论的主题。

设计

范围

此提案将以下内容视为在其范围内:

  • 支持 RFC-0127 指定的最低配置类型:布尔值、整数、长度受限的字符串以及这些数据类型的长度受限的矢量
  • 符合 RFC-0127 中指定的结构化配置哲学:简单、可靠、安全、可测试

此提案将以下内容视为未来的工作,不在本 RFC 的范围内:

CML 语法

我们将在组件清单中引入新的顶级键,以定义组件的结构化配置。相应的值包含每个配置字段的键。

结构化配置字段需要类型系统。Fuchsia 团队在为 FIDL 开发类型系统方面拥有丰富的经验,我们利用这些经验定义了与 FIDL 兼容且具有相同哲学的类型系统;例如,将布局与约束条件分离开来。可在结构化配置键中表示的一组类型目前是可在 FIDL 中表示的类型的子集。结构化配置和 FIDL 都会随时间推移而不断演变,我们认为结构化配置类型在某些方面可能会比 FIDL 类型更具表现力,例如,通过使用约束条件将数字限制在特定范围内。

为了保持一致性,我们使用 CML 样式和语法,并在存在相同概念的情况下,尽量使命名和分解保持一致。例如,FIDL 类型表示为布局,可选择进行参数化,也可选择进行约束。提议的 CML 语法扩展中存在相同的分解(类型具有可选属性);int32 等基元布局或 vector 等更复杂的布局具有相同的名称;约束条件可应用于类型,例如为矢量或字符串设置大小上限。

下面总结了对组件清单的更改:

{
    use: {
        ...,
    },
    ...
    config: {
        <key>: {
            type: "<type string>",
            <additional properties based on type>
        },
        ...
    }
}

config 是清单中的顶级键,其值为 JSON 字典。字典的每个成员都是一个配置字段,由键和类型组成。

配置键具有以下属性:

  • 它们是组件配置架构中的唯一标识符
  • 它们在系统组装和处理替换项时使用
    • 系统汇编使用已编译清单中的配置键创建配置值文件
    • 配置键是可用于定义父级替换项的稳定标识符
  • 必须符合正则表达式 [a-z]([a-z0-9_]*[a-z0-9])?
    • 此正则表达式与 FIDL、JSON 和潜在客户端库中的标识符兼容。
    • 这样可以为在需要时对键进行编码分隔符留出空间。
    • 我们日后还会对其进行扩展。
  • 长度不得超过 64 个字符
    • 我们日后可能会扩展此功能。

配置字段中的类型字符串仅限于以下值之一:

  • bool
  • uint8uint16uint32uint64
  • int8int16int32int64
  • string
  • vector

此设计支持 enumfloatstructarray 等类型,但不在本 RFC 的讨论范围之内。未来工作部分将介绍这些复杂类型的语法。

bool 和整数没有任何类型限制:

config: {
    enable_advanced_features: { type: "bool" },
    num_threads: { type: "uint64" },
}

string 必须具有 max_size 类型约束条件。max_size 会解析为 uint32

config: {
    network_id: {
        type: "string",
        max_size: 100,
    }
}

vector 必须具有 max_count 类型约束条件和 element 类型参数。max_count 会解析为 uint32element 仅限于 bool、整数或 string

config: {
    tags: {
        type: "vector",
        max_count: 10,
        element: {
            type: "string",
            max_size: 20,
        }
    }
}

示例

请考虑以下已适应使用结构化配置的组件清单。这些示例将重点介绍清单的 config 部分。

归档员

当前配置分为命令行参数与组件一起打包的 JSON 配置文件。此示例展示了现在如何将所有这些配置源整理为结构化配置。

config: {
    // proxy the kernel logger
    enable_klog: { type: "bool" },

    // initializes syslog library with a log socket to itself
    consume_own_logs: { type: "bool" },

    // connects to the component event provider. This can be set to false when the
    // archivist won't consume events from the Component Framework v1 to remove log spam.
    enable_component_event_provider: { type: "bool" },

    ...

    // initializes logging to debuglog via fuchsia.boot.WriteOnlyLog
    log_to_debuglog: { type: "bool" },

    // number of threads the archivist has available to use.
    num_threads: { type: "uint32" }
}

detect

samplerpersistencedetect 等程序会编译为“启动器”二进制文件,以节省空间。由于捆绑到启动器中的每个程序都有自己的清单,因此它们可以具有不同的结构化配置。

考虑使用命令行参数完成当前配置的 detect 程序:

program: {
    ...
    args: [
         // The mode is passed over argv because it does not vary for this component manifest.
         // Launcher will use the mode to determine the program to run.
         "detect"
    ]
},
config: {
    // how often to scan Diagnostic data
    // unit: minutes
    //
    // NOTE: in detect's CLI parsing, this is optional with a default specified in code.
    // when using structured config, the default would be provided by a build template or
    // by product assembly.
    check_every: { type: "uint64" },

    // if true, minimum times will be ignored for testing purposes.
    // never check in code with this flag enabled.
    test_only: { type: "bool" },
}

游戏机

当前配置是使用命令行参数完成的。此示例展示了在结构化配置中需要使用字符串矢量等高级类型。

config: {
    // Add a tag to the allow list. Log entries with matching tags will be output to
    // the console. If no tags are specified, all log entries will be printed.
    allowed_log_tags: {
        type: "vector",
        max_count: 40,
        element: {
            type: "string",
            max_size: 40,
        }
    },

    // Add a tag to the deny list. Log entries with matching tags will be prevented
    // from being output to the console. This takes precedence over the allow list.
    denied_log_tags: {
        type: "vector",
        max_count: 40,
        element: {
            type: "string",
            max_size: 40,
        }
    },
}

FIDL 规范

上述定义的 CML 语法必须由 cmc 编译为等效的 FIDL 对象,由组件管理器处理,并在实现的后续阶段用于替换解析。

使用 FIDL 作为配置架构是一种内部选择,可简化 cmc 和组件管理器中的实现。FIDL 不是最终开发者用于配置架构的接口。

ConfigSchema 对象包含:

  • 字段(ConfigField 对象)的有序列表
  • 架构校验和:对所有配置字段的哈希

ConfigField FIDL 对象由 keytype 组成。这两个字段的含义与 CML 语法中的含义相同:key 用于唯一标识配置字段,type 是配置值必须遵循的类型。

FIDL 编码最多允许 2^32 - 1 个配置字段。

cmc 将按确定性顺序对配置字段进行排序,但此 RFC 未指定该顺序。使用确定性顺序可确保架构校验和、下游工具和运行时配置解析的一致性。不指定顺序,以便日后进行优化。

架构校验和由 cmc 使用每个字段的键和值类型计算得出。此校验和值也会出现在配置值文件中。组件管理器会检查架构和值文件中的校验和是否完全相同,以防止出现任何版本偏差。

library fuchsia.component.decl;

// Config keys can only consist of these many bytes
const CONFIG_KEY_MAX_SIZE uint32 = 64;

// The string identifier for a config field.
alias ConfigKey = string:CONFIG_KEY_MAX_SIZE;

// The checksum produced for a configuration interface.
// Two configuration interfaces are the same if their checksums are the same.
type ConfigChecksum = flexible union {
    // A SHA-256 hash produced over a component's config interface.
    1: sha256 array<uint8>:32
};

/// The schema of a component's configuration interface.
type ConfigSchema = table {
    // Ordered fields of the component's configuration interface.
    1: fields vector<ConfigField>:MAX;

    // Checksum produced over a component's configuration interface.
    2: checksum ConfigChecksum;
};

// Declares a single config field (key + type)
type ConfigField = table {
    // The identifier for this config field.
    // This key will be used to match overrides.
    1: key ConfigKey;

    // The type of config values. Config values are verified
    // against this layout at build time and run time.
    2: type ConfigType;
};

// The type of a config value
type ConfigType = struct {
    layout ConfigTypeLayout;
    parameters vector<LayoutParameter>;
    constraints vector<LayoutConstraint>;
};

// Defines valid type ids for config fields.
type ConfigTypeLayout = flexible enum {
    BOOL = 1;
    UINT8 = 2;
    UINT16 = 3;
    UINT32 = 4;
    UINT64 = 5;
    INT8 = 6;
    INT16 = 7;
    INT32 = 8;
    INT64 = 9;
    STRING = 10;
    VECTOR = 11;
};

// Parameters of a given type layout
type LayoutParameter = table {
    // For vectors, this is the type of the nested element.
    1: nested_type ConfigType;
};

// Constraints on a given type layout
type LayoutConstraint = table {
    // For strings, this is the maximum number of bytes allowed.
    // For vectors, this is the maximum number of elements allowed.
    1: max_size uint32;
};

Component 是 CML 清单的 FIDL 等效项,现在必须包含 ConfigSchema FIDL 对象。

// *** component.fidl ***

library fuchsia.component.decl;
// NOTE: as long as the two libraries are supported, this change will also be made to
// library fuchsia.sys2;

/// A component declaration.
///
/// This information is typically encoded in the component manifest (.cm file)
/// if it has one or may be generated at runtime by a component resolver for
/// those that don't.
type Component = table {
    /// ... previous fields ...

    /// The schema of a component's configuration interface.
    10: config ConfigSchema;
};

cmc 的更改

我们将扩展 cmc,以使用 config 部分反序列化 CML,验证其内容,并将生成的架构包含在已编译的清单中。

我们将在 cmc 的功能标志后面实现此功能。在我们实现结构化配置的其余部分时,只有明确许可名单中的树内组件才能使用 config 诗节。

组件管理器的变更

我们将更改组件管理器,以便将结构化配置架构呈现给每个组件实例的 resolved/config 目录下的中心。将数据编码到集线器的命名空间中的确切编码将不稳定,并且不在本 RFC 的讨论范围内。

对中心进行的此更改将为我们提供一种创建集成测试的方法,以验证配置架构是否成功通过组件解析流水线。

ffx component 的更改

ffx component show 使用该中心输出组件实例的相关信息。随着对组件管理器所做的更改,此插件现在可以输出每个组件实例的配置架构。

$ ffx component show netstack
Moniker: /core/network/netstack
URL: fuchsia-pkg://fuchsia.com/network#meta/netstack.cm
Type: CML static component
Component State: Resolved
...
Configuration:
  log_packets [bool]
  verbosity [string:10]
  socket_stats_sampling_interval [uint32]
  opaque_iids [bool]
  tags [vector<string:10>:20]
Execution State: Running
...

请注意,上面显示的 ffx component show 命令输出可能会发生变化。在实际值解析实现之前,该命令只会输出配置架构。

实现

此设计的实现将分三个增量阶段完成。

  1. cm_rust 添加新类型,在功能标志后面解析 cmc 中的配置诗节(原型
  2. 使用集线器公开配置、集成测试(原型
  3. ffx component show 项更改

性能

  • 我们将对结构化配置对组件启动时间造成的额外性能下降进行基准测试。我们已针对此问题提交了bug
  • 此设计使用 FIDL 表编码架构,这意味着与声明结构体相比,解析 ComponentDecl FIDL 对象时会产生一些额外的开销。我们预计,与组件的总启动时间相比,此开销可以忽略不计。
  • 这种设计会将配置键作为字符串存储在 ConfigField FIDL 对象中,这会占用额外的磁盘空间,并且在启动组件时需要复制更多数据。
    • 这是非 ELF 运行程序的要求,其中一些运行程序需要字符串键才能原生编码配置值。
    • 这还可以简化通过集线器进行调试的操作。
  • 对于 N 个配置键,我们预计配置值匹配的开销为 O(N)。如果没有替换项,系统不会检查配置键是否相等。
  • 我们不关心 cmc 等托管工具的性能。
  • 组件管理器不负责对配置架构或配置值进行哈希处理。cmc 会预先进行哈希处理。组件管理器只需检查架构和值文件中的哈希是否相等即可。
  • 组件管理器将在集线器文件系统中存储配置架构。这可能会对组件管理器的内存用量产生额外的不可忽略的影响。
    • 将集线器改为基于拉取(而非基于推送)即可解决此问题。我们已为此功能请求提交bug

安全注意事项

作者没有发现任何潜在问题。请注意,此功能在 cmc 中使用许可名单进行控制,从而进一步降低安全风险。

隐私保护注意事项

配置键会显示在组件清单中。如果组件将公开发布,这些键不应包含专有信息。

作者没有发现任何其他潜在问题。

测试

我们将提供:

  • 用于测试 config 诗节的验证和编译(包括失败情况)的 cmc 单元测试
  • 用于测试中心目录结构的组件管理器单元测试,使用结构化配置
  • 用于解析组件清单并验证集线器是否显示其配置架构的组件管理器集成测试
  • 用于测试 ffx component show 是否能正确解析来自中心枢纽的结构化配置的单元测试。

文档

随着结构化配置的成熟,我们预计会添加以下文档:

  • 组件的结构化配置架构的 CML 语法
  • 组件的结构化配置架构的 FIDL 规范
  • 声明配置架构的最佳实践:注释、命名规范等
  • 示例/Codelab:向组件的清单添加 config 部分、构建组件、使用 ffx component show 验证配置字段

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

替代方案:config 部分指向 FIDL 文件

清单中的 config 部分可以指向描述配置架构的 FIDL 源文件。RFC-0127 详细介绍了此替代方案及其缺点。RFC-0127 中得出的结论在此处同样适用。

替代方案:program 下的 config 部分

program: {
    config: {
        <key>: {
            type: "<type string>",
            <additional JSON fields based on type>
        },
    }
}
  • 优点:在 programconfig 之间创建关联
  • 缺点:嵌套过多
  • 缺点:没有 program 部分的组件无法具有结构化配置。未来,配置路由可能涉及不含程序的组件。

替代方案:fields 部分

config: {
    fields: {
        <key>: {
            type: "<type string>",
        }
        ...
    }
}
  • 优点:为将来扩展配置架构留出空间。我们尚不确定是否需要这种未来可扩展性。
  • 缺点:不够简洁

替代方案:字段的详细程度

选项 B:

config: [
    {
        key: "<key>",
        type: "<type string>",
    }
    ...
]
  • 优点:使用 JSON 数组,而不是 JSON 对象。与清单的其他部分更一致:useexpose 等。
  • 缺点:此选项在视觉上与为定义的配置字段使用的结构体/表/映射语义不同。在 JSON 中,值(其中字符串键指向值)通常由映射/对象表示。
  • 缺点:更详细

选项 C:

config: {
    <key>: "<type string>"
    ...
}
  • 优点:更简洁
  • 缺点:无法为字段的未来扩展留出空间。对于复杂类型和默认值,可能需要执行此操作。

选项 D:

config: [
    {
        <type string>: "<key>",
        ...
    },
],
  • 优点:更简洁。不使用样板“type”或“key”关键字
  • 优点:与 capability 路由语法一致
  • 缺点:重复键需要进行显式检查
  • 缺点:不清楚这与矢量的 element 类型实参的运作方式

后续工作

清单分片中的结构化配置

在证明此工作是必要的之前,我们会暂时推迟执行此工作。我们也没有很好的策略来处理合并冲突。如果合并冲突导致编译停止,则每个分片都需要以防御性的方式命名配置字段。如果可以解决合并冲突,则不同的分片可能会无意中共用相同的配置字段。

配置架构中的默认值

配置字段可以支持默认值。这些默认值将使用 JSON 类型进行描述。RFC-0127 假定默认值应是配置架构的一部分,但目前,让 build 规则或子组件通过生成配置值文件提供默认值更为合理。

config: {
    enable_advanced_features: {
        type: "bool",
        default: false,
    }
    tags: {
        type: "vector",
        max_count: 10,
        element: {
            type: "string",
            max_size: 20,
        }
        default: [
            "foo",
            "bar",
            "baz",
        ]
    }
}

在能够证明子组件系统无法用于默认值之前,我们会推迟执行这项工作。

复杂数据类型

未来,我们有望添加对 arrayenumfloatstruct 等复杂类型的支持。vector 应支持这些类型,并且可能会尽可能完成额外的验证步骤。

config: {
    fsck: {
        type: "struct",
        fields: {
            check_on_mount: { type: "bool" },
            verify_hard_links: { type: "bool" },
            repair_errors: { type: "bool" },
        }
    },
    compression_type: {
        type: "enum",
        variants: {
            uncompressed: 0,
            zstd_chunked: 1,
        }
    },
    // Vectors can store complex structures
    coordinates: {
        type: "vector",
        max_count: 10,
        element: {
            type: "struct",
            fields: {
                x: { type: "int32" },
                y: { type: "int32" },
            }
        }
    },
}

配置字段注释的工具

上述示例中的配置字段包含 JSON 注释,用于更详细地描述配置字段。这些注释可以被处理并添加到结构化配置的其他区域。可以想象,Rust 和 C++ 中生成的客户端库具有相同的说明。

然后,如果开发者编写客户端代码,则会在编辑器中收到这些说明作为提示。系统汇编等工具还可以提供更详细的帮助和错误文本。

这需要对我们的 JSON 解析库进行更改,因为这些库目前不解析 JSON 注释。

config: {
      /// Add a tag to the allow list. Log entries with matching tags will be output to
      /// the console. If no tags are specified, all log entries will be printed.
      allowed_log_tags: {
          type: "vector",
          max_count: 40,
          element: {
              type: "string",
              max_size: 40,
          }
      },

      /// Add a tag to the deny list. Log entries with matching tags will be prevented
      /// from being output to the console. This takes precedence over the allow list.
      denied_log_tags: {
          type: "vector",
          max_count: 40,
          element: {
              type: "string",
              max_size: 40,
          }
      },
}

max_sizemax_count 的支持的最大值

清单可能需要为矢量和字符串中的 max_sizemax_count 属性使用支持的最大值。您可以使用 MAX 字符串来实现此目的,也可以直接忽略该属性。

config: {
    network_id: {
        type: "string",
        max_size: "MAX",
    }
}
config: {
    tags: {
        type: "vector",
        element: {
            type: "string"
        }
    }
}

先前技术和参考文献

组件清单语法

FIDL 语言规范

JSON5 数据交换格式