RFC-0215: Structured Configuration Parent Overrides

RFC-0215: Structured Configuration Parent Overrides
StatusAccepted
Areas
  • Component Framework
Description

Allow parent components to provide configuration at runtime to their children.

Issues
Gerrit change
Authors
Reviewers
Date submitted (year-month-day)2023-03-21
Date reviewed (year-month-day)2023-04-26

Summary

Allow parent components to override values in their children's structured configuration.

Motivation

In line with the proposed direction in RFC-0127, parent components should be able to provide configuration values to their children. For example, starnix_runner would benefit from being able to pass its own configuration values directly to the starnix_kernels it launches without creating configuration directories and dynamic offers.

A parent component should be able to pass configuration values when launching a dynamic child. For example, a parent implemented in Rust should be able to write code like:

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();

In the future it should be possible for the parent component to configure a static child in CML and use a generated library for dynamic children to reduce verbosity while ensuring correct types for overrides.

Stakeholders

Facilitator: jamesr@google.com

Reviewers:

  • geb@google.com (CF)
  • ypomortsev@google.com (CF)
  • markdittmer@google.com (Security)

Consulted: lindkvist@google.com, hjfreyer@google.com

Socialization: This proposal was circulated among Component Framework team members and prospective customers before being submitted as an RFC.

Requirements

  1. Initially, parents may provide configuration values to dynamic children, including those instantiated using RealmBuilder. It should eventually be possible to configure static children in CML, including using values from the parent's configuration.
  2. Component authors may evolve internal/test-only/developer-only configuration fields without coordinating with parent components.
  3. Parent and child components may be updated independently. Children may evolve their configuration schema by coordinating soft-transitions with parent components.

Design

In order to make this feature work, we need to define:

Mutable-by-parent configuration fields

Per requirement (2), parents should only be able to override configuration fields which are marked mutable by the child component.

Component authors will add a mutable_by attribute to any configuration fields which should be allowed to receive overrides. The attribute will accept a list of strings to accommodate future override mechanisms. Initially the only string accepted will be "parent". Example CML:

{
    // ...
    config: {
        fields: {
            enable_new_feature: {
                type: "bool",
                mutable_by: [ "parent" ],
            },
        },
    }
}

Mutability will be specified as a list of possible sources to make it easy to extend this syntax for new overrides sources, including a developer override database as originally scoped by RFC-0127.

A rejected alternative we could pursue in the future would be to group configuration fields by their mutability.

String keys vs. offsets/ordinals

Component Manager currently resolves packaged configuration values using the offset of the field in the compiled configuration schema, a small optimization which requires the component's manifest, configuration values, and compiled binary to agree on the exact layout of the configuration schema. This will not be possible for overrides where the provider of values and the configured component were built with different (but compatible) configuration schemas.

Parent overrides will instead be resolved by checking string equality of the keys in the child's schema and the provided overrides, allowing the order and number of fields in the child's schema to change without requiring the child component to specify explicit ordinals or the parent component to update its overrides.

A rejected alternative to this approach would be to use integer ordinals for resolution and to require child components to explicitly choose an ordinal.

Packaged defaults

Components with mutable-by-parent configuration fields will still be required to provide a default/base value in their packaged configuration. This will make the addition of new parent-visible fields a soft transition.

In the future we may allow components to require their parents to provide some configuration values.

No over-provisioning

Parent-provided configuration value files must only contain values for fields which are present and mutable-by-parent in the child component's configuration schema. Preventing over-provisioning of configuration will provide predictable behavior for parents without them having to manually verify that the child was configured as they expect.

Precedence of values from parents

Component configuration "tailors the behavior of a component instance to the context it's running in," which implies that the most authoritative configuration values should be those which encode the most knowledge of the component instance's context.

Per RFC-0127, Component Manager will resolve a value for each configuration field, preferring each source in this order:

  1. (in the future) values from a developer override service
  2. values from a component's parent
  3. values from the component's own package

A component developer working on an engineering build can have knowledge about everything running on the system, which makes those overrides the most authoritative. A parent can understand the context it provides to its children. Finally, because the values in the component's own package can only encode an understanding of how the component was packaged, all other information must be provided from an outside source.

Providing values in Realm.CreateChild

We will extend fuchsia.component.decl/Child to allow specifying configuration overrides at runtime:

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;
};

While it would be possible for parents to set configuration for dynamic children if we added this field to fuchsia.component.CreateChildArgs, that would not offer a path to specifying parent overrides for statically-defined components in CML. The proposed approach ensures there is only a single source of parent-provided values for Component Manager to consider in the future.

