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,
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

View File

@ -185,7 +185,15 @@ class AddonManager(CoreSysAttributes):
_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)

View File

@ -429,15 +429,17 @@ class DockerInterface(JobGroup):
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(

View File

@ -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
@ -24,6 +25,7 @@ from supervisor.exceptions import (
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
@ -454,3 +456,38 @@ async def test_watchdog_runs_during_update(
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_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)