RFC-0215:結構化設定父項覆寫

RFC-0215:結構化設定父項覆寫
狀態已接受
區域
  • 元件架構
說明

允許父項元件在執行階段為子項提供設定。

問題
Gerrit 變更
作者
審查人員
提交日期 (年-月-日)2023-03-21
審查日期 (年-月-日)2023-04-26

摘要

允許父項元件覆寫子項元件結構化設定中的值。

提振精神

根據 RFC-0127 的提案方向,父項元件應能為子項提供設定值。舉例來說,starnix_runner 就能直接將自己的設定值傳遞至啟動的 starnix_kernel,而不必建立設定目錄和動態供應項目。

啟動動態子項時,父項元件應能傳遞設定值。舉例來說,以 Rust 實作的父項應能編寫如下程式碼:

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

未來,父項元件應可在 CML 中設定靜態子項,並使用產生的程式庫處理動態子項,以減少冗餘,同時確保覆寫的型別正確無誤。

利害關係人

講師:jamesr@google.com

審查者:

  • geb@google.com (CF)
  • ypomortsev@google.com (CF)
  • markdittmer@google.com (安全性)

諮詢對象:lindkvist@google.com、hjfreyer@google.com

社交化:這項提案在提交為 RFC 前,已在元件架構團隊成員和潛在客戶間流通。

需求條件

  1. 一開始,家長可能會為動態子項提供設定值,包括使用 RealmBuilder 例項化的子項。最終應可在 CML 中設定靜態子項,包括使用父項設定中的值。
  2. 元件作者可以演進內部/僅供測試/僅供開發人員使用的設定欄位,不必與上層元件協調。
  3. 父項和子項元件可以獨立更新。子項可與父項元件協調軟轉換,進而演變設定結構定義。

設計

如要使用這項功能,我們需要定義:

可由上層修改的設定欄位

根據規定 (2),家長只能覆寫子項元件標示為可變動的設定欄位。

元件作者會將 mutable_by 屬性新增至任何應允許接收覆寫的設定欄位。為配合日後的覆寫機制,這項屬性會接受字串清單。一開始,系統只會接受 "parent" 字串。CML 範例:

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

可變動性會指定為可能來源的清單,方便您為新的覆寫來源擴充這個語法,包括RFC-0127 原先範圍內的開發人員覆寫資料庫。

日後可考慮的替代方案是依可變性將設定欄位分組

字串鍵與偏移/序數

元件管理服務目前會使用已編譯設定結構定義中欄位的偏移量,解析封裝的設定值,這項小幅最佳化作業需要元件的資訊清單、設定值和已編譯的二進位檔,才能就設定結構定義的確切版面配置達成一致。如果值提供者和設定的元件是使用不同 (但相容) 的設定結構定義建構,就無法覆寫。

系統會改為檢查子項結構定義和提供的覆寫項目中鍵的字串相等性,解決父項覆寫項目問題,因此子項結構定義中的欄位順序和數量可以變更,不必由子項元件指定明確的序數,也不必由父項元件更新覆寫項目。

這種做法的遭拒替代方案是使用整數序數進行解析,並要求子元件明確選擇序數。

預設套件

如果元件的設定欄位可由父項變更,仍須在封裝的設定中提供預設/基本值。這樣一來,新增家長可見欄位時,就能順利完成轉換。

未來,我們可能會允許元件要求父項提供某些設定值。

不會過度佈建

父項提供的設定值檔案只能包含子項元件設定結構定義中,存在且可由父項變更的欄位值。防止過度佈建設定,可為家長提供可預測的行為,不必手動確認子女的設定是否符合預期。

父項值優先順序

元件設定「會根據元件執行個體執行的環境調整其行為」,這表示最權威的設定值應是編碼元件執行個體環境最多知識的值。

根據 RFC-0127,元件管理服務會為每個設定欄位解析值,並依下列順序優先採用每個來源:

  1. (日後) 來自開發人員覆寫服務的值
  2. 來自元件父項的值
  3. 元件本身套件中的值

負責工程建構作業的元件開發人員可以瞭解系統上執行的所有項目,因此這些覆寫項目最為權威。家長可以瞭解孩子在該情境中獲得的資訊。最後,由於元件本身套件中的值只能編碼,瞭解元件的封裝方式,因此所有其他資訊都必須來自外部來源。

Realm.CreateChild 中提供值

我們會擴充 fuchsia.component.decl/Child,允許在執行階段指定設定覆寫:

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

如果我們將這個欄位新增至 fuchsia.component.CreateChildArgs,雖然父項可以為動態子項設定設定,但這無法提供為 CML 中靜態定義的元件指定父項覆寫項的路徑。建議做法可確保元件管理服務在日後只會考量單一來源的父項提供值。

在執行階段提供值的父項元件,必須與設定欄位的宣告型別完全相符。系統不會執行型別推斷、轉換或整數升級。

實作

提案設計已製作出原型

