Keep shared images on addon uninstall (#5259)

* Keep shared images on addon uninstall

* Add missing step for mocking new addon in store
This commit is contained in:
Mike Degatano 2024-08-21 05:14:57 -04:00 committed by GitHub
parent 12d26b05af
commit 986b92aee4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 56 additions and 7 deletions

View File

@ -763,10 +763,12 @@ class Addon(AddonModel):
limit=JobExecutionLimit.GROUP_ONCE, limit=JobExecutionLimit.GROUP_ONCE,
on_condition=AddonsJobError, 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.""" """Uninstall and cleanup this addon."""
try: try:
await self.instance.remove() await self.instance.remove(remove_image=remove_image)
except DockerError as err: except DockerError as err:
raise AddonsError() from err raise AddonsError() from err

View File

@ -185,7 +185,15 @@ class AddonManager(CoreSysAttributes):
_LOGGER.warning("Add-on %s is not installed", slug) _LOGGER.warning("Add-on %s is not installed", slug)
return 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) _LOGGER.info("Add-on '%s' successfully removed", slug)

View File

@ -429,15 +429,17 @@ class DockerInterface(JobGroup):
limit=JobExecutionLimit.GROUP_ONCE, limit=JobExecutionLimit.GROUP_ONCE,
on_condition=DockerJobError, on_condition=DockerJobError,
) )
async def remove(self) -> None: async def remove(self, *, remove_image: bool = True) -> None:
"""Remove Docker images.""" """Remove Docker images."""
# Cleanup container # Cleanup container
with suppress(DockerError): with suppress(DockerError):
await self.stop() await self.stop()
if remove_image:
await self.sys_run_in_executor( await self.sys_run_in_executor(
self.sys_docker.remove_image, self.image, self.version self.sys_docker.remove_image, self.image, self.version
) )
self._meta = None self._meta = None
@Job( @Job(

View File

@ -1,6 +1,7 @@
"""Test addon manager.""" """Test addon manager."""
import asyncio import asyncio
from copy import deepcopy
from pathlib import Path from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, Mock, PropertyMock, patch from unittest.mock import AsyncMock, MagicMock, Mock, PropertyMock, patch
@ -24,6 +25,7 @@ from supervisor.exceptions import (
DockerNotFound, DockerNotFound,
) )
from supervisor.plugins.dns import PluginDns from supervisor.plugins.dns import PluginDns
from supervisor.store.addon import AddonStore
from supervisor.store.repository import Repository from supervisor.store.repository import Repository
from supervisor.utils import check_exception_chain from supervisor.utils import check_exception_chain
from supervisor.utils.common import write_json_file from supervisor.utils.common import write_json_file
@ -454,3 +456,38 @@ async def test_watchdog_runs_during_update(
await asyncio.sleep(0) await asyncio.sleep(0)
start.assert_called_once() start.assert_called_once()
restart.assert_not_called() 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_data = deepcopy(coresys.addons.store["local_example"].data)
store = AddonStore(coresys, "local_example2", store_data)
coresys.addons.store["local_example2"] = store
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)