Skip to content

Commit 94d1f52

Browse files
committed
Bind libraries to different files and refactor images.pull
1 parent 33c09a7 commit 94d1f52

File tree

11 files changed

+174
-70
lines changed

11 files changed

+174
-70
lines changed

supervisor/bus.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
from asyncio import Task
56
from collections.abc import Callable, Coroutine
67
import logging
78
from typing import Any
@@ -38,11 +39,13 @@ def register_event(
3839
self._listeners.setdefault(event, []).append(listener)
3940
return listener
4041

41-
def fire_event(self, event: BusEvent, reference: Any) -> None:
42+
def fire_event(self, event: BusEvent, reference: Any) -> list[Task]:
4243
"""Fire an event to the bus."""
4344
_LOGGER.debug("Fire event '%s' with '%s'", event, reference)
45+
tasks: list[Task] = []
4446
for listener in self._listeners.get(event, []):
45-
self.sys_create_task(listener.callback(reference))
47+
tasks.append(self.sys_create_task(listener.callback(reference)))
48+
return tasks
4649

4750
def remove_listener(self, listener: EventListener) -> None:
4851
"""Unregister an listener."""

supervisor/docker/interface.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -452,11 +452,23 @@ async def process_pull_image_log(reference: PullLogEntry) -> None:
452452
suggestions=[SuggestionType.REGISTRY_LOGIN],
453453
)
454454
raise DockerHubRateLimitExceeded(_LOGGER.error) from err
455+
await async_capture_exception(err)
456+
raise DockerError(
457+
f"Can't install {image}:{version!s}: {err}", _LOGGER.error
458+
) from err
459+
except aiodocker.DockerError as err:
460+
if err.status == HTTPStatus.TOO_MANY_REQUESTS:
461+
self.sys_resolution.create_issue(
462+
IssueType.DOCKER_RATELIMIT,
463+
ContextType.SYSTEM,
464+
suggestions=[SuggestionType.REGISTRY_LOGIN],
465+
)
466+
raise DockerHubRateLimitExceeded(_LOGGER.error) from err
467+
await async_capture_exception(err)
455468
raise DockerError(
456469
f"Can't install {image}:{version!s}: {err}", _LOGGER.error
457470
) from err
458471
except (
459-
aiodocker.DockerError,
460472
docker.errors.DockerException,
461473
requests.RequestException,
462474
) as err:

supervisor/docker/manager.py

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import aiodocker
1919
from aiodocker.images import DockerImages
20+
from aiohttp import ClientSession, ClientTimeout, UnixConnector
2021
import attr
2122
from awesomeversion import AwesomeVersion, AwesomeVersionCompareException
2223
from docker import errors as docker_errors
@@ -211,7 +212,10 @@ def __init__(self, coresys: CoreSys):
211212
# We keep both until we can fully refactor to aiodocker
212213
self._dockerpy: DockerClient | None = None
213214
self.docker: aiodocker.Docker = aiodocker.Docker(
214-
url=f"unix:/{str(SOCKET_DOCKER)}", api_version="auto"
215+
url="unix://localhost", # dummy hostname for URL composition
216+
connector=(connector := UnixConnector(SOCKET_DOCKER.as_posix())),
217+
session=ClientSession(connector=connector, timeout=ClientTimeout(900)),
218+
api_version="auto",
215219
)
216220

217221
self._network: DockerNetwork | None = None
@@ -221,11 +225,13 @@ def __init__(self, coresys: CoreSys):
221225