Parent components providing values at runtime must match the declared type of the configuration field exactly. No type inference, casting, or integer promotion will be performed.

Implementation

The proposed design has been prototyped.

The fuchsia.component.decl FIDL library will need to have access to a Value type which is currently in fuchsia.component.config. However, fuchsia.component.config currently depends on fuchsia.component.decl, so it will take a long time to invert the dependency relationship with soft transitions. Instead, we will move all of fuchsia.component.config into fuchsia.component.decl at the current API level, deprecating and eventually removing fuchsia.component.config. In merging the two libraries we will add a Config* prefix to the appropriate types, e.g. Value becomes ConfigValue.

Newly defined FIDL API surface for this proposal will only be available at API level HEAD until we've gained experience with the feature and out-of-tree users are able to make use of structured configuration.

In addition to changes to component declarations, RealmBuilder client libraries will need to be updated to allow passing configuration overrides as part of the Child decl.

Performance

This RFC proposes that Component Manager will use string equality to match overrides with their intended fields, which will be a slower operation than the O(1) index/offset comparison currently used to resolve packaged config values. This adds a very small amount of computation to component start but is unlikely to have any observable impact as component start times are typically on the order of hundreds of milliseconds today. For context, framework overhead in component start time has not yet been a significant input to any user-facing product quality issues.

Ergonomics

In the first iteration of this feature a parent component will need to know the exact type of the configuration field being overridden which is not as ergonomic as we can eventually make this feature. For example, a component author will need to match the exact integer width and signedness of a numeric configuration field. In the future we can define looser type resolution rules to support configuring static children, and we may also generate code from a child's configuration schema which would allow language compilers to check the field names and types on behalf of a developer.

Without the ability to describe a version sequence for a component's config interface, schema evolution will be a manual & social process. This might be possible to address in the future.

Some components may end up with multiple fields that share the same mutability constraints, with an ergonomic tax imposed by repeating the same attribute for each field. We do not have any use cases which fit this pattern today, so a syntax aimed at solving this problem is a rejected alternative that we may choose to revisit in the future.

Backwards & Forwards Compatibility

Authors of components will be responsible for coordinating soft-transitions with their parent components when they modify the mutable-by-parent portions of their config schema. This section describes safe evolution procedures for modifications to configuration schemas.

Configuration fields without a mutable_by attribute will not require any special attention to versioning, as values for those fields can only be provided from a component's own package.

In the future these steps may be more ergonomically mediated by FIDL-based versioning.

Adding a new mutable-by-parent field, adding mutability to existing fields

No special considerations are required, as all configuration fields will still require a base/default value to be in a component's own packaged value file. Once the field is present in a component's configuration schema, parents will be able to provide a value for the field.

Removing a mutable-by-parent configuration key, removing mutability modifier

Safely removing a mutable-by-parent configuration field from a component's schema will require first working with parent components to ensure they are no longer passing any overrides for the field which will be removed.

For example, to remove a parent_provided config field from a component's configuration interface:

  1. component author communicates intent to deprecate and remove parent_provided
  2. parent components stop specifying values for parent_provided
  3. component author removes parent_provided from their configuration schema

Renaming a mutable-by-parent field

Renaming a field is equivalent to a simultaneous addition & removal and should generally not be performed in a single step.

Changing a mutable-by-parent field's type

Changing a field's type is equivalent to a simultaneous addition & removal and should generally not be performed in a single step.

Changing a mutable-by-parent field's constraints

Increasing a field's max_len or max_size constraint is always safe.

Reducing a field's max_len or max_size constraint is only safe if all parent-provided values are within the new range.

Security considerations

The scrutiny tool can make assertions about the final configuration of components in a built system image. This is an important safeguard mechanism to protect against accidental misconfigurations during the build and assembly processes.

We will extend scrutiny so that it will refuse configuration fields which both have policy-enforced values and are mutable-by-parent. This will ensure that security-critical configuration fields can never be mutated by a parent component outside the scope of scrutiny's static verification.

Privacy considerations

Parent overrides allow components to pass runtime data as configuration values. While some components may choose to use this functionality to pass user data, there is currently no functionality which automatically records structured configuration values in logs, metrics, or telemetry. There should be no privacy impact to implementing this feature.

Testing

The config_encoder library is used to resolve configuration in Component Manager, scrutiny, and related tools. Its unit tests will be expanded to ensure that incorrectly provisioned parent overrides (unknown key, wrong type, missing mutability) will be rejected.

Structured configuration integration tests will be expanded to ensure that a parent component can provide configuration using the Realm protocol and using RealmBuilder.

scrutiny tests will be expanded to ensure it rejects mutable-by-parent fields which have statically declared values in a policy file.

