mirror of
https://github.com/home-assistant/core.git
synced 2026-09-09 15:11:40 +01:00
Do not stage a pending HTTP config when there is no YAML to migrate (#177622)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
committed by
Bram Kragten
co-authored by
Claude Fable 5
parent
5c1e921bf9
commit
15fdf3ba75
@@ -233,14 +233,17 @@ async def async_load_config(hass: HomeAssistant, config: ConfigType) -> ConfData
|
||||
ir.async_delete_issue(hass, DOMAIN, "deprecated_yaml_import_error")
|
||||
ir.async_delete_issue(hass, DOMAIN, "deprecated_yaml")
|
||||
ir.async_delete_issue(hass, DOMAIN, "yaml_still_present_after_migration")
|
||||
elif yaml_conf is None:
|
||||
# No YAML config to migrate: the stored config is already the source
|
||||
# of truth. Synthesizing a default config here would stage it as a
|
||||
# pending trial whenever the built-in default differs from stable
|
||||
# (e.g. after the Supervisor default port changed), restarting Home
|
||||
# Assistant to revert the never-promoted trial five minutes later.
|
||||
await store.async_mark_yaml_migration_done()
|
||||
else:
|
||||
# Migrate YAML to storage and use it directly for this start. The
|
||||
# migration function also marks the migration as done so future
|
||||
# starts will ignore any remaining YAML.
|
||||
conf_in_yaml = yaml_conf is not None
|
||||
if yaml_conf is None:
|
||||
yaml_conf = cast(ConfData, HTTP_STORAGE_SCHEMA({}))
|
||||
|
||||
try:
|
||||
await store.async_migrate_yaml(yaml_conf)
|
||||
except Exception:
|
||||
@@ -256,16 +259,15 @@ async def async_load_config(hass: HomeAssistant, config: ConfigType) -> ConfData
|
||||
)
|
||||
else:
|
||||
conf = await store.async_activate_config()
|
||||
if conf_in_yaml:
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"deprecated_yaml",
|
||||
breaks_in_ha_version="2027.2.0",
|
||||
is_fixable=False,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key="deprecated_yaml",
|
||||
)
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"deprecated_yaml",
|
||||
breaks_in_ha_version="2027.2.0",
|
||||
is_fixable=False,
|
||||
severity=ir.IssueSeverity.WARNING,
|
||||
translation_key="deprecated_yaml",
|
||||
)
|
||||
|
||||
if store.active_config_type is ActiveConfigType.PENDING:
|
||||
_LOGGER.info("Using pending HTTP config")
|
||||
@@ -462,6 +464,16 @@ class HTTPConfigStore:
|
||||
|
||||
await self._hass.services.async_call(HASS_DOMAIN, SERVICE_HOMEASSISTANT_RESTART)
|
||||
|
||||
async def async_mark_yaml_migration_done(self) -> None:
|
||||
"""Mark the YAML migration as done without migrating anything.
|
||||
|
||||
Used when there is no YAML config to migrate; the stored config
|
||||
stays the source of truth.
|
||||
"""
|
||||
await self.async_load()
|
||||
self._yaml_migration_done = True
|
||||
await self._async_persist()
|
||||
|
||||
async def async_migrate_yaml(self, config: ConfData) -> None:
|
||||
"""Migrate YAML config to storage as pending if not the same as the config used for recovery."""
|
||||
await self.async_load()
|
||||
|
||||
@@ -1518,10 +1518,10 @@ async def test_setup_migrates_v1_storage_to_v2(
|
||||
await hass.async_start()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# The migrated v1 store config is only used in recovery mode. Since this
|
||||
# test isn't running in recovery mode, the YAML migration runs on first
|
||||
# boot after store migration. With no YAML http config, the default config is migrated to the pending slot and used. Therefore we assert below the default port (8123)
|
||||
assert hass.config.api.port == 8123
|
||||
# With no YAML http config there is nothing to migrate: the stable slot
|
||||
# from the v1 store stays the source of truth and no pending trial is
|
||||
# staged.
|
||||
assert hass.config.api.port == 9876
|
||||
assert hass_storage[DOMAIN]["version"] == 2
|
||||
assert hass_storage[DOMAIN]["minor_version"] == 2
|
||||
data = hass_storage[DOMAIN]["data"]
|
||||
@@ -1530,12 +1530,60 @@ async def test_setup_migrates_v1_storage_to_v2(
|
||||
assert data["stable"] == _stored_config(
|
||||
{"server_port": 9876}, created_at=dt_util.utcnow().isoformat()
|
||||
)
|
||||
assert data["pending"] == _stored_config(
|
||||
{}, created_at=dt_util.utcnow().isoformat()
|
||||
)
|
||||
assert data["pending"] is None
|
||||
assert data["yaml_migration_done"] is True
|
||||
|
||||
|
||||
async def test_upgrade_with_stored_old_default_config_keeps_port(
|
||||
hass: HomeAssistant,
|
||||
hass_storage: dict[str, Any],
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""A stored old default config keeps its port under the new Supervisor default.
|
||||
|
||||
Releases up to 2026.7 persisted the fully expanded config — including the
|
||||
explicit default server_port 8123 — to the v1 store on every boot. When
|
||||
such an install is upgraded under Supervisor (where the default port is
|
||||
now 80), the new default must not be staged as a pending trial: nothing
|
||||
promotes it, so the trial would auto-revert with a restart five minutes
|
||||
later and flip the port back to 8123.
|
||||
"""
|
||||
hass_storage[DOMAIN] = {
|
||||
"version": 1,
|
||||
"key": DOMAIN,
|
||||
"data": {
|
||||
"server_port": 8123,
|
||||
"cors_allowed_origins": ["https://cast.home-assistant.io"],
|
||||
"use_x_frame_options": True,
|
||||
"ip_ban_enabled": True,
|
||||
"login_attempts_threshold": -1,
|
||||
"ssl_profile": "modern",
|
||||
},
|
||||
}
|
||||
restart_calls = async_mock_service(hass, "homeassistant", "restart")
|
||||
|
||||
with _supervisor_default_config():
|
||||
await _setup_http_with_onboarding(hass)
|
||||
|
||||
assert hass.config.api.port == 8123
|
||||
store = await async_get_and_load_store(hass)
|
||||
assert store.active_config_type is ActiveConfigType.STABLE
|
||||
assert store.revert_deadline is None
|
||||
data = hass_storage[DOMAIN]["data"]
|
||||
assert data["pending"] is None
|
||||
assert data["stable"]["server_port"] == 8123
|
||||
assert data["yaml_migration_done"] is True
|
||||
# The stored config differs from the new default config, so the
|
||||
# legacy-port transition does not apply.
|
||||
assert hass.http._legacy_redirect_server is None
|
||||
|
||||
# Nothing was staged for trial: no auto-revert restart fires.
|
||||
freezer.tick(AUTO_REVERT_DELAY)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
assert len(restart_calls) == 0
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("freezer")
|
||||
async def test_setup_migrates_v2_1_storage_to_v2_2(
|
||||
hass: HomeAssistant,
|
||||
@@ -1615,23 +1663,30 @@ def test_default_server_port(
|
||||
assert default_server_port() == expected_port
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("freezer")
|
||||
async def test_setup_port_env_var_used_as_default(
|
||||
hass: HomeAssistant,
|
||||
hass_storage: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test SETUP_PORT is used as the default server port without YAML config."""
|
||||
"""Test SETUP_PORT is used as the default server port without YAML config.
|
||||
|
||||
In production SETUP_PORT is set before the process starts, so it is baked
|
||||
into _DEFAULT_CONFIG at import; the constant is patched to reproduce that.
|
||||
A fresh install serves the default config from the stable slot directly,
|
||||
without staging it as a pending trial.
|
||||
"""
|
||||
with (
|
||||
patch.dict(os.environ, {ENV_SETUP_PORT: "80"}),
|
||||
patch(
|
||||
"homeassistant.components.http.config._DEFAULT_CONFIG", DEFAULT_80_CONFIG
|
||||
),
|
||||
):
|
||||
assert await async_setup_component(hass, "http", {})
|
||||
await hass.async_start()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.config.api.port == 80
|
||||
assert hass_storage["http"]["data"]["pending"] == _stored_config(
|
||||
{"server_port": 80}, created_at=dt_util.utcnow().isoformat()
|
||||
)
|
||||
assert hass_storage["http"]["data"]["pending"] is None
|
||||
assert hass_storage["http"]["data"]["stable"]["server_port"] == 80
|
||||
|
||||
|
||||
async def test_default_config_under_supervisor_starts_redirect(
|
||||
|
||||
Reference in New Issue
Block a user