Skip to content

Commit 30cc172

Browse files
mdegat01agners
andauthored
Migrate images from dockerpy to aiodocker (#6252)
* Migrate images from dockerpy to aiodocker * Add missing coverage and fix bug in repair * Bind libraries to different files and refactor images.pull * Use the same socket again Try using the same socket again. * Fix pytest --------- Co-authored-by: Stefan Agner <[email protected]>
1 parent 69ae8db commit 30cc172

20 files changed

+730
-416
lines changed

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
aiodns==3.5.0
2+
aiodocker==0.24.0
23
aiohttp==3.13.2
34
atomicwrites-homeassistant==1.4.1
45
attrs==25.4.0

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/addon.py

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from pathlib import Path
1010
from typing import TYPE_CHECKING, cast
1111

12+
import aiodocker
1213
from attr import evolve
1314
from awesomeversion import AwesomeVersion
1415
import docker
@@ -717,19 +718,21 @@ def build_image():
717718
error_message = f"Docker build failed for {addon_image_tag} (exit code {result.exit_code}). Build output:\n{logs}"
718719
raise docker.errors.DockerException(error_message)
719720

720-
addon_image = self.sys_docker.images.get(addon_image_tag)
721-
722-
return addon_image, logs
721+
return addon_image_tag, logs
723722

724723
try:
725-
docker_image, log = await self.sys_run_in_executor(build_image)
724+
addon_image_tag, log = await self.sys_run_in_executor(build_image)
726725

727726
_LOGGER.debug("Build %s:%s done: %s", self.image, version, log)
728727

729728
# Update meta data
730-
self._meta = docker_image.attrs
729+
self._meta = await self.sys_docker.images.inspect(addon_image_tag)
731730

732-
except (docker.errors.DockerException, requests.RequestException) as err:
731+
except (
732+
docker.errors.DockerException,
733+
requests.RequestException,
734+
aiodocker.DockerError,
735+
) as err:
733736
_LOGGER.error("Can't build %s:%s: %s", self.image, version, err)
734737
raise DockerError() from err
735738

@@ -751,11 +754,8 @@ def export_image(self, tar_file: Path) -> None:
751754
)
752755
async def import_image(self, tar_file: Path) -> None:
753756
"""Import a tar file as image."""
754-
docker_image = await self.sys_run_in_executor(
755-
self.sys_docker.import_image, tar_file
756-
)
757-
if docker_image:
758-
self._meta = docker_image.attrs
757+
if docker_image := await self.sys_docker.import_image(tar_file):
758+
self._meta = docker_image
759759
_LOGGER.info("Importing image %s and version %s", tar_file, self.version)
760760

761761
with suppress(DockerError):
@@ -769,17 +769,21 @@ async def cleanup(
769769
version: AwesomeVersion | None = None,
770770
) -> None:
771771
"""Check if old version exists and cleanup other versions of image not in use."""
772-
await self.sys_run_in_executor(
773-
self.sys_docker.cleanup_old_images,
774-
(image := image or self.image),
775-
version or self.version,
772+
if not (use_image := image or self.image):
773+
raise DockerError("Cannot determine image from metadata!", _LOGGER.error)
774+
if not (use_version := version or self.version):
775+
raise DockerError("Cannot determine version from metadata!", _LOGGER.error)
776+
777+
await self.sys_docker.cleanup_old_images(
778+
use_image,
779+
use_version,
776780
{old_image} if old_image else None,
777781
keep_images={
778782
f"{addon.image}:{addon.version}"
779783
for addon in self.sys_addons.installed
780784
if addon.slug != self.addon.slug
781785
and addon.image
782-
and addon.image in {old_image, image}
786+
and addon.image in {old_image, use_image}
783787
},
784788
)
785789

supervisor/docker/homeassistant.py

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
"""Init file for Supervisor Docker object."""
22

3-
from collections.abc import Awaitable
43
from ipaddress import IPv4Address
54
import logging
65
import re
@@ -236,11 +235,10 @@ async def execute_command(self, command: str) -> CommandReturn:
236235
environment={ENV_TIME: self.sys_timezone},
237236
)
238237

