mirror of
https://github.com/esphome/esphome.git
synced 2026-09-23 04:58:38 +00:00
[store_yaml] Record uncaptured includes as an envelope note in both modes
This commit is contained in:
@@ -197,6 +197,22 @@ def _collect_sensitive_values() -> dict[str, _SensitiveValue]:
|
||||
return result
|
||||
|
||||
|
||||
# Envelope path of the note recording includes that could not be captured.
|
||||
UNCAPTURED_NOTE_PATH = "store_yaml_uncaptured.yaml"
|
||||
|
||||
|
||||
def _uncaptured_note(unresolved: list[str]) -> tuple[str, bytes]:
|
||||
"""Comment-only YAML entry listing includes that could not be captured, so
|
||||
a recovered config never silently appears complete. Emitted for both the
|
||||
redacted and verbatim paths; user files are never modified to carry it."""
|
||||
text = (
|
||||
"# store_yaml: the following !include paths use substitutions and\n"
|
||||
"# could not be captured; restore these files manually:\n"
|
||||
+ "".join(f"# {inc}\n" for inc in unresolved)
|
||||
)
|
||||
return (UNCAPTURED_NOTE_PATH, text.encode("utf-8"))
|
||||
|
||||
|
||||
def _build_secrets_skeleton(keys: set[str]) -> bytes:
|
||||
parts = [SECRETS_SKELETON_HEADER]
|
||||
parts.extend(f'{key}: ""\n' for key in sorted(keys))
|
||||
@@ -204,7 +220,7 @@ def _build_secrets_skeleton(keys: set[str]) -> bytes:
|
||||
|
||||
|
||||
def _generate_redacted_files(
|
||||
entries: list[tuple[str, Path]], secret_rels: set[str], unresolved: list[str]
|
||||
entries: list[tuple[str, Path]], secret_rels: set[str]
|
||||
) -> list[tuple[str, bytes]]:
|
||||
"""Re-generate each captured file from its parse tree with cv.sensitive
|
||||
values emitted as `!secret <name>` references, and replace secrets files
|
||||
@@ -248,19 +264,6 @@ def _generate_redacted_files(
|
||||
"embed secrets deliberately."
|
||||
)
|
||||
|
||||
if unresolved:
|
||||
# Record the gap inside the recovered config itself, not just in a
|
||||
# compile-time log line: substitution-pathed includes can't be
|
||||
# captured, so the user must restore those files manually.
|
||||
# Discovery parses the entry file first, so entries[0] is the entry.
|
||||
entry_rel = entries[0][0]
|
||||
texts[entry_rel] = (
|
||||
"# store_yaml: the following !include paths use substitutions and\n"
|
||||
"# could not be captured; restore these files manually:\n"
|
||||
+ "".join(f"# {inc}\n" for inc in unresolved)
|
||||
+ texts[entry_rel]
|
||||
)
|
||||
|
||||
skeleton = _build_secrets_skeleton(skeleton_keys)
|
||||
result = [
|
||||
(rel, skeleton if rel in secret_rels else texts[rel].encode("utf-8"))
|
||||
@@ -337,7 +340,9 @@ async def to_code(config: ConfigType) -> None:
|
||||
if config[CONF_INCLUDE_SECRETS]:
|
||||
files = _read_files_verbatim(entries)
|
||||
else:
|
||||
files = _generate_redacted_files(entries, secret_rels, discovered.unresolved)
|
||||
files = _generate_redacted_files(entries, secret_rels)
|
||||
if discovered.unresolved:
|
||||
files.append(_uncaptured_note(discovered.unresolved))
|
||||
envelope = _pack_envelope(files)
|
||||
compressed = zstd.compress(envelope, level=ZSTD_LEVEL)
|
||||
|
||||
|
||||
@@ -10,10 +10,12 @@ import pytest
|
||||
from esphome import yaml_util
|
||||
from esphome.components.store_yaml import (
|
||||
SECRETS_SKELETON_HEADER,
|
||||
UNCAPTURED_NOTE_PATH,
|
||||
_gather_files,
|
||||
_generate_redacted_files,
|
||||
_pack_envelope,
|
||||
_read_files_verbatim,
|
||||
_uncaptured_note,
|
||||
unpack_envelope,
|
||||
)
|
||||
from esphome.core import CORE, EsphomeError
|
||||
@@ -49,7 +51,7 @@ def _sources(
|
||||
|
||||
def _gather_redacted(discovered: DiscoveredYamlFiles) -> dict[str, bytes]:
|
||||
entries, secret_rels = _gather_files(discovered)
|
||||
return dict(_generate_redacted_files(entries, secret_rels, discovered.unresolved))
|
||||
return dict(_generate_redacted_files(entries, secret_rels))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -272,14 +274,13 @@ def test_redacted_accepts_secret_only_values(project: Path) -> None:
|
||||
assert 'api_key: ""' in files["secrets.yaml"].decode()
|
||||
|
||||
|
||||
def test_redacted_records_unresolved_includes_in_entry_file(project: Path) -> None:
|
||||
"""Substitution-pathed includes that can't be captured are noted inside the
|
||||
recovered entry file, not just in a compile-time log line."""
|
||||
discovered = _sources(project, "entry.yaml", "secrets.yaml")
|
||||
discovered.unresolved.append("${board}.yaml")
|
||||
files = _gather_redacted(discovered)
|
||||
text = files["entry.yaml"].decode()
|
||||
assert text.startswith("# store_yaml: the following !include paths")
|
||||
def test_uncaptured_note_lists_missing_includes() -> None:
|
||||
"""Substitution-pathed includes that can't be captured are recorded in a
|
||||
dedicated envelope entry (both modes), not just a compile-time log line."""
|
||||
rel, content = _uncaptured_note(["${board}.yaml"])
|
||||
assert rel == UNCAPTURED_NOTE_PATH
|
||||
text = content.decode()
|
||||
assert text.startswith("# store_yaml:")
|
||||
assert "# ${board}.yaml" in text
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user