fuchsia.component.decl FIDL 程式庫必須存取目前位於 fuchsia.component.config 中的Value型別。不過,fuchsia.component.config 目前依附於 fuchsia.component.decl,因此要透過軟性轉場效果反轉依附元件關係,需要很長的時間。我們會將所有 fuchsia.component.config 移至目前 API 級別的 fuchsia.component.decl,並淘汰及最終移除 fuchsia.component.config。合併這兩個程式庫時,我們會為適當的型別新增 Config* 前置字串,例如 Value 會變成 ConfigValue

提案中新定義的 FIDL API 介面只會在 API 級別 HEAD 提供,直到我們對這項功能有經驗,且樹狀結構外的使用者能夠使用結構化設定為止。

除了變更元件宣告外,也需要更新 RealmBuilder 用戶端程式庫,才能將設定覆寫項目做為 Child 宣告的一部分傳遞。

效能

這項 RFC 提案指出,元件管理服務會使用字串相等性,將覆寫項目與預期欄位比對,這項作業比目前用於解析封裝設定值的 O(1) 索引/偏移比較慢。這會在元件啟動時增加極少量的運算,但不太可能造成任何可觀察到的影響,因為目前元件啟動時間通常為數百毫秒。就背景而言,元件啟動時間的架構負荷尚未成為任何使用者面臨的產品品質問題的重要輸入內容。

人體工學

在本功能的第一個疊代版本中,父項元件必須知道要覆寫的設定欄位確切類型,這並非我們最終可讓這項功能達成的目標。舉例來說,元件作者必須完全符合數字設定欄位的整數寬度和正負號。日後我們可定義較寬鬆的型別解析規則,支援設定靜態子項,我們也可能從子項的設定結構定義產生程式碼,讓語言編譯器代表開發人員檢查欄位名稱和型別。

如果無法描述元件設定介面的版本序列,結構定義演進就會成為手動和社交程序。這項問題日後可能可以解決

部分元件可能會有多個欄位共用相同的可變動性限制,而為每個欄位重複相同屬性會造成人體工學上的負擔。我們目前沒有符合這種模式的用途,因此針對解決這個問題的語法是遭拒的替代方案,我們可能會在日後重新考慮。

回溯與前向相容性

如果元件作者修改設定結構定義中可由父項修改的部分,就必須負責與父項元件協調軟轉換。本節說明修改設定結構定義時的安全演進程序。

沒有 mutable_by 屬性的設定欄位不需要特別注意版本控管,因為這些欄位的值只能從元件本身的套件提供。

日後,這些步驟可能會透過 FIDL 型版本控管,以更符合人體工學的方式進行調解。

新增可由父項變更的欄位,為現有欄位新增可變更性

不需要特別考量,因為所有設定欄位仍須在元件本身的封裝值檔案中,提供基本/預設值。只要欄位出現在元件的設定結構定義中,父項就能為該欄位提供值。

移除可由父項修改的設定鍵,移除可變動性修飾符

如要從元件的結構定義中安全地移除可由父項變更的設定欄位,請先與父項元件合作,確保這些元件不再傳遞要移除欄位的任何覆寫項目。

舉例來說,如要從元件的設定介面移除 parent_provided 設定欄位:

  1. 元件作者傳達要淘汰及移除的意圖 parent_provided
  2. 父項元件停止指定 parent_provided 的值
  3. 元件作者從設定結構定義中移除 parent_provided

重新命名可由父項修改的欄位

重新命名欄位等同於同時新增及移除欄位,一般不應在單一步驟中執行。

變更可由父項變更的欄位類型

變更欄位類型等同於同時新增及移除,一般不應在單一步驟中執行。

變更可由上層項目變更的欄位限制

增加欄位的 max_lenmax_size 限制一律安全。

只有在所有上層提供的值都位於新範圍內時,縮減欄位的 max_lenmax_size 限制才安全。

安全性考量

scrutiny 工具可以進行斷言,判斷建構系統映像檔中元件的最終設定。這是重要的安全防護機制,可避免在建構和組裝過程中發生設定錯誤。

我們會擴充 scrutiny,拒絕同時具有政策強制值和可由父項變更的設定欄位。這可確保安全關鍵設定欄位絕不會遭到父項元件變動,超出審查的靜態驗證範圍。

隱私權注意事項

父項覆寫可讓元件將執行階段資料做為設定值傳遞。雖然部分元件可能會選擇使用這項功能傳遞使用者資料,但目前沒有任何功能會自動記錄記錄檔、指標或遙測中的結構化設定值。實作這項功能應不會對隱私權造成影響。

測試

config_encoder 程式庫用於解析 ComponentManager、scrutiny 和相關工具中的設定。擴充單元測試,確保系統會拒絕錯誤佈建的父項覆寫 (不明金鑰、類型錯誤、缺少可變動性)。

結構化設定整合測試將會擴大,確保父項元件可以使用 Realm 通訊協定和 RealmBuilder 提供設定。

scrutiny 測試範圍將會擴大,確保系統會拒絕政策檔案中具有靜態宣告值的可由父項變更欄位。

