mirror of
https://github.com/esphome/esphome.git
synced 2026-09-25 22:10:21 +00:00
[store_yaml] Refactor YAML discovery to share bundle.py's approach
The previous implementation extended `track_yaml_loads` across the entire validation pass to catch deferred `!include` and package loads, but that also captured framework YAML loaded internally by component validators (e.g. LVGL's `hello_world.yaml`) and produced spurious "unresolved substitution" warnings during the pre-validation force-load. bundle.py already had the right pattern: a fresh post-validation re-parse plus `force_load_include_files`. - Lift the discovery into `yaml_util.discover_user_yaml_files` and have both `bundle.py` and `config.py` use it (DRY). - Capture `secrets.yaml` / `secrets.yml` by the *un-resolved* listener fname so a `secrets.yaml` symlinked to a non-secrets-named target is still flagged for redaction. - Add a `warn_on_unresolved` flag to `force_load_include_files` so the discovery path (where substitutions haven't run) logs at debug instead of warning. - Reject `store_yaml` configs with an unencrypted API via `FINAL_VALIDATE_SCHEMA`; an explicit `allow_unencrypted: true` opt-out keeps lab setups working (renamed from `allow_unencrypted_api` so the integration-test harness's naive `api:` string replacement doesn't clobber it). - Annotate `store_yaml_chunk_buf` with the `cppcoreguidelines-avoid-non-const-global-variables` suppression that matches other intentional API-side globals. - Update unit tests to exercise the new `DiscoveredYamlFiles` shape plus a symlink-secrets case.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
api:
|
||||
|
||||
store_yaml:
|
||||
allow_unencrypted: true
|
||||
|
||||
@@ -11,3 +11,4 @@ logger:
|
||||
api:
|
||||
|
||||
store_yaml:
|
||||
allow_unencrypted: true
|
||||
|
||||
@@ -14,6 +14,7 @@ from esphome.components.store_yaml import (
|
||||
_pack_envelope,
|
||||
)
|
||||
from esphome.core import CORE, EsphomeError
|
||||
from esphome.yaml_util import DiscoveredYamlFiles
|
||||
|
||||
|
||||
def _unpack_envelope(blob: bytes) -> dict[str, bytes]:
|
||||
@@ -57,13 +58,21 @@ def _reset_core() -> None:
|
||||
CORE.config_path = None
|
||||
|
||||
|
||||
def _set_sources(project_dir: Path, *names: str) -> None:
|
||||
def _set_sources(project_dir: Path, *names: str, secrets: tuple[str, ...] = ()) -> None:
|
||||
CORE.config_path = project_dir / "entry.yaml"
|
||||
CORE.data["yaml_sources"] = [project_dir / name for name in names]
|
||||
files = [project_dir / name for name in names]
|
||||
secret_paths = {(project_dir / name).resolve() for name in secrets}
|
||||
CORE.data["yaml_sources"] = DiscoveredYamlFiles(files, secret_paths)
|
||||
|
||||
|
||||
def test_gather_redacts_secrets_by_default(project: Path) -> None:
|
||||
_set_sources(project, "entry.yaml", "wifi.yaml", "secrets.yaml")
|
||||
_set_sources(
|
||||
project,
|
||||
"entry.yaml",
|
||||
"wifi.yaml",
|
||||
"secrets.yaml",
|
||||
secrets=("secrets.yaml",),
|
||||
)
|
||||
files = dict(_gather_files(include_secrets=False))
|
||||
assert files["secrets.yaml"] == REDACTED_PLACEHOLDER
|
||||
assert b"SUPER_SECRET" not in files["secrets.yaml"]
|
||||
@@ -73,13 +82,33 @@ def test_gather_redacts_secrets_by_default(project: Path) -> None:
|
||||
def test_gather_redacts_yml_extension(project: Path) -> None:
|
||||
yml = project / "secrets.yml"
|
||||
yml.write_text("api_key: OTHER_SECRET\n")
|
||||
_set_sources(project, "entry.yaml", "secrets.yml")
|
||||
_set_sources(project, "entry.yaml", "secrets.yml", secrets=("secrets.yml",))
|
||||
files = dict(_gather_files(include_secrets=False))
|
||||
assert files["secrets.yml"] == REDACTED_PLACEHOLDER
|
||||
|
||||
|
||||
def test_gather_redacts_secret_symlinked_to_other_name(
|
||||
project: Path, tmp_path: Path
|
||||
) -> None:
|
||||
"""A `secrets.yaml` symlinked to a non-secrets-named target is still redacted
|
||||
because the un-resolved basename was captured upstream."""
|
||||
target = tmp_path / "actual_creds.yaml"
|
||||
target.write_text("api_key: FROM_SYMLINK\n")
|
||||
link = project / "secrets.yaml"
|
||||
link.unlink() # remove the regular file laid down by the fixture
|
||||
link.symlink_to(target)
|
||||
# Discovery records the un-resolved listener fname under SECRETS_FILES
|
||||
# but stores the resolved path; mimic that here.
|
||||
resolved = link.resolve()
|
||||
CORE.config_path = project / "entry.yaml"
|
||||
CORE.data["yaml_sources"] = DiscoveredYamlFiles([resolved], {resolved})
|
||||
files = dict(_gather_files(include_secrets=False))
|
||||
assert REDACTED_PLACEHOLDER in files.values()
|
||||
assert b"FROM_SYMLINK" not in b"".join(files.values())
|
||||
|
||||
|
||||
def test_gather_embeds_secrets_when_opted_in(project: Path) -> None:
|
||||
_set_sources(project, "entry.yaml", "secrets.yaml")
|
||||
_set_sources(project, "entry.yaml", "secrets.yaml", secrets=("secrets.yaml",))
|
||||
files = dict(_gather_files(include_secrets=True))
|
||||
assert b"SUPER_SECRET" in files["secrets.yaml"]
|
||||
|
||||
@@ -90,21 +119,16 @@ def test_gather_uses_relative_path_for_external_files(
|
||||
"""Files outside the project root use a ``..``-style relative path so they don't collide."""
|
||||
sibling = tmp_path / "outside.yaml"
|
||||
sibling.write_text("foo: bar\n")
|
||||
_set_sources(project, "entry.yaml")
|
||||
CORE.data["yaml_sources"].append(sibling)
|
||||
CORE.config_path = project / "entry.yaml"
|
||||
CORE.data["yaml_sources"] = DiscoveredYamlFiles(
|
||||
[project / "entry.yaml", sibling], set()
|
||||
)
|
||||
files = dict(_gather_files(include_secrets=False))
|
||||
# project root is `tmp_path/project`, sibling is in `tmp_path` so it
|
||||
# resolves to `../outside.yaml`.
|
||||
assert "../outside.yaml" in files
|
||||
|
||||
|
||||
def test_gather_deduplicates(project: Path) -> None:
|
||||
_set_sources(project, "entry.yaml", "wifi.yaml", "wifi.yaml")
|
||||
files = _gather_files(include_secrets=False)
|
||||
paths = [p for p, _ in files]
|
||||
assert paths.count("wifi.yaml") == 1
|
||||
|
||||
|
||||
def test_gather_raises_when_no_sources(project: Path) -> None:
|
||||
CORE.config_path = project / "entry.yaml"
|
||||
with pytest.raises(EsphomeError):
|
||||
|
||||
Reference in New Issue
Block a user