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 部分。

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" },
}

console

当前配置使用命令行实参完成。此示例展示了结构化配置中对字符串向量等高级类型的需求。

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 中的配置节 (原型)
  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>",
        ...
    },
],
  • 优点:更加简洁。没有样板“type”或“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 数据交换格式