mirror of
https://github.com/home-assistant/supervisor.git
synced 2026-09-30 02:08:59 +01:00
* Skip redundant inspect calls when constructing Docker container handles Several places in the Docker layer called containers.get(id), which performs an inspect API call, purely to construct a DockerContainer handle immediately followed by another call (show(), exec(), attach()) that either performs its own inspect or doesn't need one at all. Switch these spots to containers.container(id), which builds the same handle locally with no I/O, removing the redundant round-trip. Affected call sites: container_is_initialized(), stop_container() and container_run_inside() in docker/manager.py, _get_container() and _attach() in docker/interface.py, attach()/retag()/update_start_tag() in docker/supervisor.py, and write_stdin() in docker/app.py. Since containers.container()-built handles don't carry the real container ID (unlike get(), which populates it from the inspect response), _attach() and DockerSupervisor.attach() now read the ID from the inspect payload (self._meta["Id"]) instead of the handle's .id property. This intentionally excludes container_stats(), which has its own one-shot inspect+stats opportunity being handled separately in #7168. Follow-up to review discussion in #7168: https://github.com/home-assistant/supervisor/pull/7168#discussion_r3883548019 * Fix tests broken by containers.container() switch Several tests simulated Docker errors/state by mocking containers.get() (the old call site), but the production code now builds container handles via the sync, no-I/O containers.container() instead and only calls get() directly from the small set of call sites intentionally left unconverted (DockerApp's legacy-name migration in attach(), and container_stats() which is handled by #7168). Retarget these tests to mock containers.container() (or both, where a single test exercises both an app's attach() and a plugin/core's attach()) so the simulated failures actually reach the code path they're meant to test. Also add the missing "Id" key to some mocked container inspect payloads in test_check_docker_config.py: _attach() now reads the container ID from the inspect response instead of the handle's .id, so tests need that field present. * Skip unnecessary inspect before stopping container A reviewer pointed out that calling show() before stop() in stop_container() is redundant I/O. Docker returns a 304 (not an error) if the container is already stopped, 404 if it does not exist, so we can call stop() directly and handle those responses without an extra inspect call. * Centralize container fetch error handling in DockerSupervisor attach(), retag() and update_start_tag() each duplicated a try/except block around containers.container(name).show(). Reuse the existing _get_container() helper from DockerInterface instead, raising DockerNotFound explicitly when the container is missing.
102 lines
4.2 KiB
Python
102 lines
4.2 KiB
Python
"""Test Supervisor Docker container timeout handling."""
|
|
|
|
from http import HTTPStatus
|
|
|
|
import aiodocker
|
|
from awesomeversion import AwesomeVersion
|
|
import pytest
|
|
|
|
from supervisor.coresys import CoreSys
|
|
from supervisor.exceptions import DockerNotFound, DockerTimeoutError
|
|
|
|
|
|
async def test_supervisor_attach_get_timeout(coresys: CoreSys):
|
|
"""Test DockerSupervisor.attach raises DockerTimeoutError when show times out."""
|
|
coresys.docker.containers.get.return_value.show.side_effect = TimeoutError()
|
|
with pytest.raises(DockerTimeoutError, match="Timeout occurred"):
|
|
await coresys.supervisor.instance.attach(
|
|
coresys.supervisor.version or AwesomeVersion("2025.1.0")
|
|
)
|
|
|
|
|
|
async def test_supervisor_attach_not_found(coresys: CoreSys):
|
|
"""Test DockerSupervisor.attach raises DockerNotFound when container is missing."""
|
|
coresys.docker.containers.get.return_value.show.side_effect = aiodocker.DockerError(
|
|
HTTPStatus.NOT_FOUND, {"message": "missing"}
|
|
)
|
|
with pytest.raises(DockerNotFound):
|
|
await coresys.supervisor.instance.attach(
|
|
coresys.supervisor.version or AwesomeVersion("2025.1.0")
|
|
)
|
|
|
|
|
|
async def test_supervisor_retag_get_timeout(coresys: CoreSys):
|
|
"""Test DockerSupervisor.retag raises DockerTimeoutError when show times out."""
|
|
coresys.docker.containers.get.return_value.show.side_effect = TimeoutError()
|
|
with pytest.raises(DockerTimeoutError, match="Timeout occurred"):
|
|
await coresys.supervisor.instance.retag()
|
|
|
|
|
|
async def test_supervisor_retag_not_found(coresys: CoreSys):
|
|
"""Test DockerSupervisor.retag raises DockerNotFound when container is missing."""
|
|
coresys.docker.containers.get.return_value.show.side_effect = aiodocker.DockerError(
|
|
HTTPStatus.NOT_FOUND, {"message": "missing"}
|
|
)
|
|
with pytest.raises(DockerNotFound):
|
|
await coresys.supervisor.instance.retag()
|
|
|
|
|
|
async def test_supervisor_retag_tag_timeout(coresys: CoreSys):
|
|
"""Test DockerSupervisor.retag raises DockerTimeoutError when images.tag times out."""
|
|
# Attach first to initialize metadata via the public API.
|
|
coresys.docker.containers.get.return_value.show.return_value = {
|
|
"Id": "abc123",
|
|
"ImageID": "sha256:abc123",
|
|
"Image": "sha256:abc123",
|
|
"Config": {
|
|
"Image": "ghcr.io/home-assistant/amd64-hassio-supervisor:latest",
|
|
"Labels": {"io.hass.version": "2025.1.0"},
|
|
},
|
|
}
|
|
await coresys.supervisor.instance.attach(AwesomeVersion("2025.1.0"))
|
|
|
|
coresys.docker.images.tag.side_effect = TimeoutError()
|
|
with pytest.raises(
|
|
DockerTimeoutError, match="Timeout retagging Supervisor version"
|
|
):
|
|
await coresys.supervisor.instance.retag()
|
|
|
|
|
|
async def test_supervisor_update_start_tag_get_timeout(coresys: CoreSys):
|
|
"""Test update_start_tag raises DockerTimeoutError when show times out."""
|
|
coresys.docker.containers.get.return_value.show.side_effect = TimeoutError()
|
|
with pytest.raises(DockerTimeoutError, match="Timeout occurred"):
|
|
await coresys.supervisor.instance.update_start_tag(
|
|
"ghcr.io/home-assistant/amd64-hassio-supervisor",
|
|
AwesomeVersion("2025.1.0"),
|
|
)
|
|
|
|
|
|
async def test_supervisor_update_start_tag_not_found(coresys: CoreSys):
|
|
"""Test update_start_tag raises DockerNotFound when container is missing."""
|
|
coresys.docker.containers.get.return_value.show.side_effect = aiodocker.DockerError(
|
|
HTTPStatus.NOT_FOUND, {"message": "missing"}
|
|
)
|
|
with pytest.raises(DockerNotFound):
|
|
await coresys.supervisor.instance.update_start_tag(
|
|
"ghcr.io/home-assistant/amd64-hassio-supervisor",
|
|
AwesomeVersion("2025.1.0"),
|
|
)
|
|
|
|
|
|
async def test_supervisor_update_start_tag_inspect_timeout(coresys: CoreSys):
|
|
"""Test update_start_tag raises DockerTimeoutError when images.inspect times out."""
|
|
coresys.docker.images.inspect.side_effect = TimeoutError()
|
|
with pytest.raises(
|
|
DockerTimeoutError, match="Timeout getting image metadata to fix start tag"
|
|
):
|
|
await coresys.supervisor.instance.update_start_tag(
|
|
"ghcr.io/home-assistant/amd64-hassio-supervisor",
|
|
AwesomeVersion("2025.1.0"),
|
|
)
|