Skip to content

Commit

Permalink
Keep shared images on addon uninstall
Browse files Browse the repository at this point in the history
  • Loading branch information
mdegat01 committed Aug 20, 2024
1 parent 8ab396d commit 85ed855
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 7 deletions.
6 changes: 4 additions & 2 deletions supervisor/addons/addon.py
Original file line number Diff line number Diff line change
Expand Up @@ -763,10 +763,12 @@ async def install(self) -> None:
limit=JobExecutionLimit.GROUP_ONCE,
on_condition=AddonsJobError,
)
async def uninstall(self, *, remove_config: bool) -> None:
async def uninstall(
self, *, remove_config: bool, remove_image: bool = True
) -> None:
"""Uninstall and cleanup this addon."""
try:
await self.instance.remove()
await self.instance.remove(remove_image=remove_image)
except DockerError as err:
raise AddonsError() from err

Expand Down
10 changes: 9 additions & 1 deletion supervisor/addons/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,15 @@ async def uninstall(self, slug: str, *, remove_config: bool = False) -> None:
_LOGGER.warning("Add-on %s is not installed", slug)
return

await self.local[slug].uninstall(remove_config=remove_config)
shared_image = any(
self.local[slug].image == addon.image
and self.local[slug].version == addon.version
for addon in self.installed
if addon.slug != slug
)
await self.local[slug].uninstall(
remove_config=remove_config, remove_image=not shared_image
)

_LOGGER.info("Add-on '%s' successfully removed", slug)

Expand Down
10 changes: 6 additions & 4 deletions supervisor/docker/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -429,15 +429,17 @@ def start(self) -> Awaitable[None]:
limit=JobExecutionLimit.GROUP_ONCE,
on_condition=DockerJobError,
)
async def remove(self) -> None:
async def remove(self, *, remove_image: bool = True) -> None:
"""Remove Docker images."""
# Cleanup container
with suppress(DockerError):
await self.stop()

await self.sys_run_in_executor(
self.sys_docker.remove_image, self.image, self.version
)
if remove_image:
await self.sys_run_in_executor(
self.sys_docker.remove_image, self.image, self.version
)

self._meta = None

@Job(
Expand Down
37 changes: 37 additions & 0 deletions tests/addons/test_manager.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Test addon manager."""

import asyncio
from copy import deepcopy
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, Mock, PropertyMock, patch

Expand All @@ -24,6 +25,7 @@
DockerNotFound,
)
from supervisor.plugins.dns import PluginDns
from supervisor.store.addon import AddonStore
from supervisor.store.repository import Repository
from supervisor.utils import check_exception_chain
from supervisor.utils.common import write_json_file
Expand Down Expand Up @@ -454,3 +456,38 @@ async def mock_update(*args, **kwargs):
await asyncio.sleep(0)
start.assert_called_once()
restart.assert_not_called()


async def test_shared_image_kept_on_uninstall(
coresys: CoreSys, install_addon_example: Addon
):
"""Test if two addons share an image it is not removed on uninstall."""
# Clone example to a new mock copy so two share an image
store = AddonStore(
coresys, "local_example2", deepcopy(coresys.addons.store["local_example"].data)
)
coresys.addons.data.install(store)
# pylint: disable-next=protected-access
coresys.addons.data._data = coresys.addons.data._schema(coresys.addons.data._data)

example_2 = Addon(coresys, store.slug)
coresys.addons.local[example_2.slug] = example_2

image = f"{install_addon_example.image}:{install_addon_example.version}"
latest = f"{install_addon_example.image}:latest"

await coresys.addons.uninstall("local_example2")
coresys.docker.images.remove.assert_not_called()
assert not coresys.addons.get("local_example2", local_only=True)

await coresys.addons.uninstall("local_example")
assert coresys.docker.images.remove.call_count == 2
assert coresys.docker.images.remove.call_args_list[0].kwargs == {
"image": latest,
"force": True,
}
assert coresys.docker.images.remove.call_args_list[1].kwargs == {
"image": image,
"force": True,
}
assert not coresys.addons.get("local_example", local_only=True)

0 comments on commit 85ed855

Please sign in to comment.