Skip to content

Commit 8d4957c

Browse files
committed
Add missing coverage and fix bug in repair
1 parent 07e9c1c commit 8d4957c

File tree

2 files changed

+124
-26
lines changed

2 files changed

+124
-26
lines changed

supervisor/docker/manager.py

Lines changed: 24 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
from docker import errors as docker_errors
2323
from docker.api.client import APIClient
2424
from docker.client import DockerClient
25-
from docker.errors import DockerException, NotFound
2625
from docker.models.containers import Container, ContainerCollection
2726
from docker.models.networks import Network
2827
from docker.types.daemon import CancellableStream
@@ -519,7 +518,7 @@ def repair(self) -> None:
519518

520519
_LOGGER.info("Prune stale volumes")
521520
try:
522-
output = self.dockerpy.api.prune_builds()
521+
output = self.dockerpy.api.prune_volumes()
523522
_LOGGER.debug("Volumes prune: %s", output)
524523
except docker_errors.APIError as err:
525524
_LOGGER.warning("Error for volumes prune: %s", err)
@@ -574,13 +573,13 @@ async def container_is_initialized(
574573
try:
575574
docker_container = await self.sys_run_in_executor(self.containers.get, name)
576575
docker_image = await self.images.inspect(f"{image}:{version}")
577-
except NotFound:
576+
except docker_errors.NotFound:
578577
return False
579578
except aiodocker.DockerError as err:
580579
if err.status == HTTPStatus.NOT_FOUND:
581580
return False
582581
raise DockerError() from err
583-
except (DockerException, requests.RequestException) as err:
582+
except (docker_errors.DockerException, requests.RequestException) as err:
584583
raise DockerError() from err
585584

586585
# Check the image is correct and state is good
@@ -596,18 +595,18 @@ def stop_container(
596595
"""Stop/remove Docker container."""
597596
try:
598597
docker_container: Container = self.containers.get(name)
599-
except NotFound:
598+
except docker_errors.NotFound:
600599
raise DockerNotFound() from None
601-
except (DockerException, requests.RequestException) as err:
600+
except (docker_errors.DockerException, requests.RequestException) as err:
602601
raise DockerError() from err
603602

604603
if docker_container.status == "running":
605604
_LOGGER.info("Stopping %s application", name)
606-
with suppress(DockerException, requests.RequestException):
605+
with suppress(docker_errors.DockerException, requests.RequestException):
607606
docker_container.stop(timeout=timeout)
608607

609608
if remove_container:
610-
with suppress(DockerException, requests.RequestException):
609+
with suppress(docker_errors.DockerException, requests.RequestException):
611610
_LOGGER.info("Cleaning %s application", name)
612611
docker_container.remove(force=True, v=True)
613612

@@ -619,48 +618,48 @@ def start_container(self, name: str) -> None:
619618
"""Start Docker container."""
620619
try:
621620
docker_container: Container = self.containers.get(name)
622-
except NotFound:
621+
except docker_errors.NotFound:
623622
raise DockerNotFound(
624623
f"{name} not found for starting up", _LOGGER.error
625624
) from None
626-
except (DockerException, requests.RequestException) as err:
625+
except (docker_errors.DockerException, requests.RequestException) as err:
627626
raise DockerError(
628627
f"Could not get {name} for starting up", _LOGGER.error
629628
) from err
630629

631630
_LOGGER.info("Starting %s", name)
632631
try:
633632
docker_container.start()
634-
except (DockerException, requests.RequestException) as err:
633+
except (docker_errors.DockerException, requests.RequestException) as err:
635634
raise DockerError(f"Can't start {name}: {err}", _LOGGER.error) from err
636635

637636
def restart_container(self, name: str, timeout: int) -> None:
638637
"""Restart docker container."""
639638
try:
640639
container: Container = self.containers.get(name)
641-
except NotFound:
640+
except docker_errors.NotFound:
642641
raise DockerNotFound() from None
643-
except (DockerException, requests.RequestException) as err:
642+
except (docker_errors.DockerException, requests.RequestException) as err:
644643
raise DockerError() from err
645644

646645
_LOGGER.info("Restarting %s", name)
647646
try:
648647
container.restart(timeout=timeout)
649-
except (DockerException, requests.RequestException) as err:
648+
except (docker_errors.DockerException, requests.RequestException) as err:
650649
raise DockerError(f"Can't restart {name}: {err}", _LOGGER.warning) from err
651650

652651
def container_logs(self, name: str, tail: int = 100) -> bytes:
653652
"""Return Docker logs of container."""
654653
try:
655654
docker_container: Container = self.containers.get(name)
656-
except NotFound:
655+
except docker_errors.NotFound:
657656
raise DockerNotFound() from None
658-
except (DockerException, requests.RequestException) as err:
657+
except (docker_errors.DockerException, requests.RequestException) as err:
659658
raise DockerError() from err
660659

661660
try:
662661
return docker_container.logs(tail=tail, stdout=True, stderr=True)
663-
except (DockerException, requests.RequestException) as err:
662+
except (docker_errors.DockerException, requests.RequestException) as err:
664663
raise DockerError(
665664
f"Can't grep logs from {name}: {err}", _LOGGER.warning
666665
) from err
@@ -669,9 +668,9 @@ def container_stats(self, name: str) -> dict[str, Any]:
669668
"""Read and return stats from container."""
670669
try:
671670
docker_container: Container = self.containers.get(name)
672-
except NotFound:
671+
except docker_errors.NotFound:
673672
raise DockerNotFound() from None
674-
except (DockerException, requests.RequestException) as err:
673+
except (docker_errors.DockerException, requests.RequestException) as err:
675674
raise DockerError() from err
676675

677676
# container is not running
@@ -680,7 +679,7 @@ def container_stats(self, name: str) -> dict[str, Any]:
680679

681680
try:
682681
return docker_container.stats(stream=False)
683-
except (DockerException, requests.RequestException) as err:
682+
except (docker_errors.DockerException, requests.RequestException) as err:
684683
raise DockerError(
685684
f"Can't read stats from {name}: {err}", _LOGGER.error
686685
) from err
@@ -689,15 +688,15 @@ def container_run_inside(self, name: str, command: str) -> CommandReturn:
689688
"""Execute a command inside Docker container."""
690689
try:
691690
docker_container: Container = self.containers.get(name)
692-
except NotFound:
691+
except docker_errors.NotFound:
693692
raise DockerNotFound() from None
694-
except (DockerException, requests.RequestException) as err:
693+
except (docker_errors.DockerException, requests.RequestException) as err:
695694
raise DockerError() from err
696695

697696
# Execute
698697
try:
699698
code, output = docker_container.exec_run(command)
700-
except (DockerException, requests.RequestException) as err:
699+
except (docker_errors.DockerException, requests.RequestException) as err:
701700
raise DockerError() from err
702701

703702
return CommandReturn(code, output)
@@ -756,7 +755,7 @@ async def import_image(self, tar_file: Path) -> dict[str, Any] | None:
756755
return None
757756

758757
try:
759-
return await self.images.get(docker_image_list[0])
758+
return await self.images.inspect(docker_image_list[0])
760759
except (aiodocker.DockerError, requests.RequestException) as err:
761760
raise DockerError(
762761
f"Could not inspect imported image due to: {err!s}", _LOGGER.error
@@ -766,7 +765,7 @@ def export_image(self, image: str, version: AwesomeVersion, tar_file: Path) -> N
766765
"""Export current images into a tar file."""
767766
try:
768767
docker_image = self.api.get_image(f"{image}:{version}")
769-
except (DockerException, requests.RequestException) as err:
768+
except (docker_errors.DockerException, requests.RequestException) as err:
770769
raise DockerError(
771770
f"Can't fetch image {image}: {err}", _LOGGER.error
772771
) from err

tests/docker/test_manager.py

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
"""Test Docker manager."""
22

33
import asyncio
4+
from pathlib import Path
45
from unittest.mock import MagicMock, patch
56

6-
from docker.errors import DockerException
7+
from docker.errors import APIError, DockerException, NotFound
78
import pytest
89
from requests import RequestException
910

@@ -351,3 +352,101 @@ async def test_run_container_with_leftover_cidfile_directory(
351352
assert cidfile_path.read_text() == mock_container.id
352353

353354
assert result == mock_container
355+
356+
357+
async def test_repair(coresys: CoreSys, caplog: pytest.LogCaptureFixture):
358+
"""Test repair API."""
359+
coresys.docker.dockerpy.networks.get.side_effect = [
360+
hassio := MagicMock(
361+
attrs={
362+
"Containers": {
363+
"good": {"Name": "good"},
364+
"corrupt": {"Name": "corrupt"},
365+
"fail": {"Name": "fail"},
366+
}
367+
}
368+
),
369+
host := MagicMock(attrs={"Containers": {}}),
370+
]
371+
coresys.docker.dockerpy.containers.get.side_effect = [
372+
MagicMock(),
373+
NotFound("corrupt"),
374+
DockerException("fail"),
375+
]
376+
377+
await coresys.run_in_executor(coresys.docker.repair)
378+
379+
coresys.docker.dockerpy.api.prune_containers.assert_called_once()
380+
coresys.docker.dockerpy.api.prune_images.assert_called_once_with(
381+
filters={"dangling": False}
382+
)
383+
coresys.docker.dockerpy.api.prune_builds.assert_called_once()
384+
coresys.docker.dockerpy.api.prune_volumes.assert_called_once()
385+
coresys.docker.dockerpy.api.prune_networks.assert_called_once()
386+
hassio.disconnect.assert_called_once_with("corrupt", force=True)
387+
host.disconnect.assert_not_called()
388+
assert "Docker fatal error on container fail on hassio" in caplog.text
389+
390+
391+
async def test_repair_failures(coresys: CoreSys, caplog: pytest.LogCaptureFixture):
392+
"""Test repair proceeds best it can through failures."""
393+
coresys.docker.dockerpy.api.prune_containers.side_effect = APIError("fail")
394+
coresys.docker.dockerpy.api.prune_images.side_effect = APIError("fail")
395+
coresys.docker.dockerpy.api.prune_builds.side_effect = APIError("fail")
396+
coresys.docker.dockerpy.api.prune_volumes.side_effect = APIError("fail")
397+
coresys.docker.dockerpy.api.prune_networks.side_effect = APIError("fail")
398+
coresys.docker.dockerpy.networks.get.side_effect = NotFound("missing")
399+
400+
await coresys.run_in_executor(coresys.docker.repair)
401+
402+
assert "Error for containers prune: fail" in caplog.text
403+
assert "Error for images prune: fail" in caplog.text
404+
assert "Error for builds prune: fail" in caplog.text
405+
assert "Error for volumes prune: fail" in caplog.text
406+
assert "Error for networks prune: fail" in caplog.text
407+
assert "Error for networks hassio prune: missing" in caplog.text
408+
assert "Error for networks host prune: missing" in caplog.text
409+
410+
411+
@pytest.mark.parametrize("log_starter", [("Loaded image ID"), ("Loaded image")])
412+
async def test_import_image(coresys: CoreSys, tmp_path: Path, log_starter: str):
413+
"""Test importing an image into docker."""
414+
(test_tar := tmp_path / "test.tar").touch()
415+
coresys.docker.images.import_image.return_value = [
416+
{"stream": f"{log_starter}: imported"}
417+
]
418+
coresys.docker.images.inspect.return_value = {"Id": "imported"}
419+
420+
image = await coresys.docker.import_image(test_tar)
421+
422+
assert image["Id"] == "imported"
423+
coresys.docker.images.inspect.assert_called_once_with("imported")
424+
425+
426+
async def test_import_image_error(coresys: CoreSys, tmp_path: Path):
427+
"""Test failure importing an image into docker."""
428+
(test_tar := tmp_path / "test.tar").touch()
429+
coresys.docker.images.import_image.return_value = [
430+
{"errorDetail": {"message": "fail"}}
431+
]
432+
433+
with pytest.raises(DockerError, match="Can't import image from tar: fail"):
434+
await coresys.docker.import_image(test_tar)
435+
436+
coresys.docker.images.inspect.assert_not_called()
437+
438+
439+
async def test_import_multiple_images_in_tar(
440+
coresys: CoreSys, tmp_path: Path, caplog: pytest.LogCaptureFixture
441+
):
442+
"""Test importing an image into docker."""
443+
(test_tar := tmp_path / "test.tar").touch()
444+
coresys.docker.images.import_image.return_value = [
445+
{"stream": "Loaded image: imported-1"},
446+
{"stream": "Loaded image: imported-2"},
447+
]
448+
449+
assert await coresys.docker.import_image(test_tar) is None
450+
451+
assert "Unexpected image count 2 while importing image from tar" in caplog.text
452+
coresys.docker.images.inspect.assert_not_called()

0 commit comments

Comments
 (0)