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

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

在 CML 中设计、实现策略以及针对结构化配置架构制定的其他决策

问题
  • 86128
Gerrit 更改
  • 595204
作者
审核人
提交日期(年-月-日)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" }
}

检测

为了节省空间,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 目录下的 hub 下。进入 hub 命名空间的确切编码将不稳定,并且不在此 RFC 的讨论范围内。

对 hub 的这项更改使我们能够创建集成测试,以验证配置架构是否成功通过组件解析流水线。

ffx component 的更改

ffx component show 使用 hub 输出有关组件实例的信息。对组件管理器进行更改后,此插件现在可以输出每个组件实例的配置架构。

$ 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 中的配置 stanza,位于功能标志后面(原型设计
  2. 使用 hub 公开配置,进行集成测试(原型设计)
  3. ffx component show 项更改

性能

  • 由于结构化配置,我们会将增加的性能命中与组件开始时间进行基准化分析。已提交有关此问题的 bug
  • 此设计使用 FIDL 表对架构进行编码,这意味着与声明结构体相比,在解析 ComponentDecl FIDL 对象时会产生一些额外的开销。我们预计,与组件整体启动时间相比,此开销可以忽略不计。
  • 此设计将配置键以字符串形式存储在 ConfigField FIDL 对象中,这会占用额外的磁盘空间,并且在启动组件时需要复制更多数据。
    • 这是非 ELF 运行程序的要求,其中某些运行程序需要字符串键以原生方式对配置值进行编码。
    • 这也更便于通过 hub 进行调试。
  • 对于 N 个配置密钥,我们预计配置值匹配会带来 O(N) 费用。在没有替换项的情况下,系统不会检查配置键是否相等。
  • 我们并不关注 cmc 等主机工具的性能。
  • 组件管理器不负责对配置架构或配置值进行哈希处理。cmc 会提前执行哈希处理。组件管理器只需要检查架构和值文件中的哈希是否相等。
  • 组件管理器会将配置架构存储在 hub 文件系统中。这可能会对组件管理器的内存用量产生额外的不容忽视影响。
    • 让 hub 基于拉取而不是基于推送可以解决这一问题。已提交此功能请求 bug

安全注意事项

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

隐私保护注意事项

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

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

测试

我们将有:

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

文档

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

  • 组件的结构化配置架构的 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>",
        ...
    },
],
  • 优点:更简洁。没有样板“类型”或“键”关键字
  • 优点:与功能路由语法一致
  • 缺点:重复键需要显式检查
  • 缺点:不清楚这会如何与矢量的 element 类型参数一起使用

后续工作

清单分片中的结构化配置

我们将推迟这项工作,直到能够证明有必要进行为止。我们也没有很好的合并冲突处理策略。如果合并冲突导致编译停止,则每个分片都需要以防御方式为配置字段命名。如果可以解决合并冲突,则不同的分片可能会无意中共享相同的配置字段。

配置架构中的默认值

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

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 数据交换格式