239-
def is_initialize(self) -> Awaitable[bool]:
238+
async def is_initialize(self) -> bool:
240239
"""Return True if Docker container exists."""
241-
return self.sys_run_in_executor(
242-
self.sys_docker.container_is_initialized,
243-
self.name,
244-
self.image,
245-
self.sys_homeassistant.version,
240+
if not self.sys_homeassistant.version:
241+
return False
242+
return await self.sys_docker.container_is_initialized(
243+
self.name, self.image, self.sys_homeassistant.version
246244
)

supervisor/docker/interface.py

Lines changed: 54 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,18 @@
66
from collections import defaultdict
77
from collections.abc import Awaitable
88
from contextlib import suppress
9+
from http import HTTPStatus
910
import logging
1011
import re
1112
from time import time
1213
from typing import Any, cast
1314
from uuid import uuid4
1415

16+
import aiodocker
1517
from awesomeversion import AwesomeVersion
1618
from awesomeversion.strategy import AwesomeVersionStrategy
1719
import docker
1820
from docker.models.containers import Container
19-
from docker.models.images import Image
2021
import requests
2122

2223
from ..bus import EventListener
@@ -33,6 +34,7 @@
3334
from ..exceptions import (
3435
DockerAPIError,
3536
DockerError,
37+
DockerHubRateLimitExceeded,
3638
DockerJobError,
3739
DockerLogOutOfOrder,
3840
DockerNotFound,
@@ -215,7 +217,7 @@ async def _docker_login(self, image: str) -> None:
215217
if not credentials:
216218
return
217219

218-
await self.sys_run_in_executor(self.sys_docker.docker.login, **credentials)
220+
await self.sys_run_in_executor(self.sys_docker.dockerpy.login, **credentials)
219221

220222
def _process_pull_image_log( # noqa: C901
221223
self, install_job_id: str, reference: PullLogEntry
@@ -418,8 +420,7 @@ async def process_pull_image_log(reference: PullLogEntry) -> None:
418420
)
419421

420422
# Pull new image
421-
docker_image = await self.sys_run_in_executor(
422-
self.sys_docker.pull_image,
423+
docker_image = await self.sys_docker.pull_image(
423424
self.sys_jobs.current.uuid,
424425
image,
425426
str(version),
@@ -431,22 +432,37 @@ async def process_pull_image_log(reference: PullLogEntry) -> None:
431432
_LOGGER.info(
432433
"Tagging image %s with version %s as latest", image, version
433434
)
434-
await self.sys_run_in_executor(docker_image.tag, image, tag="latest")
435+
await self.sys_docker.images.tag(
436+
docker_image["Id"], image, tag="latest"
437+
)
435438
except docker.errors.APIError as err:
436-
if err.status_code == 429:
439+
if err.status_code == HTTPStatus.TOO_MANY_REQUESTS:
437440
self.sys_resolution.create_issue(
438441
IssueType.DOCKER_RATELIMIT,
439442
ContextType.SYSTEM,
440443
suggestions=[SuggestionType.REGISTRY_LOGIN],
441444
)
442-
_LOGGER.info(
443-
"Your IP address has made too many requests to Docker Hub which activated a rate limit. "
444-
"For more details see https://www.home-assistant.io/more-info/dockerhub-rate-limit"
445+
raise DockerHubRateLimitExceeded(_LOGGER.error) from err
446+
await async_capture_exception(err)
447+
raise DockerError(
448+
f"Can't install {image}:{version!s}: {err}", _LOGGER.error
449+
) from err
450+
except aiodocker.DockerError as err:
451+
if err.status == HTTPStatus.TOO_MANY_REQUESTS:
452+
self.sys_resolution.create_issue(
453+
IssueType.DOCKER_RATELIMIT,
454+
ContextType.SYSTEM,
455+
suggestions=[SuggestionType.REGISTRY_LOGIN],
445456
)
457+
raise DockerHubRateLimitExceeded(_LOGGER.error) from err
458+
await async_capture_exception(err)
446459
raise DockerError(
447460
f"Can't install {image}:{version!s}: {err}", _LOGGER.error
448461
) from err
449-
except (docker.errors.DockerException, requests.RequestException) as err:
462+
except (
463+
docker.errors.DockerException,
464+
requests.RequestException,
465+
) as err:
450466
await async_capture_exception(err)
451467
raise DockerError(
452468
f"Unknown error with {image}:{version!s} -> {err!s}", _LOGGER.error
@@ -455,14 +471,12 @@ async def process_pull_image_log(reference: PullLogEntry) -> None:
455471
if listener:
456472
self.sys_bus.remove_listener(listener)
457473

458-
self._meta = docker_image.attrs
474+
self._meta = docker_image
459475

460476
async def exists(self) -> bool:
461477
"""Return True if Docker image exists in local repository."""
462-
with suppress(docker.errors.DockerException, requests.RequestException):
463-
await self.sys_run_in_executor(
464-
self.sys_docker.images.get, f"{self.image}:{self.version!s}"
465-
)
478+
with suppress(aiodocker.DockerError, requests.RequestException):
479+
await self.sys_docker.images.inspect(f"{self.image}:{self.version!s}")
466480
return True
467481
return False
468482

@@ -521,11 +535,11 @@ async def attach(
521535
),
522536
)
523537

524-
with suppress(docker.errors.DockerException, requests.RequestException):
538+
with suppress(aiodocker.DockerError, requests.RequestException):
525539
if not self._meta and self.image:
526-
self._meta = self.sys_docker.images.get(
540+
self._meta = await self.sys_docker.images.inspect(
527541
f"{self.image}:{version!s}"
528-
).attrs
542+
)
529543

530544
# Successful?
531545
if not self._meta:
@@ -593,14 +607,17 @@ def start(self) -> Awaitable[None]:
593607
)
594608
async def remove(self, *, remove_image: bool = True) -> None:
595609
"""Remove Docker images."""
610+
if not self.image or not self.version:
611+
raise DockerError(
612+
"Cannot determine image and/or version from metadata!", _LOGGER.error
613+
)
614+
596615
# Cleanup container
597616
with suppress(DockerError):
598617
await self.stop()
599618

600619
if remove_image:
601-
await self.sys_run_in_executor(
602-
self.sys_docker.remove_image, self.image, self.version
603-
)
620+
await self.sys_docker.remove_image(self.image, self.version)
604621

605622
self._meta = None
606623

@@ -622,18 +639,16 @@ async def check_image(
622639
image_name = f"{expected_image}:{version!s}"
623640
if self.image == expected_image:
624641
try:
625-
image: Image = await self.sys_run_in_executor(
626-
self.sys_docker.images.get, image_name
627-
)
628-
except (docker.errors.DockerException, requests.RequestException) as err:
642+
image = await self.sys_docker.images.inspect(image_name)
643+
except (aiodocker.DockerError, requests.RequestException) as err:
629644
raise DockerError(
630645
f"Could not get {image_name} for check due to: {err!s}",
631646
_LOGGER.error,
632647
) from err
633648

634-
image_arch = f"{image.attrs['Os']}/{image.attrs['Architecture']}"
635-
if "Variant" in image.attrs:
636-
image_arch = f"{image_arch}/{image.attrs['Variant']}"
649+
image_arch = f"{image['Os']}/{image['Architecture']}"
650+
if "Variant" in image:
651+
image_arch = f"{image_arch}/{image['Variant']}"
637652

638653
# If we have an image and its the right arch, all set
639654
# It seems that newer Docker version return a variant for arm64 images.
@@ -695,11 +710,13 @@ async def cleanup(
695710
version: AwesomeVersion | None = None,
696711
) -> None:
697712
"""Check if old version exists and cleanup."""
698-
await self.sys_run_in_executor(
699-
self.sys_docker.cleanup_old_images,
700-
image or self.image,
701-
version or self.version,
702-
{old_image} if old_image else None,
713+
if not (use_image := image or self.image):
714+
raise DockerError("Cannot determine image from metadata!", _LOGGER.error)
715+
if not (use_version := version or self.version):
716+
raise DockerError("Cannot determine version from metadata!", _LOGGER.error)
717+
718+
await self.sys_docker.cleanup_old_images(
719+
use_image, use_version, {old_image} if old_image else None
703720
)
704721

705722
@Job(
@@ -751,10 +768,10 @@ async def get_latest_version(self) -> AwesomeVersion:
751768
"""Return latest version of local image."""
752769
available_version: list[AwesomeVersion] = []
753770
try:
754-
for image in await self.sys_run_in_executor(
755-
self.sys_docker.images.list, self.image
771+
for image in await self.sys_docker.images.list(
772+
filters=f'{{"reference": ["{self.image}"]}}'
756773
):
757-
for tag in image.tags:
774+
for tag in image["RepoTags"]:
758775
version = AwesomeVersion(tag.partition(":")[2])
759776
if version.strategy == AwesomeVersionStrategy.UNKNOWN:
760777
continue
@@ -763,7 +780,7 @@ async def get_latest_version(self) -> AwesomeVersion:
763780
if not available_version:
764781
raise ValueError()
765782

766-
except (docker.errors.DockerException, ValueError) as err:
783+
except (aiodocker.DockerError, ValueError) as err:
767784
raise DockerNotFound(
768785
f"No version found for {self.image}", _LOGGER.info
769786
) from err

0 commit comments

Comments
 (0)