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

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

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

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

摘要

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

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

设计初衷

开发者需要一种方式来为其组件声明配置架构。还必须构建用于解析该配置架构、对其进行编译并将其交付给 Component Manager 的流程。此提案旨在解决这些问题。

配置架构是面向用户的主要结构化配置界面。根据 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 部分。

archivist

当前配置在命令行实参随组件打包的 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 stanza。

对组件管理器做出的更改

我们将对组件管理器进行更改,以将结构化配置架构呈现给每个组件实例的 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 中解析了功能标志 (Prototype) 后面的配置段
  2. 使用 Hub 公开配置,集成测试(原型
  3. ffx component show 项更改

性能

  • 我们将对因结构化配置而导致组件启动时间增加的性能损失进行基准比较。我们已为此问题提交 bug
  • 此设计使用 FIDL 表来编码架构,这意味着与声明结构体相比,在解析 ComponentDecl FIDL 对象时会产生一些额外的开销。我们预计,与组件的总体启动时间相比,此开销可忽略不计。
  • 此设计将配置键存储为 ConfigField FIDL 对象中的字符串,这会消耗额外的磁盘空间,并且在启动组件时需要复制更多数据。
    • 这是非 ELF Runner 的要求,其中一些需要字符串键来以原生方式对配置值进行编码。
    • 这样一来,通过 Hub 进行调试也会更加轻松。
  • 对于 N 个配置键,我们预计配置值匹配的费用为 O(N)。在没有替换项的情况下,不会检查配置键是否相等。
  • 我们不关心 cmc 等宿主工具的性能。
  • Component Manager 不负责对配置架构或配置值进行哈希处理。cmc 会提前进行哈希处理。组件管理器只需检查架构文件和值文件中的哈希是否相等。
  • 组件管理器会将配置架构存储在 Hub 文件系统中。这可能会对组件管理器的内存使用量产生额外的不可忽略的影响。
    • 将 Hub 从基于推送改为基于拉取,即可解决此问题。已针对此功能请求提交bug

安全注意事项

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

隐私保护注意事项

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

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

测试

我们将:

  • 针对 cmc 的单元测试,用于测试 config 节的验证和编译(包括失败情况)
  • 针对组件管理器的单元测试,用于测试具有结构化配置的 Hub 的目录结构
  • 用于 Component Manager 的集成测试,可解析组件清单并验证 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>"
    ...
}
  • Pro:更简洁
  • 缺点:无法为该字段预留空间以供将来扩展。对于复杂类型和默认值,这可能是必需的。

选项 D:

config: [
    {
        <type string>: "<key>",
        ...
    },
],
  • 优点:更简洁。不得使用样板“类型”或“键”关键字
  • 优点:与功能路由语法保持一致
  • 缺点:重复的键需要进行显式检查
  • 缺点:不清楚如何将此功能与向量的 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 数据交换格式