mirror of
https://github.com/home-assistant/supervisor.git
synced 2026-08-13 00:12:57 +01:00
Respect user-overridden Core image for install and update (#7092)
* Respect user-overridden Core image for install and update The override_image setting was only honored by load(): the landingpage install, the initial Core install and Core updates always pulled the image from the update information and wrote it back to the Home Assistant config afterwards, discarding the user override on the next install or update. Add a HomeAssistant.install_image property that returns the user-overridden image if set and the image from the update information otherwise. Use it in the landingpage install, Core install and Core update paths, and persist that same image afterwards so the override is kept. Version resolution is unchanged. With an override set, a fresh install now pulls <image>:landingpage from the overridden image as well. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Mark system unsupported when a custom Core image is used As raised in review, running a Home Assistant Core fork should be visible: add an evaluation that marks the system unsupported when the configured Core image differs from the default image for the machine. The evaluation compares the configured image against the default image instead of checking the override_image flag, so it also catches images recorded through container adoption or manual configuration edits where the flag is not set. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Capture install image once per Core install and update As raised in review, the install image was read separately for the image pull and for persisting it to the Home Assistant config. If the image option changes while an install or update job is running, the pulled and the persisted image could diverge. Capture the value once per install attempt respectively once per update job (covering the rollback path) and use it for both the pull and set_image(). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
c5ea8e174e
commit
d30db522c2
@@ -156,7 +156,7 @@ class HomeAssistantCore(JobGroup):
|
||||
|
||||
_LOGGER.info("Setting up Home Assistant landingpage")
|
||||
while True:
|
||||
if not self.sys_updater.image_homeassistant:
|
||||
if not (install_image := self.sys_homeassistant.install_image):
|
||||
_LOGGER.warning(
|
||||
"Updater has no Home Assistant image information yet. Retrying in %ssec",
|
||||
INSTALL_RETRY_WAIT_SECS,
|
||||
@@ -166,9 +166,7 @@ class HomeAssistantCore(JobGroup):
|
||||
continue
|
||||
|
||||
try:
|
||||
await self.instance.install(
|
||||
LANDINGPAGE, image=self.sys_updater.image_homeassistant
|
||||
)
|
||||
await self.instance.install(LANDINGPAGE, image=install_image)
|
||||
break
|
||||
except DockerError, JobException:
|
||||
pass
|
||||
@@ -182,7 +180,7 @@ class HomeAssistantCore(JobGroup):
|
||||
await asyncio.sleep(INSTALL_RETRY_WAIT_SECS)
|
||||
|
||||
self.sys_homeassistant.version = LANDINGPAGE
|
||||
self.sys_homeassistant.set_image(self.sys_updater.image_homeassistant)
|
||||
self.sys_homeassistant.set_image(install_image)
|
||||
await self.sys_homeassistant.save_data()
|
||||
|
||||
@Job(
|
||||
@@ -223,6 +221,7 @@ class HomeAssistantCore(JobGroup):
|
||||
_LOGGER.info("Home Assistant Core installation in progress")
|
||||
|
||||
progress_task = self.sys_create_task(_periodic_progress_log())
|
||||
install_image: str | None = None
|
||||
try:
|
||||
while True:
|
||||
# read homeassistant tag and install it
|
||||
@@ -264,10 +263,8 @@ class HomeAssistantCore(JobGroup):
|
||||
)
|
||||
|
||||
try:
|
||||
await self.instance.update(
|
||||
to_version,
|
||||
image=self.sys_updater.image_homeassistant,
|
||||
)
|
||||
install_image = self.sys_homeassistant.install_image
|
||||
await self.instance.update(to_version, image=install_image)
|
||||
self.sys_homeassistant.version = self.instance.version or to_version
|
||||
break
|
||||
except DockerError, JobException:
|
||||
@@ -285,7 +282,7 @@ class HomeAssistantCore(JobGroup):
|
||||
await progress_task
|
||||
|
||||
_LOGGER.info("Home Assistant docker now installed")
|
||||
self.sys_homeassistant.set_image(self.sys_updater.image_homeassistant)
|
||||
self.sys_homeassistant.set_image(install_image)
|
||||
await self.sys_homeassistant.save_data()
|
||||
|
||||
# finishing
|
||||
@@ -337,6 +334,7 @@ class HomeAssistantCore(JobGroup):
|
||||
)
|
||||
|
||||
old_image = self.sys_homeassistant.image
|
||||
install_image = self.sys_homeassistant.install_image
|
||||
rollback_version = (
|
||||
self.sys_homeassistant.version if not self.error_state else None
|
||||
)
|
||||
@@ -364,9 +362,7 @@ class HomeAssistantCore(JobGroup):
|
||||
"""Pull the Home Assistant image for the given version."""
|
||||
_LOGGER.info("Updating Home Assistant to version %s", to_version)
|
||||
try:
|
||||
await self.instance.update(
|
||||
to_version, image=self.sys_updater.image_homeassistant
|
||||
)
|
||||
await self.instance.update(to_version, image=install_image)
|
||||
except DockerError as err:
|
||||
raise HomeAssistantUpdateImageError(
|
||||
_LOGGER.warning, version=str(to_version)
|
||||
@@ -375,7 +371,7 @@ class HomeAssistantCore(JobGroup):
|
||||
async def _start_update(to_version: AwesomeVersion) -> None:
|
||||
"""Record the new version, (re)start Core and persist the change."""
|
||||
self.sys_homeassistant.version = self.instance.version or to_version
|
||||
self.sys_homeassistant.set_image(self.sys_updater.image_homeassistant)
|
||||
self.sys_homeassistant.set_image(install_image)
|
||||
|
||||
if running:
|
||||
await self.start()
|
||||
|
||||
@@ -219,6 +219,17 @@ class HomeAssistant(FileConfiguration, CoreSysAttributes):
|
||||
"""Enable/disable image override."""
|
||||
self._data[ATTR_OVERRIDE_IMAGE] = value
|
||||
|
||||
@property
|
||||
def install_image(self) -> str | None:
|
||||
"""Return image to pull when installing or updating Home Assistant Core.
|
||||
|
||||
Uses the user-overridden image if set, otherwise the image from the
|
||||
update information.
|
||||
"""
|
||||
if self.override_image:
|
||||
return self.image
|
||||
return self.sys_updater.image_homeassistant
|
||||
|
||||
@property
|
||||
def version(self) -> AwesomeVersion | None:
|
||||
"""Return version of local version."""
|
||||
|
||||
@@ -43,6 +43,7 @@ class UnsupportedReason(StrEnum):
|
||||
DNS_SERVER = "dns_server"
|
||||
DOCKER_CONFIGURATION = "docker_configuration"
|
||||
DOCKER_VERSION = "docker_version"
|
||||
HOME_ASSISTANT_CORE_CUSTOM_IMAGE = "home_assistant_core_custom_image"
|
||||
HOME_ASSISTANT_CORE_VERSION = "home_assistant_core_version"
|
||||
JOB_CONDITIONS = "job_conditions"
|
||||
LXC = "lxc"
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
"""Evaluation class for Core image."""
|
||||
|
||||
from ...const import CoreState
|
||||
from ...coresys import CoreSys
|
||||
from ..const import UnsupportedReason
|
||||
from .base import EvaluateBase
|
||||
|
||||
|
||||
def setup(coresys: CoreSys) -> EvaluateBase:
|
||||
"""Initialize evaluation-setup function."""
|
||||
return EvaluateHomeAssistantCoreCustomImage(coresys)
|
||||
|
||||
|
||||
class EvaluateHomeAssistantCoreCustomImage(EvaluateBase):
|
||||
"""Evaluate the Home Assistant Core image."""
|
||||
|
||||
@property
|
||||
def reason(self) -> UnsupportedReason:
|
||||
"""Return a UnsupportedReason enum."""
|
||||
return UnsupportedReason.HOME_ASSISTANT_CORE_CUSTOM_IMAGE
|
||||
|
||||
@property
|
||||
def on_failure(self) -> str:
|
||||
"""Return a string that is printed when self.evaluate is True."""
|
||||
return f"Home Assistant Core is using the non-default image '{self.sys_homeassistant.image}'!"
|
||||
|
||||
@property
|
||||
def states(self) -> list[CoreState]:
|
||||
"""Return a list of valid states when this evaluation can run."""
|
||||
return [CoreState.RUNNING, CoreState.SETUP]
|
||||
|
||||
async def evaluate(self) -> bool:
|
||||
"""Run evaluation."""
|
||||
return self.sys_homeassistant.image != self.sys_homeassistant.default_image
|
||||
@@ -28,6 +28,7 @@ from supervisor.homeassistant.api import APIState
|
||||
from supervisor.homeassistant.const import LANDINGPAGE, WSEvent
|
||||
from supervisor.homeassistant.core import HomeAssistantCore
|
||||
from supervisor.homeassistant.module import HomeAssistant
|
||||
from supervisor.jobs.const import JobCondition
|
||||
from supervisor.resolution.const import ContextType, IssueType
|
||||
from supervisor.resolution.data import Issue
|
||||
from supervisor.updater import Updater
|
||||
@@ -899,6 +900,71 @@ async def test_core_load_allows_image_override(
|
||||
)
|
||||
|
||||
|
||||
async def test_install_landingpage_uses_overridden_image(coresys: CoreSys):
|
||||
"""Test landingpage install pulls the user-overridden image."""
|
||||
coresys.homeassistant.set_image("myorg/qemux86-64-homeassistant")
|
||||
coresys.homeassistant.override_image = True
|
||||
|
||||
with (
|
||||
patch.object(DockerHomeAssistant, "attach", side_effect=DockerError),
|
||||
patch.object(DockerHomeAssistant, "install") as install,
|
||||
):
|
||||
await coresys.homeassistant.core.install_landingpage()
|
||||
|
||||
install.assert_called_once_with(LANDINGPAGE, image="myorg/qemux86-64-homeassistant")
|
||||
assert coresys.homeassistant.image == "myorg/qemux86-64-homeassistant"
|
||||
assert coresys.homeassistant.version == LANDINGPAGE
|
||||
|
||||
|
||||
async def test_install_uses_overridden_image(coresys: CoreSys):
|
||||
"""Test Core install after landingpage pulls the user-overridden image."""
|
||||
coresys.homeassistant.set_image("myorg/qemux86-64-homeassistant")
|
||||
coresys.homeassistant.override_image = True
|
||||
|
||||
with (
|
||||
patch.object(HomeAssistantCore, "start"),
|
||||
patch.object(DockerHomeAssistant, "cleanup"),
|
||||
patch.object(DockerHomeAssistant, "update") as update,
|
||||
patch.object(
|
||||
Updater,
|
||||
"version_homeassistant",
|
||||
new=PropertyMock(return_value=AwesomeVersion("2022.7.3")),
|
||||
),
|
||||
):
|
||||
await coresys.homeassistant.core.install()
|
||||
|
||||
update.assert_called_once_with(
|
||||
AwesomeVersion("2022.7.3"), image="myorg/qemux86-64-homeassistant"
|
||||
)
|
||||
assert coresys.homeassistant.image == "myorg/qemux86-64-homeassistant"
|
||||
|
||||
|
||||
async def test_update_uses_overridden_image(coresys: CoreSys):
|
||||
"""Test Core update pulls the user-overridden image."""
|
||||
coresys.jobs.ignore_conditions = [
|
||||
JobCondition.FREE_SPACE,
|
||||
JobCondition.HEALTHY,
|
||||
JobCondition.INTERNET_HOST,
|
||||
JobCondition.PLUGINS_UPDATED,
|
||||
JobCondition.SUPERVISOR_UPDATED,
|
||||
]
|
||||
coresys.homeassistant.set_image("myorg/qemux86-64-homeassistant")
|
||||
coresys.homeassistant.override_image = True
|
||||
coresys.homeassistant.version = AwesomeVersion("2022.7.2")
|
||||
|
||||
with (
|
||||
patch.object(DockerHomeAssistant, "update") as update,
|
||||
patch.object(DockerHomeAssistant, "is_running", return_value=False),
|
||||
patch.object(DockerHomeAssistant, "exists", return_value=False),
|
||||
):
|
||||
await coresys.homeassistant.core.update(AwesomeVersion("2022.7.3"))
|
||||
|
||||
update.assert_called_once_with(
|
||||
AwesomeVersion("2022.7.3"), image="myorg/qemux86-64-homeassistant"
|
||||
)
|
||||
assert coresys.homeassistant.image == "myorg/qemux86-64-homeassistant"
|
||||
|
||||
|
||||
async def test_core_loads_wrong_image_for_architecture(
|
||||
coresys: CoreSys, container: DockerContainer
|
||||
):
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
"""Test Core image evaluation."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from supervisor.const import CoreState
|
||||
from supervisor.coresys import CoreSys
|
||||
from supervisor.resolution.evaluations.home_assistant_core_custom_image import (
|
||||
EvaluateHomeAssistantCoreCustomImage,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("image", "expected"),
|
||||
[
|
||||
(None, False), # Unset, default image is used
|
||||
("ghcr.io/home-assistant/qemux86-64-homeassistant", False), # Default image
|
||||
("myorg/qemux86-64-homeassistant", True), # Fork on another registry
|
||||
("ghcr.io/myorg/qemux86-64-homeassistant", True), # Fork on same registry
|
||||
],
|
||||
)
|
||||
async def test_core_image_evaluation(
|
||||
coresys: CoreSys, image: str | None, expected: bool
|
||||
):
|
||||
"""Test evaluation logic on Core image."""
|
||||
evaluation = EvaluateHomeAssistantCoreCustomImage(coresys)
|
||||
await coresys.core.set_state(CoreState.RUNNING)
|
||||
|
||||
coresys.homeassistant.set_image(image)
|
||||
|
||||
assert evaluation.reason not in coresys.resolution.unsupported
|
||||
await evaluation()
|
||||
assert (evaluation.reason in coresys.resolution.unsupported) is expected
|
||||
|
||||
|
||||
async def test_core_image_evaluation_resolves(coresys: CoreSys):
|
||||
"""Test the unsupported state is removed when the image is set back."""
|
||||
evaluation = EvaluateHomeAssistantCoreCustomImage(coresys)
|
||||
await coresys.core.set_state(CoreState.RUNNING)
|
||||
|
||||
coresys.homeassistant.set_image("myorg/qemux86-64-homeassistant")
|
||||
await evaluation()
|
||||
assert evaluation.reason in coresys.resolution.unsupported
|
||||
|
||||
coresys.homeassistant.set_image(None)
|
||||
await evaluation()
|
||||
assert evaluation.reason not in coresys.resolution.unsupported
|
||||
|
||||
|
||||
async def test_did_run(coresys: CoreSys):
|
||||
"""Test that the evaluation ran as expected."""
|
||||
evaluation = EvaluateHomeAssistantCoreCustomImage(coresys)
|
||||
should_run = evaluation.states
|
||||
should_not_run = [state for state in CoreState if state not in should_run]
|
||||
assert len(should_run) != 0
|
||||
assert len(should_not_run) != 0
|
||||
|
||||
with patch(
|
||||
"supervisor.resolution.evaluations.home_assistant_core_custom_image.EvaluateHomeAssistantCoreCustomImage.evaluate",
|
||||
return_value=None,
|
||||
) as evaluate:
|
||||
for state in should_run:
|
||||
await coresys.core.set_state(state)
|
||||
await evaluation()
|
||||
evaluate.assert_called_once()
|
||||
evaluate.reset_mock()
|
||||
|
||||
for state in should_not_run:
|
||||
await coresys.core.set_state(state)
|
||||
await evaluation()
|
||||
evaluate.assert_not_called()
|
||||
evaluate.reset_mock()
|
||||
Reference in New Issue
Block a user