Documentation

CML reference documentation will be updated to match the updated config schema syntax.

A guide on structured config schema evolution will be written. It will cover best practices for adding and removing fields. It will include guidance for managing soft transitions.

New reference documentation for structured configuration value sources and their precedence will be written.

Existing documentation on verifying security properties of a built image will be extended with an explanation for why parent-mutable fields are not allowed for security-relevant configuration.

Future Work

Generated bindings for overrides

The above proposal implies that authors of parent components will need to use string keys and "dynamically typed" values for providing overrides. This will result in some boilerplate and makes it possible for a parent to provide the wrong keys/values or to forget to update a codepath which produces overrides when there's a new field available. Errors due to incorrectly provisioned configuration values will only appear at runtime when starting the child component.

We can eventually improve the developer experience here by allowing authors of parent-configured components to generate "parent override" libraries. These could be used by authors of parent components to reduce characters typed and have their compiler check that they're correctly overriding the child's configuration:

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();

Configuring children in CML

A parent component with static children should eventually be able to provide configuration values when declaring the child in CML. For example:

{
    children: [
        {
            name: "...",
            url: "...",
            config: {
                parent_provided: "foo",
            },
        },
    ],
}

We may want to also provide syntax that lets a parent forward their own configuration value(s) directly to the static child.

We will need to decide whether scrutiny will permit mutable-by-parent fields when the provided configuration value is statically known.

If we build this feature for CML, passing literal values will require design work to bridge the gap between JSON5's loose typing for numbers and structured config's precise types. We will need to choose fuchsia.component.decl.ConfigValue representations for all possible JSON5 inputs, and we will need to define rules which allow Component Manager to make use of those values for configuration fields which might have narrower constraints. Much of this design work can be avoided if we wait for a FIDL-based representation of parent/child relationships, as fidlc would be able to precisely check types against a child's configuration schema. This design work would not be needed to allow a parent to pass its own config values to a child.

Required-from-parent configuration values

Some components may want to require that their parent provides a particular configuration value without being able to rely on a packaged default.

This would require allowing packaged value files to omit values and teaching the various libraries which resolve configuration how to handle them.

This is not required to meet current use cases but may be a useful extension to pursue in the future.

FIDL-based schemas & versioning

The Component Framework team has explored the use of FIDL for component manifests. If that is implemented we may be able to use FIDL availability annotations to coordinate configuration schema evolution.

Use string keys to resolve packaged configuration

Taking the approach proposed in this RFC will leave Component Manager with two identifiers used for resolving configuration values:

  1. packaged values will be resolved using integer offsets within the compiled config schema
  2. parent-provided values will be resolved using string keys from the config schema

As discussed in the alternative below, there are good reasons to prefer string keys for parent overrides, and the potential motivations for using integer offsets/ordinals don't benefit us much when resolving packaged values.

We should move packaged values to encoding string keys to reduce fragmentation in Component Manager's semantics. This change should be largely invisible to downstream users but will simplify the implementation of config resolution and make future debugging easier.

Fuzz config resolution

In the future we will add a fuzzer for Component Manager's manifest parsing and component resolution. When that happens, we will extend the fuzzer to include configuration values from a parent component and assert that mutability modifiers are respected.

Drawbacks, alternatives, and unknowns

Grouping fields with shared mutability constraints

If a component has many fields which all share the same mutability constraints, we might consider allowing component authors to group configuration fields together with shared attributes. For a (non-normative) example, we might define multiple config sections:

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

This direction could be useful for ergonomics but would not be motivated by any existing use cases we've identified. We can revisit this approach if we discover verbosity is a significant tax in practice.

Note that this approach could make it harder for component authors to define configuration fields which have multiple mutability specifiers, e.g. "mutable by parent and by override service".

Integer ordinals for override API

For historical reasons packaged configuration values are resolved to their fields by using their offset within the list of packaged values. This offers a more efficient encoding and less runtime overhead than checking for string equality.

To achieve the same benefits for parent overrides, we would need authors of child components to explicitly choose an ordinal for their fields, similar to a FIDL table. Parent component authors would need to specify their overridden values in terms of these ordinals or use generated libraries for human-legible names.

We would also need to design mechanisms to guide child component authors away from re-using integer ordinals, whereas well-chosen string keys should be at less risk of bug-prone reuse.

Ultimately, the binary size and runtime overhead benefits we get from resolving keys from integers is negligible at the scale of usage we anticipate for structured configuration. Using string keys lets us defer generated bindings and insulates child component authors from the complexity of managing their ordinals, with minimal cost to the system's performance.