Files
supervisor/tests/docker/test_supervisor.py
T
Mike Degatano 9adf131901 Skip redundant inspect calls when constructing Docker container handles (#7175)
* 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.
2026-09-01 00:52:00 +02:00

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"),
)