222226
async def post_init(self) -> Self:
223227
"""Post init actions that must be done in event loop."""
228+
# Use /var/run/docker.sock for this one so aiodocker and dockerpy don't
229+
# share the same handle. Temporary fix while refactoring this client out
224230
self._dockerpy = await asyncio.get_running_loop().run_in_executor(
225231
None,
226232
partial(
227233
DockerClient,
228-
base_url=f"unix:/{str(SOCKET_DOCKER)}",
234+
base_url=f"unix://var{SOCKET_DOCKER.as_posix()}",
229235
version="auto",
230236
timeout=900,
231237
),
@@ -433,20 +439,16 @@ async def pull_image(
433439
raises only if the get fails afterwards. Additionally it fires progress reports for the pull
434440
on the bus so listeners can use that to update status for users.
435441
"""
436-
437-
def api_pull():
438-
pull_log = self.dockerpy.api.pull(
439-
repository, tag=tag, platform=platform, stream=True, decode=True
442+
async for e in self.images.pull(
443+
repository, tag=tag, platform=platform, stream=True
444+
):
445+
entry = PullLogEntry.from_pull_log_dict(job_id, e)
446+
if entry.error:
447+
raise entry.exception
448+
await asyncio.gather(
449+
*self.sys_bus.fire_event(BusEvent.DOCKER_IMAGE_PULL_UPDATE, entry)
440450
)
441-
for e in pull_log:
442-
entry = PullLogEntry.from_pull_log_dict(job_id, e)
443-
if entry.error:
444-
raise entry.exception
445-
self.sys_loop.call_soon_threadsafe(
446-
self.sys_bus.fire_event, BusEvent.DOCKER_IMAGE_PULL_UPDATE, entry
447-
)
448451

449-
await self.sys_run_in_executor(api_pull)
450452
sep = "@" if tag.startswith("sha256:") else ":"
451453
return await self.images.inspect(f"{repository}{sep}{tag}")
452454

tests/addons/test_addon.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
import aiodocker
1111
from awesomeversion import AwesomeVersion
12-
from docker.errors import DockerException, NotFound
12+
from docker.errors import APIError, DockerException, NotFound
1313
import pytest
1414
from securetar import SecureTarFile
1515

@@ -931,7 +931,7 @@ async def test_addon_loads_missing_image(
931931

932932
@pytest.mark.parametrize(
933933
"pull_image_exc",
934-
[DockerException(), aiodocker.DockerError(400, {"message": "error"})],
934+
[APIError("error"), aiodocker.DockerError(400, {"message": "error"})],
935935
)
936936
@pytest.mark.usefixtures("container", "mock_amd64_arch_supported")
937937
async def test_addon_load_succeeds_with_docker_errors(
@@ -973,7 +973,7 @@ async def test_addon_load_succeeds_with_docker_errors(
973973
caplog.clear()
974974
with patch.object(DockerAPI, "pull_image", side_effect=pull_image_exc):
975975
await install_addon_ssh.load()
976-
assert "Unknown error with test/amd64-addon-ssh:9.2.1" in caplog.text
976+
assert "Can't install test/amd64-addon-ssh:9.2.1:" in caplog.text
977977

978978

979979
async def test_addon_manual_only_boot(coresys: CoreSys, install_addon_example: Addon):

tests/api/test_homeassistant.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from supervisor.homeassistant.module import HomeAssistant
2020

2121
from tests.api import common_test_api_advanced_logs
22-
from tests.common import load_json_fixture
22+
from tests.common import AsyncIterator, load_json_fixture
2323

2424

2525
@pytest.mark.parametrize("legacy_route", [True, False])
@@ -283,9 +283,9 @@ async def test_api_progress_updates_home_assistant_update(
283283
"""Test progress updates sent to Home Assistant for updates."""
284284
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
285285
coresys.core.set_state(CoreState.RUNNING)
286-
coresys.docker.dockerpy.api.pull.return_value = load_json_fixture(
287-
"docker_pull_image_log.json"
288-
)
286+
287+
logs = load_json_fixture("docker_pull_image_log.json")
288+
coresys.docker.images.pull.return_value = AsyncIterator(logs)
289289
coresys.homeassistant.version = AwesomeVersion("2025.8.0")
290290

291291
with (

tests/api/test_store.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from supervisor.store.addon import AddonStore
2525
from supervisor.store.repository import Repository
2626

27-
from tests.common import load_json_fixture
27+
from tests.common import AsyncIterator, load_json_fixture
2828
from tests.const import TEST_ADDON_SLUG
2929

3030
REPO_URL = "https://github.com/awesome-developer/awesome-repo"
@@ -732,9 +732,10 @@ async def test_api_progress_updates_addon_install_update(
732732
"""Test progress updates sent to Home Assistant for installs/updates."""
733733
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
734734
coresys.core.set_state(CoreState.RUNNING)
735-
coresys.docker.dockerpy.api.pull.return_value = load_json_fixture(
736-
"docker_pull_image_log.json"
737-
)
735+
736+
logs = load_json_fixture("docker_pull_image_log.json")
737+
coresys.docker.images.pull.return_value = AsyncIterator(logs)
738+
738739
coresys.arch._supported_arch = ["amd64"] # pylint: disable=protected-access
739740
install_addon_example.data_store["version"] = AwesomeVersion("2.0.0")
740741

tests/api/test_supervisor.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from supervisor.updater import Updater
2020

2121
from tests.api import common_test_api_advanced_logs
22-
from tests.common import load_json_fixture
22+
from tests.common import AsyncIterator, load_json_fixture
2323
from tests.dbus_service_mocks.base import DBusServiceMock
2424
from tests.dbus_service_mocks.os_agent import OSAgent as OSAgentService
2525

@@ -332,9 +332,9 @@ async def test_api_progress_updates_supervisor_update(
332332
"""Test progress updates sent to Home Assistant for updates."""
333333
coresys.hardware.disk.get_disk_free_space = lambda x: 5000
334334
coresys.core.set_state(CoreState.RUNNING)
335-
coresys.docker.dockerpy.api.pull.return_value = load_json_fixture(
336-
"docker_pull_image_log.json"
337-
)
335+
336+
logs = load_json_fixture("docker_pull_image_log.json")
337+
coresys.docker.images.pull.return_value = AsyncIterator(logs)
338338

339339
with (
340340
patch.object(

tests/common.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
"""Common test functions."""
22

33
import asyncio
4+
from collections.abc import Sequence
45
from datetime import datetime
56
from functools import partial
67
from importlib import import_module
78
from inspect import getclosurevars
89
import json
910
from pathlib import Path
10-
from typing import Any
11+
from typing import Any, Self
1112

1213
from dbus_fast.aio.message_bus import MessageBus
1314

@@ -145,3 +146,22 @@ async def __aenter__(self):
145146

146147
async def __aexit__(self, exc_type, exc, tb):
147148
"""Exit the context manager."""
149+
150+
151+
class AsyncIterator:
152+
"""Make list/fixture into async iterator for test mocks."""
153+
154+
def __init__(self, seq: Sequence[Any]) -> None:
155+
"""Initialize with sequence."""
156+
self.iter = iter(seq)
157+
158+
def __aiter__(self) -> Self:
159+
"""Implement aiter."""
160+
return self
161+
162+
async def __anext__(self) -> Any:
163+
"""Return next in sequence."""
164+
try:
165+
return next(self.iter)
166+
except StopIteration:
167+
raise StopAsyncIteration() from None

tests/conftest.py

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
from supervisor.utils.dt import utcnow
5757

5858
from .common import (
59+
AsyncIterator,
5960
MockResponse,
6061
load_binary_fixture,
6162
load_fixture,
@@ -125,14 +126,8 @@ async def docker() -> DockerAPI:
125126
patch(
126127
"supervisor.docker.manager.DockerAPI.containers", return_value=MagicMock()
127128
),
128-
patch(
129-
"supervisor.docker.manager.DockerAPI.api",
130-
return_value=(api_mock := MagicMock()),
131-
),
132-
patch(
133-
"supervisor.docker.manager.DockerAPI.info",
134-
return_value=MagicMock(),
135-
),
129+
patch("supervisor.docker.manager.DockerAPI.api", return_value=MagicMock()),
130+
patch("supervisor.docker.manager.DockerAPI.info", return_value=MagicMock()),
136131
patch("supervisor.docker.manager.DockerAPI.unload"),
137132
patch("supervisor.docker.manager.aiodocker.Docker", return_value=MagicMock()),
138133
patch(
@@ -153,13 +148,12 @@ async def docker() -> DockerAPI:
153148
{"stream": "Loaded image: test:latest\n"}
154149
]
155150

151+
docker_images.pull.return_value = AsyncIterator([{}])
152+
156153
docker_obj.info.logging = "journald"
157154
docker_obj.info.storage = "overlay2"
158155
docker_obj.info.version = AwesomeVersion("1.0.0")
159156

160-
# Need an iterable for logs
161-
api_mock.pull.return_value = []
162-
163157
yield docker_obj
164158

165159

0 commit comments

Comments
 (0)