說明文件

CML 參考說明文件會更新,以符合更新後的設定結構定義語法。

我們將撰寫結構化設定結構定義演進指南。內容包括新增和移除欄位的最佳做法。內容包括管理軟性轉移的指引。

我們將編寫結構化設定值來源的新參考文件,並說明這些來源的優先順序。

現有的建構映像檔安全性屬性驗證文件將會擴充,說明為何安全性相關設定不允許使用父項可變動欄位。

後續作業

為覆寫產生的繫結

上述提案表示父項元件的作者必須使用字串鍵和「動態型別」值來提供覆寫。這會導致一些樣板,並讓父項提供錯誤的鍵/值,或忘記在有新欄位可用時更新產生覆寫的程式碼路徑。如果設定值佈建錯誤,只有在啟動子元件時,才會在執行階段顯示錯誤。

我們最終可以允許父項設定元件的作者產生「父項覆寫」程式庫,進而改善此處的開發人員體驗。父項元件的作者可以使用這些項目,減少輸入的字元,並讓編譯器檢查是否正確覆寫子項的設定:

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

在 CML 中設定子項

具有靜態子項的父項元件最終應能在 CML 中宣告子項時提供設定值。例如:

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

我們可能也想提供語法,讓父項直接將自己的設定值轉送至靜態子項。

我們需要決定在靜態已知提供的設定值時,scrutiny 是否允許父項可變動的欄位。

如果我們為 CML 建構這項功能,傳遞字面值時,需要設計工作來彌合 JSON5 寬鬆的數字型別與結構化設定精確型別之間的差距。我們需要為所有可能的 JSON5 輸入內容選擇fuchsia.component.decl.ConfigValue表示法,並定義規則,讓元件管理服務能夠針對可能具有較窄限制的設定欄位使用這些值。如果等待以 FIDL 為基礎的父項/子項關係表示法,即可避免許多設計工作,因為 fidlc 能夠根據子項的設定結構定義精確檢查型別。如果允許父項將自己的設定值傳遞給子項,就不需要這項設計工作。

從父項取得的必要設定值

部分元件可能需要父項提供特定設定值,但無法依賴封裝的預設值。

這需要允許封裝值檔案省略值,並教導解析設定的各種程式庫如何處理這些值。

目前的使用案例不需要這項功能,但日後可能會有用。

以 FIDL 為基礎的結構定義和版本管理

元件架構團隊已探索如何將 FIDL 用於元件資訊清單。如果實作了這項功能,我們或許就能使用 FIDL 可用性註解,協調設定結構定義的演進。

使用字串鍵解析封裝設定

採用本 RFC 建議的方法後,元件管理服務會留下兩個用於解析設定值的 ID:

  1. 封裝值會使用已編譯設定結構定義中的整數偏移量解析
  2. 系統會使用設定結構定義中的字串鍵,解析父項提供的值

下文所述,偏好使用字串鍵進行父項覆寫有充分的理由,而使用整數偏移/序數的潛在動機,在解析封裝值時對我們沒有太大幫助。

我們應將封裝值移至編碼字串鍵,以減少元件管理服務工具語意中的片段。這項變更對下游使用者來說應該幾乎沒有影響,但可簡化設定解析度的實作方式,並方便日後進行偵錯。

模糊設定解析

我們日後會為元件管理服務的資訊清單剖析和元件解析新增模糊測試器。發生這種情況時,我們會擴充模糊測試器,納入父項元件的設定值,並確認系統會遵守可變動性修飾符。

缺點、替代方案和未知事項

將具有相同可變動限制的欄位分組

如果元件有多個欄位共用相同的可變動性限制,我們可能會考慮允許元件作者將設定欄位與共用屬性分組。舉例來說,我們可能會定義多個設定區段 (非規範性):

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

這個方向可能對人體工學有幫助,但我們目前尚未發現任何相關的用例。如果我們發現詳細程度實際上會造成重大負擔,可以重新考慮這個做法。

請注意,如果設定欄位有多個可變動性指定符 (例如「可由父項和覆寫服務變動」),這種做法可能會讓元件作者更難定義設定欄位。

Override API 的整數序數

基於歷史因素,封裝設定值會透過在封裝值清單中的偏移量,解析為其欄位。相較於檢查字串是否相等,這種做法的編碼效率更高,且執行階段負擔較小。

如要為父項覆寫項目取得相同優勢,子項元件的作者必須明確選擇欄位的序數,類似於 FIDL 表格。父項元件的作者必須以這些序數指定覆寫的值,或使用產生的程式庫,取得可供人解讀的名稱。

我們也需要設計機制,引導子項元件作者避免重複使用整數序數,而精心挑選的字串鍵則較不容易重複使用,因此較不容易發生錯誤。

最終,以整數解析鍵所帶來的二進位大小和執行階段負荷效益,在我們預期結構化設定的使用規模下微不足道。使用字串鍵可延後產生的繫結,並讓子項元件作者免於管理序數的複雜性,對系統效能的影響極小。